From ec974b89fee1912232093fecd5bda345b59b13be Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 21 Jun 2024 17:26:30 +1000 Subject: [PATCH 01/54] Start extracting freezer changes for tree-states --- Cargo.lock | 383 +++++++-- Cargo.toml | 2 + beacon_node/beacon_chain/src/beacon_chain.rs | 24 +- beacon_node/beacon_chain/src/migrate.rs | 7 +- beacon_node/beacon_chain/src/schema_change.rs | 29 +- .../src/schema_change/migration_schema_v17.rs | 88 --- .../src/schema_change/migration_schema_v18.rs | 119 --- .../src/schema_change/migration_schema_v19.rs | 65 -- beacon_node/src/config.rs | 23 +- beacon_node/src/lib.rs | 2 +- beacon_node/store/Cargo.toml | 6 + beacon_node/store/src/config.rs | 239 +++++- beacon_node/store/src/errors.rs | 48 +- beacon_node/store/src/forwards_iter.rs | 22 +- beacon_node/store/src/hdiff.rs | 403 ++++++++++ beacon_node/store/src/hot_cold_store.rs | 748 ++++++++---------- beacon_node/store/src/lib.rs | 22 +- beacon_node/store/src/metadata.rs | 5 + beacon_node/store/src/partial_beacon_state.rs | 576 -------------- beacon_node/store/src/reconstruct.rs | 36 +- database_manager/src/lib.rs | 6 +- 21 files changed, 1363 insertions(+), 1490 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v17.rs delete mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs delete mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v19.rs create mode 100644 beacon_node/store/src/hdiff.rs delete mode 100644 beacon_node/store/src/partial_beacon_state.rs diff --git a/Cargo.lock b/Cargo.lock index 2e87f8b7f08..95fa28d1ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,7 +46,7 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "filesystem", - "rand", + "rand 0.8.5", "regex", "rpassword", "serde", @@ -237,7 +237,7 @@ dependencies = [ "k256 0.13.3", "keccak-asm", "proptest", - "rand", + "rand 0.8.5", "ruint", "serde", "tiny-keccak", @@ -476,7 +476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -486,7 +486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -674,6 +674,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.3.0", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -826,7 +835,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "proto_array", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "sensitive_url", @@ -935,6 +944,29 @@ dependencies = [ "shlex", ] +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.66", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1031,7 +1063,7 @@ dependencies = [ "ethereum_serde_utils", "ethereum_ssz", "hex", - "rand", + "rand 0.8.5", "serde", "tree_hash", "zeroize", @@ -1432,6 +1464,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" @@ -1629,7 +1670,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", ] @@ -1641,7 +1682,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", ] @@ -1653,7 +1694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -2092,7 +2133,7 @@ dependencies = [ "lru", "more-asserts", "parking_lot 0.11.2", - "rand", + "rand 0.8.5", "rlp", "smallvec", "socket2 0.4.10", @@ -2169,7 +2210,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.8", "subtle", @@ -2227,7 +2268,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", @@ -2247,7 +2288,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", @@ -2274,7 +2315,7 @@ dependencies = [ "hex", "k256 0.13.3", "log", - "rand", + "rand 0.8.5", "rlp", "serde", "sha3 0.10.8", @@ -2490,7 +2531,7 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand", + "rand 0.8.5", "scrypt", "serde", "serde_json", @@ -2531,7 +2572,7 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_repr", @@ -2763,7 +2804,7 @@ dependencies = [ "k256 0.11.6", "once_cell", "open-fastrlp", - "rand", + "rand 0.8.5", "rlp", "rlp-derive", "serde", @@ -2888,7 +2929,7 @@ dependencies = [ "lru", "parking_lot 0.12.3", "pretty_reqwest_error", - "rand", + "rand 0.8.5", "reqwest", "sensitive_url", "serde", @@ -2966,7 +3007,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", ] @@ -2976,7 +3017,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", ] @@ -3017,7 +3058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -3030,7 +3071,7 @@ checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "arbitrary", "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -3101,6 +3142,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" @@ -3378,7 +3425,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec 0.3.1", "quickcheck", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.10.8", @@ -3394,7 +3441,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", ] @@ -3405,7 +3452,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", ] @@ -3575,7 +3622,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "socket2 0.5.7", "thiserror", "tinyvec", @@ -3597,7 +3644,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -3654,6 +3701,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -3983,7 +4039,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.28", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -4057,7 +4113,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.3.0", "hashbrown 0.12.3", ] @@ -4516,7 +4572,7 @@ dependencies = [ "parking_lot 0.12.3", "pin-project", "quick-protobuf", - "rand", + "rand 0.8.5", "rw-stream-sink", "smallvec", "thiserror", @@ -4578,7 +4634,7 @@ dependencies = [ "multihash", "p256", "quick-protobuf", - "rand", + "rand 0.8.5", "sec1 0.7.3", "sha2 0.10.8", "thiserror", @@ -4600,7 +4656,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand", + "rand 0.8.5", "smallvec", "socket2 0.5.7", "tokio", @@ -4637,7 +4693,7 @@ dependencies = [ "libp2p-identity", "nohash-hasher", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "smallvec", "tracing", "unsigned-varint 0.7.2", @@ -4659,7 +4715,7 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand", + "rand 0.8.5", "sha2 0.10.8", "snow", "static_assertions", @@ -4700,7 +4756,7 @@ dependencies = [ "libp2p-tls", "parking_lot 0.12.3", "quinn", - "rand", + "rand 0.8.5", "ring 0.17.8", "rustls 0.23.8", "socket2 0.5.7", @@ -4726,7 +4782,7 @@ dependencies = [ "lru", "multistream-select", "once_cell", - "rand", + "rand 0.8.5", "smallvec", "tokio", "tracing", @@ -4835,7 +4891,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -4970,7 +5026,7 @@ dependencies = [ "prometheus-client", "quickcheck", "quickcheck_macros", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.9.9", @@ -5048,7 +5104,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg", + "autocfg 1.3.0", "scopeguard", ] @@ -5183,7 +5239,7 @@ name = "mdbx-sys" version = "0.11.6-4" source = "git+https://github.com/sigp/libmdbx-rs?tag=v0.1.4#096da80a83d14343f8df833006483f48075cd135" dependencies = [ - "bindgen", + "bindgen 0.59.2", "cc", "cmake", "libc", @@ -5207,7 +5263,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "autocfg", + "autocfg 1.3.0", ] [[package]] @@ -5528,7 +5584,7 @@ dependencies = [ "matches", "operation_pool", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "rlp", "slog", "slog-async", @@ -5641,7 +5697,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "serde", "smallvec", "zeroize", @@ -5668,7 +5724,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "autocfg", + "autocfg 1.3.0", "num-integer", "num-traits", ] @@ -5679,7 +5735,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg", + "autocfg 1.3.0", "libm", ] @@ -5829,7 +5885,7 @@ dependencies = [ "lighthouse_metrics", "maplit", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "rayon", "serde", "state_processing", @@ -5969,7 +6025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -6240,7 +6296,7 @@ dependencies = [ "hmac 0.12.1", "md-5", "memchr", - "rand", + "rand 0.8.5", "sha2 0.10.8", "stringprep", ] @@ -6285,6 +6341,16 @@ dependencies = [ "sensitive_url", ] +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.66", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -6413,9 +6479,9 @@ dependencies = [ "bitflags 2.5.0", "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.3", "rusty-fork", "tempfile", @@ -6509,7 +6575,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand", + "rand 0.8.5", ] [[package]] @@ -6548,7 +6614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e974563a4b1c2206bbc61191ca4da9c22e4308b4c455e8906751cc7828393f08" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring 0.17.8", "rustc-hash", "rustls 0.23.8", @@ -6613,6 +6679,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" @@ -6620,8 +6705,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]] @@ -6631,9 +6726,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" @@ -6643,13 +6753,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]] @@ -6684,6 +6856,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" @@ -6950,7 +7131,7 @@ dependencies = [ "parity-scale-codec 3.6.12", "primitive-types 0.12.2", "proptest", - "rand", + "rand 0.8.5", "rlp", "ruint-macro", "serde", @@ -7577,7 +7758,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]] @@ -7587,7 +7768,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]] @@ -7632,7 +7813,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.3.0", ] [[package]] @@ -7654,7 +7835,7 @@ dependencies = [ "lru", "maplit", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "rayon", "safe_arith", "serde", @@ -7835,7 +8016,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", @@ -7968,6 +8149,7 @@ name = "store" version = "0.2.0" dependencies = [ "beacon_chain", + "bls", "db-key", "directory", "ethereum_ssz", @@ -7976,15 +8158,20 @@ dependencies = [ "lazy_static", "leveldb", "lighthouse_metrics", + "logging", "lru", "parking_lot 0.12.3", + "safe_arith", "serde", "slog", "sloggers", + "smallvec", "state_processing", "strum", "tempfile", "types", + "xdelta3", + "zstd 0.13.1", ] [[package]] @@ -8261,7 +8448,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.8", @@ -8358,7 +8545,7 @@ dependencies = [ "hmac 0.12.1", "once_cell", "pbkdf2 0.11.0", - "rand", + "rand 0.8.5", "rustc-hash", "sha2 0.10.8", "thiserror", @@ -8469,7 +8656,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand", + "rand 0.8.5", "socket2 0.5.7", "tokio", "tokio-util", @@ -8793,8 +8980,8 @@ dependencies = [ "milhouse", "parking_lot 0.12.3", "paste", - "rand", - "rand_xorshift", + "rand 0.8.5", + "rand_xorshift 0.3.0", "rayon", "regex", "rpds", @@ -9014,7 +9201,7 @@ dependencies = [ "malloc_utils", "monitoring_api", "parking_lot 0.12.3", - "rand", + "rand 0.8.5", "reqwest", "ring 0.16.20", "safe_arith", @@ -9051,7 +9238,7 @@ dependencies = [ "filesystem", "hex", "lockfile", - "rand", + "rand 0.8.5", "tempfile", "tree_hash", "types", @@ -9313,7 +9500,7 @@ dependencies = [ "logging", "network", "r2d2", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -9379,6 +9566,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.34", +] + [[package]] name = "whoami" version = "1.5.1" @@ -9738,7 +9937,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", ] @@ -9760,6 +9959,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.20" @@ -9797,7 +10010,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -9813,7 +10026,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -9883,7 +10096,7 @@ dependencies = [ "pbkdf2 0.11.0", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -9892,7 +10105,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe 7.1.0", ] [[package]] @@ -9905,6 +10127,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.10+zstd.1.5.6" diff --git a/Cargo.toml b/Cargo.toml index b942d1719e2..4d40ca42d7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -237,6 +237,8 @@ unused_port = { path = "common/unused_port" } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } warp_utils = { path = "common/warp_utils" } +xdelta3 = { git = "http://github.com/michaelsproul/xdelta3-rs", rev="ae9a1d2585ef998f4656acdc35cf263ee88e6dfa" } +zstd = "0.13" [profile.maxperf] inherits = "release" diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 77e1bc095ed..ccf96e2ff05 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -779,7 +779,6 @@ impl BeaconChain { start_slot, local_head.beacon_state.clone(), local_head.beacon_block_root, - &self.spec, )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -804,12 +803,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(), 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| { @@ -879,7 +877,6 @@ impl BeaconChain { start_slot, local_head.beacon_state_root(), local_head.beacon_state.clone(), - &self.spec, )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -896,12 +893,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(), 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| { diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 08b2a51720d..f83535e92c5 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -24,6 +24,10 @@ 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; +/// Maximum number of blocks applied in each reconstruction burst. +/// +/// This limits the amount of time that the finalization migration is paused for. +const BLOCKS_PER_RECONSTRUCTION: usize = 8192 * 4; /// Default number of epochs to wait between finalization migrations. pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; @@ -201,7 +205,8 @@ impl, Cold: ItemStore> BackgroundMigrator>, log: &Logger) { - if let Err(e) = db.reconstruct_historic_states() { + // FIXME(sproul): still need to port more changes here + if let Err(e) = db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { error!( log, "State reconstruction failed"; diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 63eb72c43ab..6ac9052dccb 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -1,7 +1,4 @@ //! Utilities for managing database schema changes. -mod migration_schema_v17; -mod migration_schema_v18; -mod migration_schema_v19; use crate::beacon_chain::BeaconChainTypes; use crate::types::ChainSpec; @@ -52,32 +49,8 @@ pub fn migrate_schema( } // - // Migrations from before SchemaVersion(16) are deprecated. + // Migrations from before SchemaVersion(19) are deprecated. // - (SchemaVersion(16), SchemaVersion(17)) => { - let ops = migration_schema_v17::upgrade_to_v17::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(17), SchemaVersion(16)) => { - let ops = migration_schema_v17::downgrade_from_v17::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(17), SchemaVersion(18)) => { - let ops = migration_schema_v18::upgrade_to_v18::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(18), SchemaVersion(17)) => { - let ops = migration_schema_v18::downgrade_from_v18::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(18), SchemaVersion(19)) => { - let ops = migration_schema_v19::upgrade_to_v19::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(19), SchemaVersion(18)) => { - let ops = migration_schema_v19::downgrade_from_v19::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v17.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v17.rs deleted file mode 100644 index 770cbb8ab55..00000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v17.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, FORK_CHOICE_DB_KEY}; -use crate::persisted_fork_choice::{PersistedForkChoiceV11, PersistedForkChoiceV17}; -use proto_array::core::{SszContainerV16, SszContainerV17}; -use slog::{debug, Logger}; -use ssz::{Decode, Encode}; -use std::sync::Arc; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; - -pub fn upgrade_fork_choice( - mut fork_choice: PersistedForkChoiceV11, -) -> Result { - let ssz_container_v16 = SszContainerV16::from_ssz_bytes( - &fork_choice.fork_choice.proto_array_bytes, - ) - .map_err(|e| { - Error::SchemaMigrationError(format!( - "Failed to decode ProtoArrayForkChoice during schema migration: {:?}", - e - )) - })?; - - let ssz_container_v17: SszContainerV17 = ssz_container_v16.try_into().map_err(|e| { - Error::SchemaMigrationError(format!( - "Missing checkpoint during schema migration: {:?}", - e - )) - })?; - fork_choice.fork_choice.proto_array_bytes = ssz_container_v17.as_ssz_bytes(); - - Ok(fork_choice.into()) -} - -pub fn downgrade_fork_choice( - mut fork_choice: PersistedForkChoiceV17, -) -> Result { - let ssz_container_v17 = SszContainerV17::from_ssz_bytes( - &fork_choice.fork_choice.proto_array_bytes, - ) - .map_err(|e| { - Error::SchemaMigrationError(format!( - "Failed to decode ProtoArrayForkChoice during schema migration: {:?}", - e - )) - })?; - - let ssz_container_v16: SszContainerV16 = ssz_container_v17.into(); - fork_choice.fork_choice.proto_array_bytes = ssz_container_v16.as_ssz_bytes(); - - Ok(fork_choice.into()) -} - -pub fn upgrade_to_v17( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Get persisted_fork_choice. - let v11 = db - .get_item::(&FORK_CHOICE_DB_KEY)? - .ok_or_else(|| Error::SchemaMigrationError("fork choice missing from database".into()))?; - - let v17 = upgrade_fork_choice(v11)?; - - debug!( - log, - "Removing unused best_justified_checkpoint from fork choice store." - ); - - Ok(vec![v17.as_kv_store_op(FORK_CHOICE_DB_KEY)]) -} - -pub fn downgrade_from_v17( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Get persisted_fork_choice. - let v17 = db - .get_item::(&FORK_CHOICE_DB_KEY)? - .ok_or_else(|| Error::SchemaMigrationError("fork choice missing from database".into()))?; - - let v11 = downgrade_fork_choice(v17)?; - - debug!( - log, - "Adding junk best_justified_checkpoint to fork choice store." - ); - - Ok(vec![v11.as_kv_store_op(FORK_CHOICE_DB_KEY)]) -} 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 deleted file mode 100644 index 04a9da84128..00000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v18.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::beacon_chain::BeaconChainTypes; -use slog::{error, info, warn, Logger}; -use slot_clock::SlotClock; -use std::sync::Arc; -use std::time::Duration; -use store::{ - get_key_for_col, metadata::BLOB_INFO_KEY, DBColumn, Error, HotColdDB, KeyValueStoreOp, -}; -use types::{Epoch, EthSpec, Hash256, Slot}; - -/// The slot clock isn't usually available before the database is initialized, so we construct a -/// temporary slot clock by reading the genesis state. It should always exist if the database is -/// initialized at a prior schema version, however we still handle the lack of genesis state -/// gracefully. -fn get_slot_clock( - db: &HotColdDB, - log: &Logger, -) -> Result, Error> { - let spec = db.get_chain_spec(); - let Some(genesis_block) = db.get_blinded_block(&Hash256::zero())? else { - error!(log, "Missing genesis block"); - return Ok(None); - }; - let Some(genesis_state) = db.get_state(&genesis_block.state_root(), Some(Slot::new(0)))? else { - error!(log, "Missing genesis state"; "state_root" => ?genesis_block.state_root()); - return Ok(None); - }; - Ok(Some(T::SlotClock::new( - spec.genesis_slot, - Duration::from_secs(genesis_state.genesis_time()), - Duration::from_secs(spec.seconds_per_slot), - ))) -} - -fn get_current_epoch( - db: &Arc>, - log: &Logger, -) -> Result { - get_slot_clock::(db, log)? - .and_then(|clock| clock.now()) - .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) - .ok_or(Error::SlotClockUnavailableForMigration) -} - -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 { - let current_epoch = get_current_epoch::(&db, &log)?; - if current_epoch >= deneb_fork_epoch { - warn!( - log, - "Attempting upgrade to v18 schema"; - "info" => "this may not work as Deneb has already been activated" - ); - } else { - info!( - log, - "Upgrading to v18 schema"; - "info" => "ready for Deneb", - "epochs_until_deneb" => deneb_fork_epoch - current_epoch - ); - } - } else { - info!( - log, - "Upgrading to v18 schema"; - "info" => "ready for Deneb once it is scheduled" - ); - } - Ok(vec![]) -} - -pub fn downgrade_from_v18( - db: Arc>, - log: Logger, -) -> Result, Error> { - // We cannot downgrade from V18 once the Deneb fork has been activated, because there will - // be blobs and blob metadata in the database that aren't understood by the V17 schema. - if let Some(deneb_fork_epoch) = db.get_chain_spec().deneb_fork_epoch { - let current_epoch = get_current_epoch::(&db, &log)?; - if current_epoch >= deneb_fork_epoch { - error!( - log, - "Deneb already active: v18+ is mandatory"; - "current_epoch" => current_epoch, - "deneb_fork_epoch" => deneb_fork_epoch, - ); - return Err(Error::UnableToDowngrade); - } else { - info!( - log, - "Downgrading to v17 schema"; - "info" => "you will need to upgrade before Deneb", - "epochs_until_deneb" => deneb_fork_epoch - current_epoch - ); - } - } else { - info!( - log, - "Downgrading to v17 schema"; - "info" => "you need to upgrade before Deneb", - ); - } - - let ops = vec![KeyValueStoreOp::DeleteKey(get_key_for_col( - DBColumn::BeaconMeta.into(), - BLOB_INFO_KEY.as_bytes(), - ))]; - - Ok(ops) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v19.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v19.rs deleted file mode 100644 index 578e9bad314..00000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v19.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::beacon_chain::BeaconChainTypes; -use slog::{debug, info, Logger}; -use std::sync::Arc; -use store::{get_key_for_col, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp}; - -pub fn upgrade_to_v19( - db: Arc>, - log: Logger, -) -> Result, Error> { - let mut hot_delete_ops = vec![]; - let mut blob_keys = vec![]; - let column = DBColumn::BeaconBlob; - - debug!(log, "Migrating from v18 to v19"); - // Iterate through the blobs on disk. - for res in db.hot_db.iter_column_keys::>(column) { - let key = res?; - let key_col = get_key_for_col(column.as_str(), &key); - hot_delete_ops.push(KeyValueStoreOp::DeleteKey(key_col)); - blob_keys.push(key); - } - - let num_blobs = blob_keys.len(); - debug!(log, "Collected {} blob lists to migrate", num_blobs); - - let batch_size = 500; - let mut batch = Vec::with_capacity(batch_size); - - for key in blob_keys { - let next_blob = db.hot_db.get_bytes(column.as_str(), &key)?; - if let Some(next_blob) = next_blob { - let key_col = get_key_for_col(column.as_str(), &key); - batch.push(KeyValueStoreOp::PutKeyValue(key_col, next_blob)); - - if batch.len() >= batch_size { - db.blobs_db.do_atomically(batch.clone())?; - batch.clear(); - } - } - } - - // Process the remaining batch if it's not empty - if !batch.is_empty() { - db.blobs_db.do_atomically(batch)?; - } - - debug!(log, "Wrote {} blobs to the blobs db", num_blobs); - - // Delete all the blobs - info!(log, "Upgrading to v19 schema"); - Ok(hot_delete_ops) -} - -pub fn downgrade_from_v19( - _db: Arc>, - log: Logger, -) -> Result, Error> { - // No-op - info!( - log, - "Downgrading to v18 schema"; - ); - - Ok(vec![]) -} diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 35fad0718c1..de523eaf110 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -402,9 +402,7 @@ pub fn get_config( 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; + // FIXME(sproul): port hierarchy config if let Some(block_cache_size) = cli_args.get_one::("block-cache-size") { client_config.store.block_cache_size = block_cache_size @@ -1472,25 +1470,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 4ca084c3165..f398f7d67bd 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -12,7 +12,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/config.rs b/beacon_node/store/src/config.rs index d43999d8220..4fef9e16537 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,15 +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; @@ -17,14 +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. @@ -33,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). @@ -44,27 +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, @@ -75,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) } } @@ -106,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 91e6a920ba3..fc74c72733d 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,9 +1,10 @@ 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, EpochCacheError, Hash256, InconsistentFork, Slot}; +use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; pub type Result = std::result::Result; @@ -42,21 +43,40 @@ pub enum Error { expected: Hash256, computed: Hash256, }, + MissingStateRoot(Slot), + MissingState(Hash256), + MissingSnapshot(Slot), + 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), - RandaoMixOutOfBounds, - FinalizedStateDecreasingSlot, - FinalizedStateUnaligned, - StateForCacheHasPendingUpdates { - state_root: Hash256, - slot: Slot, - }, + MissingBlock(Hash256), } pub trait HandleUnavailable { @@ -109,6 +129,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) diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 1ccf1da1b7c..7c4da66b988 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -78,10 +78,24 @@ impl Root for StateRoots { 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() + let split_slot = store.get_split_slot(); + let anchor_info = store.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) + } } } 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 9c247c983a9..2343a71e6e3 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,15 +1,10 @@ -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::chunked_vector::BlockRoots; +use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; +use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; use crate::impls::beacon_state::{get_full_state, store_full_state}; 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, @@ -20,9 +15,9 @@ use crate::metrics; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, - PartialBeaconState, StoreItem, StoreOp, + StoreItem, StoreOp, }; -use itertools::process_results; +use itertools::{process_results, Itertools}; use leveldb::iterator::LevelDBIterator; use lru::LruCache; use parking_lot::{Mutex, RwLock}; @@ -35,12 +30,14 @@ use state_processing::{ SlotProcessingError, }; use std::cmp::min; +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}; /// On-disk database that stores finalized states efficiently. /// @@ -58,6 +55,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`. @@ -73,7 +71,11 @@ pub struct HotColdDB, Cold: ItemStore> { /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. state_cache: Mutex>, /// LRU cache of replayed states. + // 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. @@ -134,14 +136,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), @@ -174,7 +183,9 @@ impl HotColdDB, MemoryStore> { spec: ChainSpec, log: Logger, ) -> Result, MemoryStore>, Error> { - Self::verify_config(&config)?; + config.verify::()?; + + let hierarchy = config.hierarchy_config.to_moduli()?; let db = HotColdDB { split: RwLock::new(Split::default()), @@ -186,7 +197,9 @@ impl HotColdDB, MemoryStore> { block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + diff_buffer_cache: Mutex::new(LruCache::new(config.diff_buffer_cache_size)), config, + hierarchy, spec, log, _phantom: PhantomData, @@ -210,9 +223,11 @@ 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 mut db = HotColdDB { + let db = HotColdDB { split: RwLock::new(Split::default()), anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), @@ -222,31 +237,17 @@ impl HotColdDB, LevelDB> { block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + diff_buffer_cache: Mutex::new(LruCache::new(config.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 @@ -318,7 +319,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()?; @@ -796,14 +810,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state: BeaconState, end_block_root: Hash256, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsBlockRootsIterator::new( self, start_slot, None, || Ok((end_state, end_block_root)), - spec, + &self.spec, ) } @@ -812,9 +825,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, + start_slot, + Some(end_slot), + get_state, + &self.spec, + ) } pub fn forwards_state_roots_iterator( @@ -822,14 +840,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state_root: Hash256, end_state: BeaconState, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsStateRootsIterator::new( self, start_slot, None, || Ok((end_state, end_state_root)), - spec, + &self.spec, ) } @@ -838,9 +855,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) + HybridForwardsStateRootsIterator::new( + self, + start_slot, + Some(end_slot), + get_state, + &self.spec, + ) } /// Load an epoch boundary state by using the hot state summary look-up. @@ -1214,7 +1236,6 @@ impl, Cold: ItemStore> HotColdDB state.build_all_caches(&self.spec)?; let latest_block_root = state.get_latest_block_root(state_root); - let state_slot = state.slot(); if let PutStateOutcome::New = self.state_cache .lock() @@ -1224,7 +1245,7 @@ impl, Cold: ItemStore> HotColdDB self.log, "Cached ancestor state"; "state_root" => ?state_root, - "slot" => state_slot, + "slot" => slot, ); } Ok(()) @@ -1247,48 +1268,130 @@ impl, Cold: ItemStore> HotColdDB } } + 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(()) } @@ -1306,148 +1409,109 @@ 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)?; - let mut state: BeaconState = partial_state.try_into()?; - state.apply_pending_mutations()?; - Ok(state) - } + // 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, Some(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.historic_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; + // 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); - // 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; + 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.historic_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(); + // Load diff and apply it to buffer. + let diff = self.load_hdiff_for_slot(slot)?; + diff.apply(&mut buffer)?; - 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 mut state = self.replay_blocks(low_state, blocks, slot, Some(state_root_iter), None)?; - state.apply_pending_mutations()?; - - // If state is not error, put it in the cache. - self.historic_state_cache.lock().put(slot, state.clone()); - - 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> { + let block_root_iter = + self.forwards_block_roots_iterator_until(start_slot, end_slot, || { + panic!("FIXME(sproul): error here") + })?; + process_results(block_root_iter, |iter| { + iter.map(|(block_root, _slot)| block_root) + .dedup() + .map(|block_root| { + self.get_blinded_block(&block_root)? + .ok_or(Error::MissingBlock(block_root)) + }) + .collect() + })? } /// Load the blocks between `start_slot` and `end_slot` by backtracking from `end_block_hash`. @@ -1580,30 +1644,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) @@ -1636,16 +1676,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. @@ -1875,32 +1912,6 @@ impl, Cold: ItemStore> HotColdDB self.split.read_recursive().as_kv_store_op(SPLIT_KEY) } - /// Load the state root of a restore point. - fn load_restore_point_hash(&self, restore_point_index: u64) -> Result { - let key = Self::restore_point_key(restore_point_index); - self.cold_db - .get(&key)? - .map(|r: RestorePointHash| r.state_root) - .ok_or_else(|| HotColdDBError::MissingRestorePointHash(restore_point_index).into()) - } - - /// Store the state root of a restore point. - fn store_restore_point_hash( - &self, - restore_point_index: u64, - state_root: Hash256, - ops: &mut Vec, - ) { - let value = &RestorePointHash { state_root }; - let op = value.as_kv_store_op(Self::restore_point_key(restore_point_index)); - ops.push(op); - } - - /// Convert a `restore_point_index` into a database key. - fn restore_point_key(restore_point_index: u64) -> Hash256 { - Hash256::from_low_u64_be(restore_point_index) - } - /// Load a frozen state's slot, given its root. pub fn load_cold_state_slot(&self, state_root: &Hash256) -> Result, Error> { Ok(self @@ -1928,52 +1939,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()?; @@ -2258,21 +2223,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) => { @@ -2318,84 +2278,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 @@ -2407,9 +2289,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() { @@ -2440,6 +2319,7 @@ impl, Cold: ItemStore> HotColdDB let columns = [ DBColumn::BeaconState, DBColumn::BeaconStateSummary, + DBColumn::BeaconStateDiff, DBColumn::BeaconRestorePoint, DBColumn::BeaconStateRoots, DBColumn::BeaconHistoricalRoots, @@ -2571,26 +2451,20 @@ pub fn migrate_database, Cold: ItemStore>( } let mut hot_db_ops = vec![]; - let mut cold_db_ops = vec![]; - - // 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)?; - - // 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 mut cold_db_block_ops = vec![]; + 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 @@ -2599,12 +2473,18 @@ pub fn migrate_database, Cold: ItemStore>( hot_db_ops.push(StoreOp::DeleteExecutionPayload(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. @@ -2614,33 +2494,30 @@ 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![]; - 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 @@ -2651,8 +2528,7 @@ pub fn migrate_database, Cold: ItemStore>( // 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. - - // 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()?; { diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 66032d89c52..8f2ced91129 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -18,13 +18,13 @@ pub mod consensus_context; pub mod errors; mod forwards_iter; mod garbage_collection; +pub mod hdiff; pub mod hot_cold_store; mod impls; mod leveldb_store; mod memory_store; pub mod metadata; pub mod metrics; -mod partial_beacon_state; pub mod reconstruct; pub mod state_cache; @@ -36,7 +36,6 @@ pub use self::consensus_context::OnDiskConsensusContext; 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; @@ -222,13 +221,24 @@ 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, #[strum(serialize = "blb")] BeaconBlob, - /// For full `BeaconState`s in the hot database (finalized or fork-boundary states). + /// For full `BeaconState`s in the hot database (epoch 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, @@ -250,7 +260,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")] @@ -312,7 +322,9 @@ impl DBColumn { | Self::BeaconStateRoots | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries - | Self::BeaconRandaoMixes => 8, + | Self::BeaconRandaoMixes + | Self::BeaconStateSnapshot + | Self::BeaconStateDiff => 8, } } } diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 1675051bd80..e75efe65993 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -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/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs deleted file mode 100644 index e56d0580ac2..00000000000 --- a/beacon_node/store/src/partial_beacon_state.rs +++ /dev/null @@ -1,576 +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, Bellatrix, 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: List, - pub eth1_deposit_index: u64, - - // Registry - pub validators: List, - pub balances: List, - - // 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: Vector, - - // Attestations (genesis fork only) - #[superstruct(only(Base))] - pub previous_epoch_attestations: List, E::MaxPendingAttestations>, - #[superstruct(only(Base))] - pub current_epoch_attestations: List, E::MaxPendingAttestations>, - - // Participation (Altair and later) - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] - pub previous_epoch_participation: List, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] - pub current_epoch_participation: List, - - // Finality - pub justification_bits: BitVector, - pub previous_justified_checkpoint: Checkpoint, - pub current_justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - - // Inactivity - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] - pub inactivity_scores: List, - - // Light-client sync committees - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] - pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] - pub next_sync_committee: Arc>, - - // Execution - #[superstruct( - only(Bellatrix), - partial_getter(rename = "latest_execution_payload_header_bellatrix") - )] - pub latest_execution_payload_header: ExecutionPayloadHeaderBellatrix, - #[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>, - - // Electra - #[superstruct(only(Electra))] - pub deposit_receipts_start_index: u64, - #[superstruct(only(Electra))] - pub deposit_balance_to_consume: u64, - #[superstruct(only(Electra))] - pub exit_balance_to_consume: u64, - #[superstruct(only(Electra))] - pub earliest_exit_epoch: Epoch, - #[superstruct(only(Electra))] - pub consolidation_balance_to_consume: u64, - #[superstruct(only(Electra))] - pub earliest_consolidation_epoch: Epoch, - - // TODO(electra) should these be optional? - #[superstruct(only(Electra))] - pub pending_balance_deposits: List, - #[superstruct(only(Electra))] - pub pending_partial_withdrawals: - List, - #[superstruct(only(Electra))] - pub pending_consolidations: List, -} - -/// 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::Bellatrix(s) => impl_from_state_forgetful!( - s, - outer, - Bellatrix, - PartialBeaconStateBellatrix, - [ - 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, - deposit_receipts_start_index, - deposit_balance_to_consume, - exit_balance_to_consume, - earliest_exit_epoch, - consolidation_balance_to_consume, - earliest_consolidation_epoch, - pending_balance_deposits, - pending_partial_withdrawals, - pending_consolidations - ], - [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 - .get_mut(current_epoch.as_usize() % len) - .ok_or(Error::RandaoMixOutOfBounds)? = *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(), - - // 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::Bellatrix(inner) => impl_try_into_beacon_state!( - inner, - Bellatrix, - BeaconStateBellatrix, - [ - 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, - deposit_receipts_start_index, - deposit_balance_to_consume, - exit_balance_to_consume, - earliest_exit_epoch, - consolidation_balance_to_consume, - earliest_consolidation_epoch, - pending_balance_deposits, - pending_partial_withdrawals, - pending_consolidations - ], - [historical_summaries] - ), - }; - Ok(state) - } -} diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 8ef4886565c..623231ff300 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -8,7 +8,7 @@ use state_processing::{ 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 @@ -111,7 +105,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/database_manager/src/lib.rs b/database_manager/src/lib.rs index fafff0f0f9d..ee70da38f4a 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::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::{get_color_style, FLAG_HEADER}; use environment::{Environment, RuntimeContext}; @@ -251,10 +251,6 @@ 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")? { From df5e7160214fa2689526112413c2b4b3282efdaa Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:57:24 +0200 Subject: [PATCH 02/54] Remove unused config args --- beacon_node/store/src/config.rs | 29 +---------------------------- beacon_node/store/src/errors.rs | 1 - consensus/types/src/beacon_state.rs | 1 - 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 4fef9e16537..fd2aec529d3 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -41,8 +41,6 @@ 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. @@ -60,7 +58,6 @@ pub struct StoreConfig { #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] // FIXME(sproul): schema migration pub struct OnDiskStoreConfig { - pub linear_blocks: bool, pub hierarchy_config: HierarchyConfig, } @@ -96,7 +93,6 @@ impl Default for StoreConfig { 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, @@ -109,7 +105,6 @@ impl Default for StoreConfig { impl StoreConfig { pub fn as_disk_config(&self) -> OnDiskStoreConfig { OnDiskStoreConfig { - linear_blocks: self.linear_blocks, hierarchy_config: self.hierarchy_config.clone(), } } @@ -122,8 +117,7 @@ impl StoreConfig { ) -> Result<(), StoreConfigError> { 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 + (db_config.hierarchy_config == on_disk_config.hierarchy_config || anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot))) { Ok(()) @@ -231,11 +225,9 @@ mod test { #[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(); @@ -244,30 +236,13 @@ mod test { .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], }, @@ -281,11 +256,9 @@ mod test { #[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], }, diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index fc74c72733d..41477883bdf 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -62,7 +62,6 @@ pub enum Error { }, AddPayloadLogicError, SlotClockUnavailableForMigration, - MissingImmutableValidator(usize), MissingValidator(usize), V9MigrationFailure(Hash256), ValidatorPubkeyCacheError(String), diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 577f282a556..8049d27d8f1 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -155,7 +155,6 @@ pub enum Error { current_fork: ForkName, }, TotalActiveBalanceDiffUninitialized, - MissingImmutableValidator(usize), IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), From 17ce7d0bf01795c24c55f99ae6dccd9c888da8ae Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:57:46 +0200 Subject: [PATCH 03/54] Add comments --- beacon_node/store/src/hdiff.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 962b602a51b..c124848b9bf 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -16,10 +16,28 @@ pub enum Error { UnableToComputeDiff, UnableToApplyDiff, Compression(std::io::Error), + InvalidSSZState(ssz::DecodeError), + InvalidBalancesLength, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] pub struct HierarchyConfig { + /// A sequence of powers of two to define how frequently to store each layer of state diffs. + /// The last value always represents the frequency of full state snapshots. Adding more + /// exponents increases the number of diff layers. This value allows to customize the trade-off + /// between reconstruction speed and disk space. + /// + /// Consider an example `exponents value of `[5,13,21]`. This means we have 3 layers: + /// - Full state stored every 2^21 slots (2097152 slots or 291 days) + /// - First diff layer stored every 2^13 slots (8192 slots or 2.3 hours) + /// - Second diff layer stored every 2^5 slots (32 slots or 1 epoch) + /// + /// To reconstruct a state at slot 3,000,003 we load each closest layer + /// - Layer 0: 3000003 - (3000003 mod 2^21) = 2097152 + /// - Layer 1: 3000003 - (3000003 mod 2^13) = 2998272 + /// - Layer 2: 3000003 - (3000003 mod 2^5) = 3000000 + /// Layer 0 is full state snaphost, apply layer 1 diff, then apply layer 2 diff and then replay + /// blocks 3,000,001 to 3,000,003. pub exponents: Vec, } @@ -63,6 +81,19 @@ pub struct HDiffBuffer { } /// Hierarchical state diff. +/// +/// Splits the diff into two data sections: +/// +/// - **balances**: The balance of each active validator is almost certain to change every epoch. +/// So this is the field in the state with most entropy. However the balance changes are small. +/// We can optimize the diff significantly by computing the balance difference first and then +/// compressing the result to squash those leading zero bytes. +/// +/// - **everything else**: Instead of trying to apply heuristics and be clever on each field, +/// running a generic binary diff algorithm on the rest of fields yields very good results. With +/// this strategy the HDiff code is easily mantainable across forks, as new fields are covered +/// automatically. xdelta3 algorithm showed diff compute and apply times of ~200 ms on a mainnet +/// state from Apr 2023 (570k indexes), and a 92kB diff size. #[derive(Debug, Encode, Decode)] pub struct HDiff { state_diff: BytesDiff, From 3c5d722078ee3e4258960cef5d2d7a10c098eebd Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:58:09 +0200 Subject: [PATCH 04/54] Remove unwraps --- beacon_node/store/src/hdiff.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index c124848b9bf..3a3c9834897 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -112,6 +112,7 @@ pub struct CompressedU64Diff { impl HDiffBuffer { pub fn from_state(mut beacon_state: BeaconState) -> Self { + // Set state.balances to empty list, and then serialize state as ssz let balances_list = std::mem::take(beacon_state.balances_mut()); let state = beacon_state.as_ssz_bytes(); @@ -121,8 +122,10 @@ impl HDiffBuffer { } 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(); + let mut state = + BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSSZState)?; + *state.balances_mut() = + List::new(self.balances).map_err(|_| Error::InvalidBalancesLength)?; Ok(state) } } From 31bcd8491ea27c10774ee41d35cea31975eba3d6 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:58:22 +0200 Subject: [PATCH 05/54] Subjective more clear implementation --- beacon_node/store/src/hdiff.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 3a3c9834897..78cc89a0425 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -282,34 +282,35 @@ impl HierarchyConfig { impl HierarchyModuli { pub fn storage_strategy(&self, slot: Slot) -> Result { + // last = full snapshot interval let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; + // first = most frequent diff layer, need to replay blocks from this layer 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 + Ok(self .moduli .iter() .rev() .tuple_windows() .find_map(|(&n_big, &n_small)| { - (slot % n_small == 0).then(|| { + if slot % n_small == 0 { // Diff from the previous layer. - slot / n_big * n_big - }) - }); - - Ok(diff_from.map_or( - StorageStrategy::ReplayFrom(replay_from), - StorageStrategy::DiffFrom, - )) + StorageStrategy::DiffFrom(slot / n_big * n_big) + } else { + // Keep trying with next layer + None + } + }) + // Exhausted layers, need to replay from most frequent layer + .unwrap_or(StorageStrategy::ReplayFrom(slot / first * first))) } /// Return the smallest slot greater than or equal to `slot` at which a full snapshot should From 394abba4d868a6a88a2da08ce65ad02888b97aea Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 2 Jul 2024 18:40:02 +1000 Subject: [PATCH 06/54] Clean up hdiff --- beacon_node/src/config.rs | 6 ++-- beacon_node/store/src/config.rs | 5 ++- beacon_node/store/src/hdiff.rs | 45 +++++++++++++++---------- beacon_node/store/src/hot_cold_store.rs | 4 +-- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index de523eaf110..d623133e071 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -402,8 +402,6 @@ pub fn get_config( client_config.blobs_db_path = Some(PathBuf::from(blobs_db_dir)); } - // FIXME(sproul): port hierarchy config - if let Some(block_cache_size) = cli_args.get_one::("block-cache-size") { client_config.store.block_cache_size = block_cache_size .parse() @@ -434,6 +432,10 @@ pub fn get_config( client_config.store.prune_payloads = prune_payloads; } + 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")? { diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index fd2aec529d3..e4efd0559fe 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -117,8 +117,8 @@ impl StoreConfig { ) -> Result<(), StoreConfigError> { let db_config = self.as_disk_config(); // Allow changing the hierarchy exponents if no historic states are stored. - (db_config.hierarchy_config == on_disk_config.hierarchy_config - || anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot))) + if db_config.hierarchy_config == on_disk_config.hierarchy_config + || anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot)) { Ok(()) } else { @@ -236,7 +236,6 @@ mod test { .is_ok()); } - #[test] fn check_compatibility_hierarchy_config_incompatible() { let store_config = StoreConfig { diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 78cc89a0425..80716bac1e3 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -1,5 +1,5 @@ //! Hierarchical diff implementation. -use crate::{DBColumn, StoreItem}; +use crate::{DBColumn, StoreConfig, StoreItem}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; @@ -15,8 +15,9 @@ pub enum Error { U64DiffDeletionsNotSupported, UnableToComputeDiff, UnableToApplyDiff, + BalancesIncompleteChunk, Compression(std::io::Error), - InvalidSSZState(ssz::DecodeError), + InvalidSszState(ssz::DecodeError), InvalidBalancesLength, } @@ -123,7 +124,7 @@ impl HDiffBuffer { pub fn into_state(self, spec: &ChainSpec) -> Result, Error> { let mut state = - BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSSZState)?; + BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSszState)?; *state.balances_mut() = List::new(self.balances).map_err(|_| Error::InvalidBalancesLength)?; Ok(state) @@ -131,9 +132,13 @@ impl HDiffBuffer { } impl HDiff { - pub fn compute(source: &HDiffBuffer, target: &HDiffBuffer) -> Result { + pub fn compute( + source: &HDiffBuffer, + target: &HDiffBuffer, + config: &StoreConfig, + ) -> Result { let state_diff = BytesDiff::compute(&source.state, &target.state)?; - let balances_diff = CompressedU64Diff::compute(&source.balances, &target.balances)?; + let balances_diff = CompressedU64Diff::compute(&source.balances, &target.balances, config)?; Ok(Self { state_diff, @@ -141,11 +146,11 @@ impl HDiff { }) } - pub fn apply(&self, source: &mut HDiffBuffer) -> Result<(), Error> { + pub fn apply(&self, source: &mut HDiffBuffer, config: &StoreConfig) -> 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)?; + self.balances_diff.apply(&mut source.balances, config)?; Ok(()) } @@ -194,7 +199,7 @@ impl BytesDiff { } impl CompressedU64Diff { - pub fn compute(xs: &[u64], ys: &[u64]) -> Result { + pub fn compute(xs: &[u64], ys: &[u64], config: &StoreConfig) -> Result { if xs.len() > ys.len() { return Err(Error::U64DiffDeletionsNotSupported); } @@ -209,9 +214,9 @@ impl CompressedU64Diff { }) .collect(); - // FIXME(sproul): reconsider - let compression_level = 1; - let mut compressed_bytes = Vec::with_capacity(uncompressed_bytes.len() / 2); + let compression_level = config.compression_level; + let mut compressed_bytes = + Vec::with_capacity(config.estimate_compressed_size(uncompressed_bytes.len())); let mut encoder = Encoder::new(&mut compressed_bytes, compression_level).map_err(Error::Compression)?; encoder @@ -224,9 +229,10 @@ impl CompressedU64Diff { }) } - pub fn apply(&self, xs: &mut Vec) -> Result<(), Error> { + pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { // Decompress balances diff. - let mut balances_diff_bytes = Vec::with_capacity(2 * self.bytes.len()); + let mut balances_diff_bytes = + Vec::with_capacity(config.estimate_decompressed_size(self.bytes.len())); let mut decoder = Decoder::new(&*self.bytes).map_err(Error::Compression)?; decoder .read_to_end(&mut balances_diff_bytes) @@ -236,8 +242,10 @@ impl CompressedU64Diff { .chunks(u64::BITS as usize / 8) .enumerate() { - // FIXME(sproul): unwrap - let diff = u64::from_be_bytes(diff_bytes.try_into().unwrap()); + let diff = diff_bytes + .try_into() + .map(u64::from_be_bytes) + .map_err(|_| Error::BalancesIncompleteChunk)?; if let Some(x) = xs.get_mut(i) { *x = x.wrapping_add(diff); @@ -303,7 +311,7 @@ impl HierarchyModuli { .find_map(|(&n_big, &n_small)| { if slot % n_small == 0 { // Diff from the previous layer. - StorageStrategy::DiffFrom(slot / n_big * n_big) + Some(StorageStrategy::DiffFrom(slot / n_big * n_big)) } else { // Keep trying with next layer None @@ -411,6 +419,7 @@ mod tests { 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 config = &StoreConfig::default(); let to_bytes = |nums: &[u64]| -> Vec { nums.iter().flat_map(|x| x.to_be_bytes()).collect() }; @@ -418,10 +427,10 @@ mod tests { 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 u64_diff = CompressedU64Diff::compute(&x_values, &y_values, config).unwrap(); let mut y_from_u64_diff = x_values; - u64_diff.apply(&mut y_from_u64_diff).unwrap(); + u64_diff.apply(&mut y_from_u64_diff, config).unwrap(); assert_eq!(y_values, y_from_u64_diff); diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 2343a71e6e3..43975907af8 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1384,7 +1384,7 @@ impl, Cold: ItemStore> HotColdDB // 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 = HDiff::compute(&base_buffer, &target_buffer, &self.config)?; let diff_bytes = diff.as_ssz_bytes(); let key = get_key_for_col( @@ -1480,7 +1480,7 @@ impl, Cold: ItemStore> HotColdDB // Load diff and apply it to buffer. let diff = self.load_hdiff_for_slot(slot)?; - diff.apply(&mut buffer)?; + diff.apply(&mut buffer, &self.config)?; self.diff_buffer_cache.lock().put(slot, buffer.clone()); debug!( From cac767207eef652785cf61a4c76684d3141878ee Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 4 Jul 2024 12:24:00 +1000 Subject: [PATCH 07/54] Update xdelta3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95fa28d1ebf..d9a3fb54996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9962,7 +9962,7 @@ dependencies = [ [[package]] name = "xdelta3" version = "0.1.5" -source = "git+http://github.com/michaelsproul/xdelta3-rs?rev=ae9a1d2585ef998f4656acdc35cf263ee88e6dfa#ae9a1d2585ef998f4656acdc35cf263ee88e6dfa" +source = "git+http://github.com/sigp/xdelta3-rs?rev=2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f#2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f" dependencies = [ "bindgen 0.66.1", "cc", diff --git a/Cargo.toml b/Cargo.toml index 4d40ca42d7f..ced2055ee3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -237,7 +237,7 @@ unused_port = { path = "common/unused_port" } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } warp_utils = { path = "common/warp_utils" } -xdelta3 = { git = "http://github.com/michaelsproul/xdelta3-rs", rev="ae9a1d2585ef998f4656acdc35cf263ee88e6dfa" } +xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f" } zstd = "0.13" [profile.maxperf] From b87c6bb04cc05ac98eb2de202e46e0600bf7055d Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 4 Jul 2024 05:03:28 +0200 Subject: [PATCH 08/54] Tree states archive metrics (#6040) * Add store cache size metrics * Add compress timer metrics * Add diff apply compute timer metrics * Add diff buffer cache hit metrics * Add hdiff buffer load times * Add blocks replayed metric * Move metrics to store * Future proof some metrics --------- Co-authored-by: Michael Sproul --- beacon_node/beacon_chain/src/metrics.rs | 1 + beacon_node/store/src/hdiff.rs | 5 + beacon_node/store/src/hot_cold_store.rs | 119 ++++++++++++++++++------ beacon_node/store/src/metrics.rs | 64 +++++++++++++ 4 files changed, 162 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 4ceaf675cec..f1ed45878c8 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1194,6 +1194,7 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { let attestation_stats = beacon_chain.op_pool.attestation_stats(); let chain_metrics = beacon_chain.metrics(); + // Kept duplicated for backwards compatibility set_gauge_by_usize( &BLOCK_PROCESSING_SNAPSHOT_CACHE_SIZE, beacon_chain.store.state_cache_len(), diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 80716bac1e3..b6064afe706 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -129,6 +129,11 @@ impl HDiffBuffer { List::new(self.balances).map_err(|_| Error::InvalidBalancesLength)?; Ok(state) } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.state.len() + self.balances.len() + } } impl HDiff { diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 43975907af8..3fbb5e1c33b 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -387,6 +387,41 @@ impl, Cold: ItemStore> HotColdDB self.state_cache.lock().len() } + pub fn register_metrics(&self) { + let diff_buffer_cache = self.diff_buffer_cache.lock(); + let diff_buffer_cache_byte_size = diff_buffer_cache + .iter() + .map(|(_, diff)| diff.size()) + .sum::(); + let diff_buffer_cache_len = diff_buffer_cache.len(); + drop(diff_buffer_cache); + + metrics::set_gauge( + &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, + self.block_cache.lock().block_cache.len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_BLOB_CACHE_SIZE, + self.block_cache.lock().blob_cache.len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_STATE_CACHE_SIZE, + self.state_cache.lock().len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_HISTORIC_STATE_CACHE_SIZE, + self.historic_state_cache.lock().len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_DIFF_BUFFER_CACHE_SIZE, + diff_buffer_cache_len as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_DIFF_BUFFER_CACHE_BYTE_SIZE, + diff_buffer_cache_byte_size as i64, + ); + } + /// Store a block and update the LRU cache. pub fn put_block( &self, @@ -1335,12 +1370,15 @@ impl, Cold: ItemStore> HotColdDB 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 compressed_value = { + let _timer = metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_COMPRESS_TIME); + let mut out = Vec::with_capacity(self.config.estimate_compressed_size(bytes.len())); + let mut encoder = Encoder::new(&mut out, self.config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + out + }; let key = get_key_for_col( DBColumn::BeaconStateSnapshot.into(), @@ -1356,6 +1394,8 @@ impl, Cold: ItemStore> HotColdDB &slot.as_u64().to_be_bytes(), )? { Some(bytes) => { + let _timer = + metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME); let mut ssz_bytes = Vec::with_capacity(self.config.estimate_decompressed_size(bytes.len())); let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; @@ -1382,9 +1422,12 @@ impl, Cold: ItemStore> HotColdDB ops: &mut Vec, ) -> Result<(), Error> { // Load diff base state bytes. - let (_, base_buffer) = self.load_hdiff_buffer_for_slot(from_slot)?; + let (_, base_buffer) = self.load_hdiff_buffer_for_slot(from_slot, 0)?; let target_buffer = HDiffBuffer::from_state(state.clone()); - let diff = HDiff::compute(&base_buffer, &target_buffer, &self.config)?; + let diff = { + let _timer = metrics::start_timer(&metrics::STORE_BEACON_DIFF_BUFFER_COMPUTE_TIME); + HDiff::compute(&base_buffer, &target_buffer, &self.config)? + }; let diff_bytes = diff.as_ssz_bytes(); let key = get_key_for_col( @@ -1409,7 +1452,7 @@ 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> { - let (base_slot, hdiff_buffer) = self.load_hdiff_buffer_for_slot(slot)?; + let (base_slot, hdiff_buffer) = self.load_hdiff_buffer_for_slot(slot, 0)?; let base_state = hdiff_buffer.into_state(&self.spec)?; debug_assert_eq!(base_slot, base_state.slot()); @@ -1442,20 +1485,31 @@ impl, Cold: ItemStore> HotColdDB /// 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> { + fn load_hdiff_buffer_for_slot( + &self, + slot: Slot, + recursion: usize, + ) -> Result<(Slot, HDiffBuffer), Error> { if let Some(buffer) = self.diff_buffer_cache.lock().get(&slot) { debug!( self.log, "Hit diff buffer cache"; "slot" => slot ); + metrics::inc_counter(&metrics::STORE_BEACON_DIFF_BUFFER_CACHE_HIT); return Ok((slot, buffer.clone())); + } else { + metrics::inc_counter(&metrics::STORE_BEACON_DIFF_BUFFER_CACHE_MISS); } + // Do not time recursive calls into load_hdiff_buffer_for_slot to not double count + let _timer = (recursion == 0) + .then(|| metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME)); + // 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)? { + match self.hierarchy.storage_strategy(slot)? { // Base case. StorageStrategy::Snapshot => { let state = self @@ -1471,26 +1525,35 @@ impl, Cold: ItemStore> HotColdDB "slot" => slot ); - return Ok((slot, buffer)); + Ok((slot, buffer)) } // Recursive case. - StorageStrategy::DiffFrom(from) => self.load_hdiff_buffer_for_slot(from)?, - StorageStrategy::ReplayFrom(from) => return self.load_hdiff_buffer_for_slot(from), - }; - - // Load diff and apply it to buffer. - let diff = self.load_hdiff_for_slot(slot)?; - diff.apply(&mut buffer, &self.config)?; + StorageStrategy::DiffFrom(from) => { + let (_buffer_slot, mut buffer) = + self.load_hdiff_buffer_for_slot(from, recursion + 1)?; + + // Load diff and apply it to buffer. + let diff = self.load_hdiff_for_slot(slot)?; + { + let _timer = + metrics::start_timer(&metrics::STORE_BEACON_DIFF_BUFFER_APPLY_TIME); + diff.apply(&mut buffer, &self.config)?; + } - 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 - ); + 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 + ); - Ok((slot, buffer)) + Ok((slot, buffer)) + } + StorageStrategy::ReplayFrom(from) => { + self.load_hdiff_buffer_for_slot(from, recursion + 1) + } + } } /// Load cold blocks between `start_slot` and `end_slot` inclusive. @@ -1566,6 +1629,8 @@ impl, Cold: ItemStore> HotColdDB state_root_iter: Option>>, pre_slot_hook: Option>, ) -> Result, Error> { + metrics::inc_counter_by(&metrics::STORE_BEACON_REPLAYED_BLOCKS, blocks.len() as u64); + let mut block_replayer = BlockReplayer::new(state, &self.spec) .no_signature_verification() .minimal_block_root_verification(); diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 2d901fdd932..d667ebc1a6e 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -129,6 +129,70 @@ lazy_static! { "store_beacon_block_write_bytes_total", "Total number of beacon block bytes written to the DB" ); + + /* + * Caches + */ + + /* + * Store metrics + */ + pub static ref STORE_BEACON_BLOCK_CACHE_SIZE: Result = try_create_int_gauge( + "store_beacon_block_cache_size", + "Current count of items in beacon store block cache", + ); + pub static ref STORE_BEACON_BLOB_CACHE_SIZE: Result = try_create_int_gauge( + "store_beacon_blob_cache_size", + "Current count of items in beacon store blob cache", + ); + pub static ref STORE_BEACON_STATE_CACHE_SIZE: Result = try_create_int_gauge( + "store_beacon_state_cache_size", + "Current count of items in beacon store state cache", + ); + pub static ref STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: Result = try_create_int_gauge( + "store_beacon_historic_state_cache_size", + "Current count of items in beacon store historic state cache", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_CACHE_SIZE: Result = try_create_int_gauge( + "store_beacon_diff_buffer_cache_size", + "Current count of items in beacon store diff buffer cache", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_CACHE_BYTE_SIZE: Result = try_create_int_gauge( + "store_beacon_diff_buffer_cache_byte_size", + "Current byte size sum of all elements in beacon store diff buffer cache", + ); + pub static ref STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: Result = try_create_histogram( + "store_beacon_state_compress_seconds", + "Time taken to compress a state snapshot for the freezer DB", + ); + pub static ref STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME: Result = try_create_histogram( + "store_beacon_state_decompress_seconds", + "Time taken to decompress a state snapshot for the freezer DB", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_APPLY_TIME: Result = try_create_histogram( + "store_beacon_diff_buffer_apply_seconds", + "Time taken to apply diff buffer to a state buffer", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_COMPUTE_TIME: Result = try_create_histogram( + "store_beacon_diff_buffer_compute_seconds", + "Time taken to compute diff buffer to a state buffer", + ); + pub static ref STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: Result = try_create_histogram( + "store_beacon_hdiff_buffer_load_seconds", + "Time taken to load an hdiff buffer", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_CACHE_HIT: Result = try_create_int_counter( + "store_beacon_diff_buffer_cache_hit_total", + "Total count of diff buffer cache hits", + ); + pub static ref STORE_BEACON_DIFF_BUFFER_CACHE_MISS: Result = try_create_int_counter( + "store_beacon_diff_buffer_cache_miss_total", + "Total count of diff buffer cache miss", + ); + pub static ref STORE_BEACON_REPLAYED_BLOCKS: Result = try_create_int_counter( + "store_beacon_replayed_blocks_total", + "Total count of replayed blocks", + ); } /// Updates the global metrics registry with store-related information. From e578f5d26598febee911fe91b1f734e7873e761b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 5 Jul 2024 11:18:29 +1000 Subject: [PATCH 09/54] Port and clean up forwards iterator changes --- beacon_node/store/src/errors.rs | 5 +- beacon_node/store/src/forwards_iter.rs | 253 +++++++++++++++--------- beacon_node/store/src/hot_cold_store.rs | 8 +- beacon_node/store/src/lib.rs | 7 + 4 files changed, 174 insertions(+), 99 deletions(-) diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 41477883bdf..704ceb49e27 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,7 +1,7 @@ use crate::chunked_vector::ChunkError; use crate::config::StoreConfigError; -use crate::hdiff; use crate::hot_cold_store::HotColdDBError; +use crate::{hdiff, DBColumn}; use ssz::DecodeError; use state_processing::BlockReplayError; use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; @@ -75,6 +75,9 @@ pub enum Error { InconsistentFork(InconsistentFork), ZeroCacheSize, CacheBuildError(EpochCacheError), + ForwardsIterInvalidColumn(DBColumn), + ForwardsIterGap(DBColumn, Slot, Slot), + ForwardsIterBadStart(DBColumn, Slot), MissingBlock(Hash256), } diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 7c4da66b988..946047136bd 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 { + 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 { + Err(Error::ForwardsIterInvalidColumn(column)) + } + } -impl Root for BlockRoots { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, + 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, + 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,65 +63,126 @@ impl Root for StateRoots { Ok(SimpleForwardsIterator { values }) } - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option { - let split_slot = store.get_split_slot(); - let anchor_info = store.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 - }) { + /// Compute the maximum /slot (exclusive) + fn freezer_upper_bound_for_column( + &self, + column: DBColumn, + start_slot: Slot, + ) -> Result> { + if column == DBColumn::BeaconBlockRoots { + Ok(self.freezer_upper_bound_for_block_roots(start_slot)) + } else if column == DBColumn::BeaconStateRoots { + Ok(self.freezer_upper_bound_for_state_roots(start_slot)) + } else { + Err(Error::ForwardsIterInvalidColumn(column)) + } + } + + fn freezer_upper_bound_for_block_roots(&self, start_slot: Slot) -> Option { + let oldest_block_slot = self.get_oldest_block_slot(); + if start_slot == 0 { + // Slot 0 block root is always available. + Some(Slot::new(1)) + } else if start_slot < oldest_block_slot { + // Non-zero block roots are not available prior to the `oldest_block_slot`. 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) + // Block roots are stored for all slots up to the split slot (exclusive). + Some(self.get_split_slot()) + } + } + + fn freezer_upper_bound_for_state_roots(&self, start_slot: Slot) -> Option { + let split_slot = self.get_split_slot(); + let anchor_info = self.get_anchor_info(); + + match anchor_info { + Some(anchor) => { + if start_slot <= anchor.state_lower_limit { + // Starting slot is prior to lower limit, so that's the upper limit. We can't + // iterate past the lower limit into the gap. The +1 accounts for exclusivity. + Some(anchor.state_lower_limit + 1) + } else if start_slot >= anchor.state_upper_limit { + // Starting slot is after the upper limit, so the split is the upper limit. + // The split state's root is not available in the freezer so this is exclusive. + Some(split_slot) + } else { + // In the gap, nothing is available. + None + } + } + None => { + // No anchor indicates that all state roots up to the split slot are available. + Some(split_slot) + } } } } -/// 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>, +/// Forwards root iterator that makes use of a slot -> root mapping in the freezer DB. +pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + inner: ColumnIter<'a, Vec>, + column: DBColumn, + 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, - ) -> Self { - Self { - inner: ChunkedVectorIter::new( - store, - start_slot.as_usize(), - last_restore_point_slot, - spec, - ), + end_slot: Slot, + ) -> Result { + if column != DBColumn::BeaconBlockRoots && column != DBColumn::BeaconStateRoots { + return Err(Error::ForwardsIterInvalidColumn(column)); } + let start = start_slot.as_u64().to_be_bytes(); + Ok(Self { + inner: store.cold_db.iter_column_from(column, &start), + column, + 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)| { + let slot = slot_bytes + .try_into() + .map(u64::from_be_bytes) + .map(Slot::new) + .map_err(|_| Error::InvalidBytes)?; + if root_bytes.len() != std::mem::size_of::() { + return Err(Error::InvalidBytes); + } + let root = Hash256::from_slice(&root_bytes); + + if slot != self.next_slot { + return Err(Error::ForwardsIterGap(self.column, slot, self.next_slot)); + } + self.next_slot += 1; + + Ok(Some((root, slot))) + }) + .transpose() } } @@ -153,10 +202,12 @@ 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, + column: DBColumn, /// Data required by the `PostFinalization` iterator when we get to it. continuation_data: Option, Hash256)>>, }, @@ -164,6 +215,7 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, C continuation_data: Option, Hash256)>>, store: &'a HotColdDB, start_slot: Slot, + column: DBColumn, }, PostFinalization { iter: SimpleForwardsIterator, @@ -171,8 +223,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. /// @@ -188,37 +240,41 @@ 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_bound = store + .freezer_upper_bound_for_column(column, start_slot)? + .ok_or(Error::ForwardsIterBadStart(column, start_slot))?; - let result = if start_slot < freezer_upper_limit { + let result = if start_slot < freezer_upper_bound { let iter = Box::new(FrozenForwardsIterator::new( store, + column, start_slot, - freezer_upper_limit, - spec, - )); + freezer_upper_bound, + )?); // No continuation data is needed if the forwards iterator plans to halt before // `end_slot`. If it tries to continue further a `NoContinuationData` error will be // returned. let continuation_data = - if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_limit) { + if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { None } else { Some(Box::new(get_state()?)) }; PreFinalization { iter, + store, end_slot, + column, continuation_data, } } else { @@ -226,6 +282,7 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> continuation_data: Some(Box::new(get_state()?)), store, start_slot, + column, } }; @@ -239,29 +296,31 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> PreFinalization { iter, end_slot, + store, 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() @@ -272,11 +331,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() } @@ -286,8 +351,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/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 3fbb5e1c33b..036abb64311 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -848,10 +848,10 @@ impl, Cold: ItemStore> HotColdDB ) -> Result> + '_, Error> { HybridForwardsBlockRootsIterator::new( self, + DBColumn::BeaconBlockRoots, start_slot, None, || Ok((end_state, end_block_root)), - &self.spec, ) } @@ -863,10 +863,10 @@ impl, Cold: ItemStore> HotColdDB ) -> Result, Error> { HybridForwardsBlockRootsIterator::new( self, + DBColumn::BeaconBlockRoots, start_slot, Some(end_slot), get_state, - &self.spec, ) } @@ -878,10 +878,10 @@ impl, Cold: ItemStore> HotColdDB ) -> Result> + '_, Error> { HybridForwardsStateRootsIterator::new( self, + DBColumn::BeaconStateRoots, start_slot, None, || Ok((end_state, end_state_root)), - &self.spec, ) } @@ -893,10 +893,10 @@ impl, Cold: ItemStore> HotColdDB ) -> Result, Error> { HybridForwardsStateRootsIterator::new( self, + DBColumn::BeaconStateRoots, start_slot, Some(end_slot), get_state, - &self.spec, ) } diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 8f2ced91129..efac1c2db68 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -261,14 +261,20 @@ pub enum DBColumn { #[strum(serialize = "pkc")] PubkeyCache, /// For the legacy table mapping restore point numbers to state roots. + /// + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "brp")] BeaconRestorePoint, + /// Mapping from slot to beacon block root in the freezer DB. #[strum(serialize = "bbr")] BeaconBlockRoots, + /// Mapping from slot to beacon state root in the freezer DB. #[strum(serialize = "bsr")] BeaconStateRoots, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "bhr")] BeaconHistoricalRoots, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "brm")] BeaconRandaoMixes, #[strum(serialize = "dht")] @@ -276,6 +282,7 @@ pub enum DBColumn { /// For Optimistically Imported Merge Transition Blocks #[strum(serialize = "otb")] OptimisticTransitionBlock, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "bhs")] BeaconHistoricalSummaries, #[strum(serialize = "olc")] From bdcc818c357866f159aa76c576b3950b13c36ec6 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 5 Jul 2024 12:00:57 +1000 Subject: [PATCH 10/54] Add and polish hierarchy-config flag --- beacon_node/src/cli.rs | 20 +++++++++++++++++--- beacon_node/store/src/hdiff.rs | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 40a343a7fe4..e7e76d6a9b4 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -722,9 +722,23 @@ pub fn cli_app() -> Command { Arg::new("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. This flag has no effect.") + .action(ArgAction::Set) + .display_order(0) + ) + .arg( + Arg::new("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]") .action(ArgAction::Set) .display_order(0) ) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index b6064afe706..dd523f11eb6 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -279,7 +279,7 @@ impl HierarchyConfig { } pub fn validate(&self) -> Result<(), Error> { - if self.exponents.len() > 2 + if self.exponents.len() >= 1 && self .exponents .iter() From aba6b8b89901457d2c78086fd432721b56c73f3d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 5 Jul 2024 16:36:30 +1000 Subject: [PATCH 11/54] Cleaner errors --- beacon_node/store/src/errors.rs | 1 + beacon_node/store/src/hot_cold_store.rs | 4 ++-- beacon_node/store/src/reconstruct.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 704ceb49e27..170fc9b8d57 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -78,6 +78,7 @@ pub enum Error { ForwardsIterInvalidColumn(DBColumn), ForwardsIterGap(DBColumn, Slot, Slot), ForwardsIterBadStart(DBColumn, Slot), + StateShouldNotBeRequired(Slot), MissingBlock(Hash256), } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 036abb64311..d2ad82211fd 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1465,7 +1465,7 @@ impl, Cold: ItemStore> HotColdDB // 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") + Err(Error::StateShouldNotBeRequired(slot)) })?; self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None) @@ -1564,7 +1564,7 @@ impl, Cold: ItemStore> HotColdDB ) -> Result>, Error> { let block_root_iter = self.forwards_block_roots_iterator_until(start_slot, end_slot, || { - panic!("FIXME(sproul): error here") + Err(Error::StateShouldNotBeRequired(end_slot)) })?; process_results(block_root_iter, |iter| { iter.map(|(block_root, _slot)| block_root) diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 623231ff300..d3845965bbf 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -46,7 +46,7 @@ where // 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") + Err(Error::StateShouldNotBeRequired(upper_limit_slot - 1)) })? .take(num_blocks.unwrap_or(usize::MAX)); From 420e5241f14c8669d3bf8905b1578179e117427a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 5 Jul 2024 16:55:06 +1000 Subject: [PATCH 12/54] Fix beacon_chain test compilation --- beacon_node/beacon_chain/tests/store_tests.rs | 280 ++---------------- beacon_node/store/src/forwards_iter.rs | 21 +- 2 files changed, 31 insertions(+), 270 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index c2468102eed..91b6cd9f3d5 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -25,17 +25,13 @@ 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::{ - chunked_vector::{chunk_key, Field}, - get_key_for_col, iter::{BlockRootsIterator, StateRootsIterator}, - BlobInfo, DBColumn, HotColdDB, KeyValueStore, KeyValueStoreOp, LevelDB, StoreConfig, + BlobInfo, DBColumn, HotColdDB, LevelDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; use tokio::time::sleep; -use tree_hash::TreeHash; use types::test_utils::{SeedableRng, XorShiftRng}; use types::*; @@ -106,253 +102,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; @@ -680,10 +429,19 @@ 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 + // The freezer upper bound 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); + let block_upper_bound = store + .freezer_upper_bound_for_column(DBColumn::BeaconBlockRoots, Slot::new(0)) + .unwrap() + .unwrap(); + assert!(block_upper_bound > 0); + let state_upper_bound = store + .freezer_upper_bound_for_column(DBColumn::BeaconStateRoots, Slot::new(0)) + .unwrap() + .unwrap(); + assert!(state_upper_bound > 0); + assert_eq!(state_upper_bound, block_upper_bound); let chain = &harness.chain; let head_state = harness.get_current_state(); @@ -708,14 +466,12 @@ async fn forwards_iter_block_and_state_roots_until() { }; let split_slot = store.get_split_slot(); - assert!(split_slot > last_restore_point_slot); + assert_eq!(split_slot, block_upper_bound); - 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), head_state.slot()); } @@ -2613,7 +2369,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); } diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 946047136bd..1f0a66ca5dd 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -63,8 +63,11 @@ impl, Cold: ItemStore> HotColdDB Ok(SimpleForwardsIterator { values }) } - /// Compute the maximum /slot (exclusive) - fn freezer_upper_bound_for_column( + /// Values in `column` are available in the range `start_slot..upper_bound`. + /// + /// If `None` is returned then no values are available from `start_slot` due to pruning or + /// incomplete backfill. + pub fn freezer_upper_bound_for_column( &self, column: DBColumn, start_slot: Slot, @@ -80,12 +83,14 @@ impl, Cold: ItemStore> HotColdDB fn freezer_upper_bound_for_block_roots(&self, start_slot: Slot) -> Option { let oldest_block_slot = self.get_oldest_block_slot(); - if start_slot == 0 { - // Slot 0 block root is always available. - Some(Slot::new(1)) - } else if start_slot < oldest_block_slot { - // Non-zero block roots are not available prior to the `oldest_block_slot`. - None + if start_slot < oldest_block_slot { + if start_slot == 0 { + // Slot 0 block root is always available. + Some(Slot::new(1)) + // Non-zero block roots are not available prior to the `oldest_block_slot`. + } else { + None + } } else { // Block roots are stored for all slots up to the split slot (exclusive). Some(self.get_split_slot()) From 0500e64937dea8648753c6088d01e16f16687449 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 9 Jul 2024 19:22:53 +1000 Subject: [PATCH 13/54] Patch a few more freezer block roots --- beacon_node/beacon_chain/src/builder.rs | 4 + .../beacon_chain/src/historical_blocks.rs | 25 +++++-- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- beacon_node/store/src/chunk_writer.rs | 75 ------------------- beacon_node/store/src/forwards_iter.rs | 15 +++- beacon_node/store/src/hot_cold_store.rs | 20 +++-- beacon_node/store/src/lib.rs | 2 - 7 files changed, 47 insertions(+), 96 deletions(-) delete mode 100644 beacon_node/store/src/chunk_writer.rs diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 90461b8f03e..5d771d7adb4 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -361,6 +361,10 @@ where store .put_block(&beacon_block_root, beacon_block.clone()) .map_err(|e| format!("Failed to store genesis block: {:?}", e))?; + store + .store_frozen_block_root_at_skip_slots(Slot::new(0), Slot::new(1), beacon_block_root) + .and_then(|ops| store.cold_db.do_atomically(ops)) + .map_err(|e| format!("Failed to store genesis block root: {e:?}"))?; // Store the genesis block under the `ZERO_HASH` key. store diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 85208c8ad6f..b2b16ce1299 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,8 +97,6 @@ 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); @@ -129,8 +127,17 @@ 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)?; + for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { + debug!( + self.store.log, + "Storing block root"; + "slot" => slot, + "block_root" => ?block_root + ); + 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 +149,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()), + 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(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 4abf701ba70..fea57a5f464 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2145,7 +2145,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await; let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); - let log = test_logger(); + let log = harness.chain.logger().clone(); let temp2 = tempdir().unwrap(); let store = get_store(&temp2); let spec = test_spec::(); 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/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 1f0a66ca5dd..6431997871d 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -2,6 +2,7 @@ use crate::errors::{Error, Result}; use crate::iter::{BlockRootsIterator, StateRootsIterator}; use crate::{ColumnIter, DBColumn, HotColdDB, ItemStore}; use itertools::process_results; +use slog::debug; use std::marker::PhantomData; use types::{BeaconState, EthSpec, Hash256, Slot}; @@ -166,11 +167,11 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator if self.next_slot == self.end_slot { return None; } - self.inner .next()? .and_then(|(slot_bytes, root_bytes)| { let slot = slot_bytes + .clone() .try_into() .map(u64::from_be_bytes) .map(Slot::new) @@ -259,11 +260,15 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> .ok_or(Error::ForwardsIterBadStart(column, start_slot))?; let result = if start_slot < freezer_upper_bound { + // EXCLUSIVE end slot for the frozen portion of the iterator. + let frozen_end_slot = end_slot.map_or(freezer_upper_bound, |end_slot| { + std::cmp::min(end_slot + 1, freezer_upper_bound) + }); let iter = Box::new(FrozenForwardsIterator::new( store, column, start_slot, - freezer_upper_bound, + frozen_end_slot, )?); // No continuation data is needed if the forwards iterator plans to halt before @@ -271,6 +276,12 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> // returned. let continuation_data = if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { + debug!( + store.log, + "No continuation data should be required"; + "end_slot" => ?end_slot, + "freezer_upper_bound" => freezer_upper_bound, + ); None } else { Some(Box::new(get_state()?)) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index d2ad82211fd..24f2ee962a1 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,4 +1,3 @@ -use crate::chunked_vector::BlockRoots; use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; @@ -14,8 +13,7 @@ use crate::metadata::{ use crate::metrics; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, - StoreItem, StoreOp, + get_key_for_col, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem, StoreOp, }; use itertools::{process_results, Itertools}; use leveldb::iterator::LevelDBIterator; @@ -2060,12 +2058,18 @@ 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() { + debug!( + self.log, + "Storing frozen block root"; + "slot" => slot, + "block_root" => ?block_root, + ); + 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) } diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 7a52ff690fa..7cef501cc35 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -7,7 +7,6 @@ //! //! Provides a simple API for storing/retrieving all types that sometimes needs type-hints. See //! tests for implementation examples. -mod chunk_writer; pub mod chunked_iter; pub mod chunked_vector; pub mod config; @@ -27,7 +26,6 @@ pub mod state_cache; pub mod iter; -pub use self::chunk_writer::ChunkWriter; pub use self::config::StoreConfig; pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; From 2715f6088868617911727e9118547439f8dbc75c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 11 Jul 2024 16:26:04 +1000 Subject: [PATCH 14/54] Fix genesis block root bug --- beacon_node/beacon_chain/src/historical_blocks.rs | 2 +- beacon_node/beacon_chain/tests/store_tests.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index b2b16ce1299..251f44d60be 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -152,7 +152,7 @@ impl BeaconChain { 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()), - block_root.as_bytes().to_vec(), + self.genesis_block_root.as_bytes().to_vec(), )); } prev_block_slot = genesis_slot; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index fea57a5f464..e9bb7bdb781 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -56,8 +56,8 @@ fn get_store_generic( config: StoreConfig, spec: ChainSpec, ) -> Arc, LevelDB>> { - let hot_path = db_path.path().join("hot_db"); - let cold_path = db_path.path().join("cold_db"); + let hot_path = db_path.path().join("chain_db"); + let cold_path = db_path.path().join("freezer_db"); let blobs_path = db_path.path().join("blobs_db"); let log = test_logger(); From fa1a9413678d83f4dce5f1400abe4ef3d352bd07 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 12 Jul 2024 11:17:50 +1000 Subject: [PATCH 15/54] Fix test failing due to pending updates --- beacon_node/beacon_chain/src/historical_blocks.rs | 6 ------ beacon_node/beacon_chain/src/test_utils.rs | 3 ++- beacon_node/beacon_chain/tests/store_tests.rs | 8 +++++--- beacon_node/store/src/forwards_iter.rs | 7 ------- beacon_node/store/src/hot_cold_store.rs | 6 ------ 5 files changed, 7 insertions(+), 23 deletions(-) diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 251f44d60be..a3bd5858396 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -128,12 +128,6 @@ impl BeaconChain { // Store block roots, including at all skip slots in the freezer DB. for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { - debug!( - self.store.log, - "Storing block root"; - "slot" => slot, - "block_root" => ?block_root - ); cold_batch.push(KeyValueStoreOp::PutKeyValue( get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), block_root.as_bytes().to_vec(), diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index bd98f19af6f..dd5d48bd2e0 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2245,8 +2245,9 @@ where .await .unwrap(); state = new_state; + let state_root = state.update_tree_hash_cache().unwrap(); block_hash_from_slot.insert(*slot, block_hash); - state_hash_from_slot.insert(*slot, state.tree_hash_root().into()); + state_hash_from_slot.insert(*slot, state_root.into()); latest_block_hash = Some(block_hash); } ( diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index e9bb7bdb781..c990262a7a2 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -360,14 +360,16 @@ async fn epoch_boundary_state_attestation_processing() { .get_blinded_block(&block_root) .unwrap() .expect("block exists"); - let epoch_boundary_state = store + let mut 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()) + let ebs_state_root = epoch_boundary_state.update_tree_hash_cache().unwrap(); + let mut ebs_of_ebs = store + .load_epoch_boundary_state(&ebs_state_root) .expect("no error") .expect("ebs of ebs exists"); + ebs_of_ebs.apply_pending_mutations().unwrap(); assert_eq!(epoch_boundary_state, ebs_of_ebs); // If the attestation is pre-finalization it should be rejected. diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 6431997871d..cd1acb77410 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -2,7 +2,6 @@ use crate::errors::{Error, Result}; use crate::iter::{BlockRootsIterator, StateRootsIterator}; use crate::{ColumnIter, DBColumn, HotColdDB, ItemStore}; use itertools::process_results; -use slog::debug; use std::marker::PhantomData; use types::{BeaconState, EthSpec, Hash256, Slot}; @@ -276,12 +275,6 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> // returned. let continuation_data = if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { - debug!( - store.log, - "No continuation data should be required"; - "end_slot" => ?end_slot, - "freezer_upper_bound" => freezer_upper_bound, - ); None } else { Some(Box::new(get_state()?)) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 24f2ee962a1..d41462592fa 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2059,12 +2059,6 @@ impl, Cold: ItemStore> HotColdDB ) -> Result, Error> { let mut ops = vec![]; for slot in start_slot.as_u64()..end_slot.as_u64() { - debug!( - self.log, - "Storing frozen block root"; - "slot" => slot, - "block_root" => ?block_root, - ); ops.push(KeyValueStoreOp::PutKeyValue( get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), block_root.as_bytes().to_vec(), From ee032dffeb62a75d873fb382a42c50cbfad229fe Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 16 Jul 2024 11:55:45 +1000 Subject: [PATCH 16/54] Beacon chain tests passing --- beacon_node/beacon_chain/tests/store_tests.rs | 33 +++------ beacon_node/store/src/errors.rs | 1 - beacon_node/store/src/forwards_iter.rs | 67 +++++++++---------- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index c990262a7a2..ca8521b8406 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3198,15 +3198,15 @@ async fn prune_historic_states() { ) .await; - // Check historical state is present. - let state_roots_iter = harness + // Check historical states are present. + let first_epoch_state_roots = harness .chain .forwards_iter_state_roots(Slot::new(0)) - .unwrap(); - for (state_root, slot) in state_roots_iter + .unwrap() .take(E::slots_per_epoch() as usize) .map(Result::unwrap) - { + .collect::>(); + for &(state_root, slot) in &first_epoch_state_roots { assert!(store.get_state(&state_root, Some(slot)).unwrap().is_some()); } @@ -3219,25 +3219,14 @@ async fn prune_historic_states() { assert_eq!(anchor_info.state_lower_limit, 0); assert_eq!(anchor_info.state_upper_limit, STATE_UPPER_LIMIT_NO_RETAIN); - // Historical states should be pruned. - let state_roots_iter = harness - .chain - .forwards_iter_state_roots(Slot::new(1)) - .unwrap(); - for (state_root, slot) in state_roots_iter - .take(E::slots_per_epoch() as usize) - .map(Result::unwrap) - { - assert!(store.get_state(&state_root, Some(slot)).unwrap().is_none()); + // Ensure all epoch 0 states other than the genesis have been pruned. + for &(state_root, slot) in &first_epoch_state_roots { + assert_eq!( + store.get_state(&state_root, Some(slot)).unwrap().is_some(), + slot == 0 + ); } - // Ensure that genesis state is still accessible - let genesis_state_root = harness.chain.genesis_state_root; - assert!(store - .get_state(&genesis_state_root, Some(Slot::new(0))) - .unwrap() - .is_some()); - // Run for another two epochs. let additional_blocks_produced = 2 * E::slots_per_epoch(); harness diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 170fc9b8d57..7b4783bc372 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -77,7 +77,6 @@ pub enum Error { CacheBuildError(EpochCacheError), ForwardsIterInvalidColumn(DBColumn), ForwardsIterGap(DBColumn, Slot, Slot), - ForwardsIterBadStart(DBColumn, Slot), StateShouldNotBeRequired(Slot), MissingBlock(Hash256), } diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index cd1acb77410..adffc576dd8 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -254,48 +254,45 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> // 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_bound = store - .freezer_upper_bound_for_column(column, start_slot)? - .ok_or(Error::ForwardsIterBadStart(column, start_slot))?; + let opt_freezer_upper_bound = store.freezer_upper_bound_for_column(column, start_slot)?; - let result = if start_slot < freezer_upper_bound { - // EXCLUSIVE end slot for the frozen portion of the iterator. - let frozen_end_slot = end_slot.map_or(freezer_upper_bound, |end_slot| { - std::cmp::min(end_slot + 1, freezer_upper_bound) - }); - let iter = Box::new(FrozenForwardsIterator::new( - store, - column, - start_slot, - frozen_end_slot, - )?); + match opt_freezer_upper_bound { + Some(freezer_upper_bound) if start_slot < freezer_upper_bound => { + // EXCLUSIVE end slot for the frozen portion of the iterator. + let frozen_end_slot = end_slot.map_or(freezer_upper_bound, |end_slot| { + std::cmp::min(end_slot + 1, freezer_upper_bound) + }); + let iter = Box::new(FrozenForwardsIterator::new( + store, + column, + start_slot, + frozen_end_slot, + )?); - // No continuation data is needed if the forwards iterator plans to halt before - // `end_slot`. If it tries to continue further a `NoContinuationData` error will be - // returned. - let continuation_data = - if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { - None - } else { - Some(Box::new(get_state()?)) - }; - PreFinalization { - iter, - store, - end_slot, - column, - continuation_data, + // No continuation data is needed if the forwards iterator plans to halt before + // `end_slot`. If it tries to continue further a `NoContinuationData` error will be + // returned. + let continuation_data = + if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { + None + } else { + Some(Box::new(get_state()?)) + }; + Ok(PreFinalization { + iter, + store, + end_slot, + column, + continuation_data, + }) } - } else { - PostFinalizationLazy { + _ => Ok(PostFinalizationLazy { continuation_data: Some(Box::new(get_state()?)), store, start_slot, column, - } - }; - - Ok(result) + }), + } } fn do_next(&mut self) -> Result> { From d2049ca1b7e2e02db929281c1e15e68b04b41e77 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 29 Jul 2024 22:42:06 +1000 Subject: [PATCH 17/54] Fix doc lint --- beacon_node/store/src/hdiff.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 6e381878bc0..4f508a88b78 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -37,6 +37,7 @@ pub struct HierarchyConfig { /// - Layer 0: 3000003 - (3000003 mod 2^21) = 2097152 /// - Layer 1: 3000003 - (3000003 mod 2^13) = 2998272 /// - Layer 2: 3000003 - (3000003 mod 2^5) = 3000000 + /// /// Layer 0 is full state snaphost, apply layer 1 diff, then apply layer 2 diff and then replay /// blocks 3,000,001 to 3,000,003. pub exponents: Vec, From 57b73dfb08b3c5190c1c0a615618f9bb3a0ffb7c Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 19 Aug 2024 07:35:26 +0200 Subject: [PATCH 18/54] Implement DB schema upgrade for hierarchical state diffs (#6193) * DB upgrade * Add flag * Delete RestorePointHash * Update docs * Update docs * Implement hierarchical state diffs config migration (#6245) * Implement hierarchical state diffs config migration * Review PR * Remove TODO * Set CURRENT_SCHEMA_VERSION correctly * Fix genesis state loading * Re-delete some PartialBeaconState stuff --------- Co-authored-by: Michael Sproul --- Cargo.lock | 1 + beacon_node/beacon_chain/src/schema_change.rs | 35 +- .../src/schema_change/migration_schema_v22.rs | 111 +++++ beacon_node/client/src/builder.rs | 16 +- beacon_node/src/cli.rs | 9 + beacon_node/src/config.rs | 6 + beacon_node/store/Cargo.toml | 1 + beacon_node/store/src/config.rs | 146 ++++-- beacon_node/store/src/errors.rs | 6 +- beacon_node/store/src/hot_cold_store.rs | 63 +-- beacon_node/store/src/lib.rs | 1 + beacon_node/store/src/metadata.rs | 2 +- beacon_node/store/src/partial_beacon_state.rs | 415 ++++++++++++++++++ book/src/help_bn.md | 16 +- common/eth2_config/src/lib.rs | 6 + common/eth2_network_config/src/lib.rs | 22 + database_manager/src/lib.rs | 51 ++- lighthouse/tests/beacon_node.rs | 7 + 18 files changed, 787 insertions(+), 127 deletions(-) create mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs create mode 100644 beacon_node/store/src/partial_beacon_state.rs diff --git a/Cargo.lock b/Cargo.lock index 07644f9f775..ae555949d46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8213,6 +8213,7 @@ dependencies = [ "smallvec", "state_processing", "strum", + "superstruct", "tempfile", "types", "xdelta3", diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 4f7770e22c6..2f9ba87c1bd 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -1,24 +1,23 @@ //! Utilities for managing database schema changes. mod migration_schema_v20; mod migration_schema_v21; +mod migration_schema_v22; use crate::beacon_chain::BeaconChainTypes; -use crate::types::ChainSpec; use slog::Logger; use std::sync::Arc; use store::hot_cold_store::{HotColdDB, HotColdDBError}; use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; use store::Error as StoreError; +use types::Hash256; /// Migrate the database from one schema version to another, applying all requisite mutations. -#[allow(clippy::only_used_in_recursion)] // spec is not used but likely to be used in future pub fn migrate_schema( db: Arc>, - deposit_contract_deploy_block: u64, + genesis_state_root: Option, from: SchemaVersion, to: SchemaVersion, log: Logger, - spec: &ChainSpec, ) -> Result<(), StoreError> { match (from, to) { // Migrating from the current schema version to itself is always OK, a no-op. @@ -26,28 +25,14 @@ pub fn migrate_schema( // 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); - migrate_schema::( - db.clone(), - deposit_contract_deploy_block, - from, - next, - log.clone(), - spec, - )?; - migrate_schema::(db, deposit_contract_deploy_block, next, to, log, spec) + migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; + migrate_schema::(db, genesis_state_root, next, to, log) } // Downgrade across multiple versions by recursively migrating one step at a time. (_, _) if to.as_u64() + 1 < from.as_u64() => { let next = SchemaVersion(from.as_u64() - 1); - migrate_schema::( - db.clone(), - deposit_contract_deploy_block, - from, - next, - log.clone(), - spec, - )?; - migrate_schema::(db, deposit_contract_deploy_block, next, to, log, spec) + migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; + migrate_schema::(db, genesis_state_root, next, to, log) } // @@ -69,6 +54,12 @@ pub fn migrate_schema( let ops = migration_schema_v21::downgrade_from_v21::(db.clone(), log)?; db.store_schema_version_atomically(to, ops) } + (SchemaVersion(21), SchemaVersion(22)) => { + let ops = + migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log)?; + db.store_schema_version_atomically(to, ops) + } + // FIXME(sproul): consider downgrade // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs new file mode 100644 index 00000000000..717d2874fc5 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -0,0 +1,111 @@ +use crate::beacon_chain::BeaconChainTypes; +use slog::error; +use slog::{info, Logger}; +use std::sync::Arc; +use store::chunked_iter::ChunkedVectorIter; +use store::{ + chunked_vector::BlockRoots, get_key_for_col, partial_beacon_state::PartialBeaconState, + DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, +}; +use types::{BeaconState, Hash256, Slot}; + +const LOG_EVERY: usize = 200_000; + +fn load_old_schema_frozen_state( + db: &HotColdDB, + state_root: Hash256, +) -> Result>, Error> { + let Some(partial_state_bytes) = db + .cold_db + .get_bytes(DBColumn::BeaconState.into(), state_root.as_bytes())? + else { + return Ok(None); + }; + let mut partial_state: PartialBeaconState = + PartialBeaconState::from_ssz_bytes(&partial_state_bytes, db.get_chain_spec())?; + + // Fill in the fields of the partial state. + partial_state.load_block_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_state_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_historical_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_randao_mixes(&db.cold_db, db.get_chain_spec())?; + partial_state.load_historical_summaries(&db.cold_db, db.get_chain_spec())?; + + partial_state.try_into().map(Some) +} + +pub fn upgrade_to_v22( + db: Arc>, + genesis_state_root: Option, + log: Logger, +) -> Result, Error> { + info!(log, "Upgrading from v21 to v22"); + + let anchor = db.get_anchor_info().ok_or(Error::NoAnchorInfo)?; + let split_slot = db.get_split_slot(); + let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; + + if !db.get_config().allow_tree_states_migration && !anchor.no_historic_states_stored(split_slot) + { + error!( + log, + "You are attempting to migrate to tree-states but this is a destructive operation. \ + Upgrading will require FIXME(sproul) minutes of downtime before Lighthouse starts again. \ + All current historic states will be deleted. Reconstructing the states in the new \ + schema will take up to 2 weeks. \ + \ + To proceed add the flag --allow-tree-states-migration OR run lighthouse db prune-states" + ); + return Err(Error::DestructiveFreezerUpgrade); + } + + let mut ops = vec![]; + + rewrite_block_roots::(&db, anchor.oldest_block_slot, split_slot, &mut ops, &log)?; + + let mut genesis_state = load_old_schema_frozen_state::(&db, genesis_state_root)? + .ok_or(Error::MissingGenesisState)?; + let genesis_state_root = genesis_state.update_tree_hash_cache()?; + + db.prune_historic_states(genesis_state_root, &genesis_state)?; + + Ok(ops) +} + +pub fn rewrite_block_roots( + db: &HotColdDB, + oldest_block_slot: Slot, + split_slot: Slot, + ops: &mut Vec, + log: &Logger, +) -> Result<(), Error> { + // Block roots are available from the `oldest_block_slot` to the `split_slot`. + let start_vindex = oldest_block_slot.as_usize(); + let block_root_iter = ChunkedVectorIter::::new( + db, + start_vindex, + split_slot, + db.get_chain_spec(), + ); + + // OK to hold these in memory (10M slots * 43 bytes per KV ~= 430 MB). + for (i, (slot, block_root)) in block_root_iter.enumerate() { + ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconBlockRoots.into(), + &(slot as u64).to_be_bytes(), + ), + block_root.as_bytes().to_vec(), + )); + + if i > 0 && i % LOG_EVERY == 0 { + info!( + log, + "Beacon block root migration in progress"; + "roots_migrated" => i + ); + } + } + + Ok(()) +} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 393ce35f000..1ca942b26e2 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1057,25 +1057,25 @@ where .chain_spec .clone() .ok_or("disk_store requires a chain spec")?; + let network_config = context + .eth2_network_config + .as_ref() + .ok_or("disk_store requires a network config")?; self.db_path = Some(hot_path.into()); self.freezer_db_path = Some(cold_path.into()); - let inner_spec = spec.clone(); - let deposit_contract_deploy_block = context - .eth2_network_config - .as_ref() - .map(|config| config.deposit_contract_deploy_block) - .unwrap_or(0); + let genesis_state_root = network_config + .genesis_state_root::() + .map_err(|e| format!("error determining genesis state root: {e:?}"))?; let schema_upgrade = |db, from, to| { migrate_schema::>( db, - deposit_contract_deploy_block, + genesis_state_root, from, to, log, - &inner_spec, ) }; diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 74546447619..6d070c53444 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -982,6 +982,15 @@ pub fn cli_app() -> Command { .default_value("0") .display_order(0) ) + .arg( + Arg::new("allow-tree-states-migration") + .long("allow-tree-states-migration") + .value_name("BOOLEAN") + .help("Whether to allow a destructive freezer DB migration for hierarchical state diffs") + .action(ArgAction::Set) + .default_value("false") + .display_order(0) + ) /* * Misc. diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index e93119672b2..440f18fe5e9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -451,6 +451,12 @@ pub fn get_config( client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; } + if let Some(allow_tree_states_migration) = + clap_utils::parse_optional(cli_args, "allow-tree-states-migration")? + { + client_config.store.allow_tree_states_migration = allow_tree_states_migration; + } + /* * Zero-ports * diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index fec07ddec7d..181459aad61 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -15,6 +15,7 @@ parking_lot = { workspace = true } itertools = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +superstruct = { workspace = true } types = { workspace = true } state_processing = { workspace = true } slog = { workspace = true } diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index e4efd0559fe..7bc5a7cd536 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -5,6 +5,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::io::Write; use std::num::NonZeroUsize; +use superstruct::superstruct; use types::non_zero_usize::new_non_zero_usize; use types::{EthSpec, Unsigned}; use zstd::Encoder; @@ -41,8 +42,6 @@ pub struct StoreConfig { pub compact_on_prune: bool, /// Whether to prune payloads on initialization and finalization. pub prune_payloads: 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. @@ -52,15 +51,37 @@ pub struct StoreConfig { /// The margin for blob pruning in epochs. The oldest blobs are pruned up until /// data_availability_boundary - blob_prune_margin_epochs. Default: 0. pub blob_prune_margin_epochs: u64, + /// Whether to allow a destructive freezer DB migration for hierarchical state diffs. + /// + /// i.e. "on-disk tree-states" + pub allow_tree_states_migration: bool, } /// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -// FIXME(sproul): schema migration +#[superstruct( + variants(V1, V22), + variant_attributes(derive(Debug, Clone, PartialEq, Eq, Encode, Decode)) +)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OnDiskStoreConfig { + #[superstruct(only(V1))] + pub slots_per_restore_point: u64, + /// Prefix byte to future-proof versions of the `OnDiskStoreConfig` post V1 + #[superstruct(only(V22))] + version_byte: u8, + #[superstruct(only(V22))] pub hierarchy_config: HierarchyConfig, } +impl OnDiskStoreConfigV22 { + fn new(hierarchy_config: HierarchyConfig) -> Self { + Self { + version_byte: 22, + hierarchy_config, + } + } +} + #[derive(Debug, Clone)] pub enum StoreConfigError { MismatchedSlotsPerRestorePoint { @@ -79,6 +100,7 @@ pub enum StoreConfigError { max_supported: u64, }, ZeroEpochsPerBlobPrune, + InvalidVersionByte(Option), } impl Default for StoreConfig { @@ -93,20 +115,18 @@ impl Default for StoreConfig { compact_on_init: false, compact_on_prune: true, prune_payloads: 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, + allow_tree_states_migration: false, } } } impl StoreConfig { pub fn as_disk_config(&self) -> OnDiskStoreConfig { - OnDiskStoreConfig { - hierarchy_config: self.hierarchy_config.clone(), - } + OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(self.hierarchy_config.clone())) } pub fn check_compatibility( @@ -115,17 +135,23 @@ impl StoreConfig { split: &Split, anchor: Option<&AnchorInfo>, ) -> Result<(), StoreConfigError> { - let db_config = self.as_disk_config(); // Allow changing the hierarchy exponents if no historic states are stored. - if db_config.hierarchy_config == on_disk_config.hierarchy_config - || anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot)) - { - Ok(()) - } else { + let no_historic_states_stored = + anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot)); + let hierarchy_config_changed = + if let Ok(on_disk_hierarchy_config) = on_disk_config.hierarchy_config() { + *on_disk_hierarchy_config != self.hierarchy_config + } else { + false + }; + + if hierarchy_config_changed && !no_historic_states_stored { Err(StoreConfigError::IncompatibleStoreConfig { - config: db_config, + config: self.as_disk_config(), on_disk: on_disk_config.clone(), }) + } else { + Ok(()) } } @@ -208,11 +234,21 @@ impl StoreItem for OnDiskStoreConfig { } fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() + match self { + OnDiskStoreConfig::V1(value) => value.as_ssz_bytes(), + OnDiskStoreConfig::V22(value) => value.as_ssz_bytes(), + } } fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) + // NOTE: V22 config can never be deserialized as a V1 because the minimum length of its + // serialization is: 1 prefix byte + 1 offset (OnDiskStoreConfigV1 container) + + // 1 offset (HierarchyConfig container) = 9. + if let Ok(value) = OnDiskStoreConfigV1::from_ssz_bytes(bytes) { + return Ok(Self::V1(value)); + } + + Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?)) } } @@ -220,6 +256,7 @@ impl StoreItem for OnDiskStoreConfig { mod test { use super::*; use crate::{metadata::STATE_UPPER_LIMIT_NO_RETAIN, AnchorInfo, Split}; + use ssz::DecodeError; use types::{Hash256, Slot}; #[test] @@ -227,9 +264,23 @@ mod test { let store_config = StoreConfig { ..Default::default() }; - let on_disk_config = OnDiskStoreConfig { - hierarchy_config: store_config.hierarchy_config.clone(), + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new( + 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_after_migration() { + let store_config = StoreConfig { + ..Default::default() }; + let on_disk_config = OnDiskStoreConfig::V1(OnDiskStoreConfigV1 { + slots_per_restore_point: 8192, + }); let split = Split::default(); assert!(store_config .check_compatibility(&on_disk_config, &split, None) @@ -241,11 +292,9 @@ mod test { let store_config = StoreConfig { ..Default::default() }; - let on_disk_config = OnDiskStoreConfig { - hierarchy_config: HierarchyConfig { - exponents: vec![5, 8, 11, 13, 16, 18, 21], - }, - }; + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + })); let split = Split::default(); assert!(store_config .check_compatibility(&on_disk_config, &split, None) @@ -257,11 +306,9 @@ mod test { let store_config = StoreConfig { ..Default::default() }; - let on_disk_config = OnDiskStoreConfig { - hierarchy_config: HierarchyConfig { - exponents: vec![5, 8, 11, 13, 16, 18, 21], - }, - }; + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + })); let split = Split::default(); let anchor = AnchorInfo { anchor_slot: Slot::new(0), @@ -274,4 +321,45 @@ mod test { .check_compatibility(&on_disk_config, &split, Some(&anchor)) .is_ok()); } + + #[test] + fn serde_on_disk_config_v0_from_v1_default() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); + let config_bytes = config.as_store_bytes(); + // On a downgrade, the previous version of lighthouse will attempt to deserialize the + // prefixed V22 as just the V1 version. + assert_eq!( + OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), + DecodeError::InvalidByteLength { + len: 16, + expected: 8 + }, + ); + } + + #[test] + fn serde_on_disk_config_v0_from_v1_empty() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![], + })); + let config_bytes = config.as_store_bytes(); + // On a downgrade, the previous version of lighthouse will attempt to deserialize the + // prefixed V22 as just the V1 version. + assert_eq!( + OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), + DecodeError::InvalidByteLength { + len: 9, + expected: 8 + }, + ); + } + + #[test] + fn serde_on_disk_config_v1_roundtrip() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); + let bytes = config.as_store_bytes(); + assert_eq!(bytes[0], 22); + let config_out = OnDiskStoreConfig::from_store_bytes(&bytes).unwrap(); + assert_eq!(config_out, config); + } } diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 7b4783bc372..5141211b25c 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -45,6 +45,7 @@ pub enum Error { }, MissingStateRoot(Slot), MissingState(Hash256), + MissingGenesisState, MissingSnapshot(Slot), NoBaseStateFound(Hash256), BlockReplayError(BlockReplayError), @@ -63,7 +64,6 @@ pub enum Error { AddPayloadLogicError, SlotClockUnavailableForMigration, MissingValidator(usize), - V9MigrationFailure(Hash256), ValidatorPubkeyCacheError(String), DuplicateValidatorPublicKey, InvalidValidatorPubkeyBytes(bls::Error), @@ -79,6 +79,10 @@ pub enum Error { ForwardsIterGap(DBColumn, Slot, Slot), StateShouldNotBeRequired(Slot), MissingBlock(Hash256), + DestructiveFreezerUpgrade, + NoAnchorInfo, + RandaoMixOutOfBounds, + GenesisStateUnknown, } pub trait HandleUnavailable { diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index d41462592fa..50383814181 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -133,7 +133,6 @@ pub enum HotColdDBError { proposed_split_slot: Slot, }, MissingStateToFreeze(Hash256), - MissingRestorePointHash(u64), MissingRestorePointState(Slot), MissingRestorePoint(Hash256), MissingColdStateSummary(Hash256), @@ -323,13 +322,15 @@ impl HotColdDB, LevelDB> { .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, - ); + if let Ok(hierarchy_config) = disk_config.hierarchy_config() { + if &db.config.hierarchy_config != hierarchy_config { + info!( + db.log, + "Updating historic state config"; + "previous_config" => ?hierarchy_config, + "new_config" => ?db.config.hierarchy_config, + ); + } } } db.store_config()?; @@ -2379,9 +2380,12 @@ impl, Cold: ItemStore> HotColdDB // migrating to the tree-states schema (delete everything in the freezer then start afresh). let mut cold_ops = vec![]; + // This function works for both pre-tree-states and post-tree-states pruning. It deletes + // everything related to historic states from either DB! let columns = [ DBColumn::BeaconState, DBColumn::BeaconStateSummary, + DBColumn::BeaconStateSnapshot, DBColumn::BeaconStateDiff, DBColumn::BeaconRestorePoint, DBColumn::BeaconStateRoots, @@ -2399,20 +2403,13 @@ impl, Cold: ItemStore> HotColdDB ))); } } - - // XXX: We need to commit the mass deletion here *before* re-storing the genesis state, as - // the current schema performs reads as part of `store_cold_state`. This can be deleted - // once the target schema is tree-states. If the process is killed before the genesis state - // is written this can be fixed by re-running. - info!( - self.log, - "Deleting historic states"; - "num_kv" => cold_ops.len(), - ); - self.cold_db.do_atomically(std::mem::take(&mut cold_ops))?; + let delete_ops = cold_ops.len(); // If we just deleted the the genesis state, re-store it using the *current* schema, which // may be different from the schema of the genesis state we just deleted. + // + // During the tree-states migration this will re-store the genesis state as compressed + // beacon state SSZ, which is different from the previous `PartialBeaconState` format. if self.get_split_slot() > 0 { info!( self.log, @@ -2420,9 +2417,15 @@ impl, Cold: ItemStore> HotColdDB "state_root" => ?genesis_state_root, ); self.store_cold_state(&genesis_state_root, genesis_state, &mut cold_ops)?; - self.cold_db.do_atomically(cold_ops)?; } + info!( + self.log, + "Deleting historic states"; + "delete_ops" => delete_ops, + ); + self.cold_db.do_atomically(cold_ops)?; + // In order to reclaim space, we need to compact the freezer DB as well. self.cold_db.compact()?; @@ -2750,26 +2753,6 @@ impl StoreItem for ColdStateSummary { } } -/// Struct for storing the state root of a restore point in the database. -#[derive(Debug, Clone, Copy, Default, Encode, Decode)] -struct RestorePointHash { - state_root: Hash256, -} - -impl StoreItem for RestorePointHash { - fn db_column() -> DBColumn { - DBColumn::BeaconRestorePoint - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - #[derive(Debug, Clone, Copy, Default)] pub struct TemporaryFlag; diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 6c53d08ef2d..b825828e89c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -21,6 +21,7 @@ mod leveldb_store; mod memory_store; pub mod metadata; pub mod metrics; +pub mod partial_beacon_state; pub mod reconstruct; pub mod state_cache; diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 6fd09d23e25..be6654fba04 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(21); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(22); // All the keys that get stored under the `BeaconMeta` column. // diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs new file mode 100644 index 00000000000..c646fe276ba --- /dev/null +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -0,0 +1,415 @@ +use crate::chunked_vector::{ + load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots, + HistoricalSummaries, RandaoMixes, StateRoots, +}; +use crate::{Error, KeyValueStore}; +use ssz::{Decode, DecodeError}; +use ssz_derive::{Decode, Encode}; +use std::sync::Arc; +use types::historical_summary::HistoricalSummary; +use types::superstruct; +use types::*; + +/// DEPRECATED Lightweight variant of the `BeaconState` that is stored in the database. +/// +/// Utilises lazy-loading from separate storage for its vector fields. +/// +/// This can be deleted once schema versions prior to V22 are no longer supported. +#[superstruct( + variants(Base, Altair, Bellatrix, 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: List, + pub eth1_deposit_index: u64, + + // Registry + pub validators: List, + pub balances: List, + + // 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: Vector, + + // Attestations (genesis fork only) + #[superstruct(only(Base))] + pub previous_epoch_attestations: List, E::MaxPendingAttestations>, + #[superstruct(only(Base))] + pub current_epoch_attestations: List, E::MaxPendingAttestations>, + + // Participation (Altair and later) + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + pub previous_epoch_participation: List, + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + pub current_epoch_participation: List, + + // Finality + pub justification_bits: BitVector, + pub previous_justified_checkpoint: Checkpoint, + pub current_justified_checkpoint: Checkpoint, + pub finalized_checkpoint: Checkpoint, + + // Inactivity + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + pub inactivity_scores: List, + + // Light-client sync committees + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + pub current_sync_committee: Arc>, + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + pub next_sync_committee: Arc>, + + // Execution + #[superstruct( + only(Bellatrix), + partial_getter(rename = "latest_execution_payload_header_bellatrix") + )] + pub latest_execution_payload_header: ExecutionPayloadHeaderBellatrix, + #[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>, + + // Electra + #[superstruct(only(Electra))] + pub deposit_requests_start_index: u64, + #[superstruct(only(Electra))] + pub deposit_balance_to_consume: u64, + #[superstruct(only(Electra))] + pub exit_balance_to_consume: u64, + #[superstruct(only(Electra))] + pub earliest_exit_epoch: Epoch, + #[superstruct(only(Electra))] + pub consolidation_balance_to_consume: u64, + #[superstruct(only(Electra))] + pub earliest_consolidation_epoch: Epoch, + + #[superstruct(only(Electra))] + pub pending_balance_deposits: List, + #[superstruct(only(Electra))] + pub pending_partial_withdrawals: + List, + #[superstruct(only(Electra))] + pub pending_consolidations: List, +} + +impl PartialBeaconState { + /// 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)? + )) + } + + 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 + .get_mut(current_epoch.as_usize() % len) + .ok_or(Error::RandaoMixOutOfBounds)? = *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(), + + // 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::Bellatrix(inner) => impl_try_into_beacon_state!( + inner, + Bellatrix, + BeaconStateBellatrix, + [ + 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, + deposit_requests_start_index, + deposit_balance_to_consume, + exit_balance_to_consume, + earliest_exit_epoch, + consolidation_balance_to_consume, + earliest_consolidation_epoch, + pending_balance_deposits, + pending_partial_withdrawals, + pending_consolidations + ], + [historical_summaries] + ), + }; + Ok(state) + } +} diff --git a/book/src/help_bn.md b/book/src/help_bn.md index f9180b65832..8e9d1e47207 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -8,6 +8,9 @@ beacon chain and publishing messages to the network. Usage: lighthouse beacon_node [OPTIONS] Options: + --allow-tree-states-migration + Whether to allow a destructive freezer DB migration for hierarchical + state diffs [default: false] --auto-compact-db Enable or disable automatic compaction of the database on finalization. [default: true] @@ -166,6 +169,15 @@ 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] @@ -375,9 +387,7 @@ 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)] + DEPRECATED. This flag has no effect. --state-cache-size Specifies the size of the state cache [default: 128] --suggested-fee-recipient diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 9104db8f67d..e1866d0fb30 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -31,6 +31,7 @@ const HOLESKY_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url ], checksum: "0xd750639607c337bbb192b15c27f447732267bf72d1650180a0e44c2d93a80741", genesis_validators_root: "0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1", + genesis_state_root: "0x0ea3f6f9515823b59c863454675fefcd1d8b4f2dbe454db166206a41fda060a0", }; const CHIADO_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url { @@ -38,6 +39,7 @@ const CHIADO_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url urls: &[], checksum: "0xd4a039454c7429f1dfaa7e11e397ef3d0f50d2d5e4c0e4dc04919d153aa13af1", genesis_validators_root: "0x9d642dac73058fbf39c0ae41ab1e34e4d889043cb199851ded7095bc99eb4c1e", + genesis_state_root: "0xa48419160f8f146ecaa53d12a5d6e1e6af414a328afdc56b60d5002bb472a077", }; /// The core configuration of a Lighthouse beacon node. @@ -102,6 +104,10 @@ pub enum GenesisStateSource { /// /// The format should be 0x-prefixed ASCII bytes. genesis_validators_root: &'static str, + /// The genesis state root. + /// + /// The format should be 0x-prefixed ASCII bytes. + genesis_state_root: &'static str, }, } diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index fb8c6938cdb..5cb5fe53fcb 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -173,6 +173,26 @@ impl Eth2NetworkConfig { } } + pub fn genesis_state_root(&self) -> Result, String> { + if let GenesisStateSource::Url { + genesis_state_root, .. + } = self.genesis_state_source + { + Hash256::from_str(genesis_state_root) + .map(Option::Some) + .map_err(|e| format!("Unable to parse genesis state root: {:?}", e)) + } else { + self.get_genesis_state_from_bytes::() + .and_then(|mut state| { + Ok(Some( + state + .canonical_root() + .map_err(|e| format!("Hashing error: {e:?}"))?, + )) + }) + } + } + /// Construct a consolidated `ChainSpec` from the YAML config. pub fn chain_spec(&self) -> Result { ChainSpec::from_config::(&self.config).ok_or_else(|| { @@ -204,6 +224,7 @@ impl Eth2NetworkConfig { urls: built_in_urls, checksum, genesis_validators_root, + .. } => { let checksum = Hash256::from_str(checksum).map_err(|e| { format!("Unable to parse genesis state bytes checksum: {:?}", e) @@ -526,6 +547,7 @@ mod tests { urls, checksum, genesis_validators_root, + .. } = net.genesis_state_source { Hash256::from_str(checksum).expect("the checksum must be a valid 32-byte value"); diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 0eb8f0406ac..a439ceb69b4 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -292,6 +292,7 @@ fn parse_migrate_config(migrate_config: &Migrate) -> Result( migrate_config: MigrateConfig, client_config: ClientConfig, + mut genesis_state: BeaconState, runtime_context: &RuntimeContext, log: Logger, ) -> Result<(), Error> { @@ -322,13 +323,13 @@ pub fn migrate_db( "to" => to.as_u64(), ); + let genesis_state_root = genesis_state.canonical_root()?; migrate_schema::, _, _, _>>( db, - client_config.eth1.deposit_contract_deploy_block, + Some(genesis_state_root), from, to, log, - spec, ) } @@ -478,10 +479,33 @@ pub fn run( let log = context.log().clone(); let format_err = |e| format!("Fatal error: {:?}", e); + let get_genesis_state = || { + let executor = env.core_context().executor; + let network_config = context + .eth2_network_config + .clone() + .ok_or("Missing network config")?; + + executor + .block_on_dangerous( + network_config.genesis_state::( + client_config.genesis_state_url.as_deref(), + client_config.genesis_state_url_timeout, + &log, + ), + "get_genesis_state", + ) + .ok_or("Shutting down")? + .map_err(|e| format!("Error getting genesis state: {e}"))? + .ok_or("Genesis state missing".to_string()) + }; + match &db_manager_config.subcommand { cli::DatabaseManagerSubcommand::Migrate(migrate_config) => { let migrate_config = parse_migrate_config(migrate_config)?; - migrate_db(migrate_config, client_config, &context, log).map_err(format_err) + let genesis_state = get_genesis_state()?; + migrate_db(migrate_config, client_config, genesis_state, &context, log) + .map_err(format_err) } cli::DatabaseManagerSubcommand::Inspect(inspect_config) => { let inspect_config = parse_inspect_config(inspect_config)?; @@ -497,27 +521,8 @@ pub fn run( prune_blobs(client_config, &context, log).map_err(format_err) } cli::DatabaseManagerSubcommand::PruneStates(prune_states_config) => { - let executor = env.core_context().executor; - let network_config = context - .eth2_network_config - .clone() - .ok_or("Missing network config")?; - - let genesis_state = executor - .block_on_dangerous( - network_config.genesis_state::( - client_config.genesis_state_url.as_deref(), - client_config.genesis_state_url_timeout, - &log, - ), - "get_genesis_state", - ) - .ok_or("Shutting down")? - .map_err(|e| format!("Error getting genesis state: {e}"))? - .ok_or("Genesis state missing")?; - let prune_config = parse_prune_states_config(prune_states_config)?; - + let genesis_state = get_genesis_state()?; prune_states(client_config, prune_config, genesis_state, &context, log) } cli::DatabaseManagerSubcommand::Compact(compact_config) => { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index db23b7e6d1b..ea3933cc33d 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1958,6 +1958,13 @@ fn blob_prune_margin_epochs_on_startup_ten() { .with_config(|config| assert!(config.store.blob_prune_margin_epochs == 10)); } #[test] +fn allow_tree_states_migration_on_startup_true() { + CommandLineTest::new() + .flag("allow-tree-states-migration", Some("true")) + .run_with_zero_port() + .with_config(|config| assert!(config.store.allow_tree_states_migration == true)); +} +#[test] fn reconstruct_historic_states_flag() { CommandLineTest::new() .flag("reconstruct-historic-states", None) From 17985a681e0ccca9c720a63e783ac85fdada773b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 19 Aug 2024 16:38:57 +1000 Subject: [PATCH 19/54] Fix test compilation --- beacon_node/beacon_chain/tests/store_tests.rs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index e355a6c736e..d69aca8efa4 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -115,14 +115,7 @@ async fn light_client_updates_test() { let log = test_logger(); let seconds_per_slot = spec.seconds_per_slot; - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); + let store = get_store_generic(&db_path, StoreConfig::default(), test_spec::()); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); let num_initial_slots = E::slots_per_epoch() * 10; @@ -3047,7 +3040,6 @@ async fn schema_downgrade_to_min_version() { let db_path = tempdir().unwrap(); let store = get_store(&db_path); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - let spec = &harness.chain.spec.clone(); harness .extend_chain( @@ -3058,6 +3050,7 @@ async fn schema_downgrade_to_min_version() { .await; let min_version = SchemaVersion(19); + let genesis_state_root = Some(harness.chain.genesis_state_root); // Save the slot clock so that the new harness doesn't revert in time. let slot_clock = harness.chain.slot_clock.clone(); @@ -3070,25 +3063,22 @@ async fn schema_downgrade_to_min_version() { let store = get_store(&db_path); // Downgrade. - let deposit_contract_deploy_block = 0; migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, CURRENT_SCHEMA_VERSION, min_version, store.logger().clone(), - spec, ) .expect("schema downgrade to minimum version should work"); // Upgrade back. migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, min_version, CURRENT_SCHEMA_VERSION, store.logger().clone(), - spec, ) .expect("schema upgrade from minimum version should work"); @@ -3111,11 +3101,10 @@ async fn schema_downgrade_to_min_version() { let min_version_sub_1 = SchemaVersion(min_version.as_u64().checked_sub(1).unwrap()); migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, CURRENT_SCHEMA_VERSION, min_version_sub_1, harness.logger().clone(), - spec, ) .expect_err("should not downgrade below minimum version"); } From b2f785a4fb618c673ed04f2e6c3204d99df1abeb Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 19 Aug 2024 17:22:26 +1000 Subject: [PATCH 20/54] Update schema downgrade test --- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index d69aca8efa4..78a654f56bd 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3049,7 +3049,7 @@ async fn schema_downgrade_to_min_version() { ) .await; - let min_version = SchemaVersion(19); + let min_version = SchemaVersion(22); let genesis_state_root = Some(harness.chain.genesis_state_root); // Save the slot clock so that the new harness doesn't revert in time. From 7789725831d1f98efcfc76036e164b196cb1ca03 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 19 Aug 2024 18:06:24 +1000 Subject: [PATCH 21/54] Fix tests --- beacon_node/client/src/builder.rs | 14 +++++++------- beacon_node/tests/test.rs | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index bd9e877657c..9491d668732 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1061,17 +1061,17 @@ where .chain_spec .clone() .ok_or("disk_store requires a chain spec")?; - let network_config = context - .eth2_network_config - .as_ref() - .ok_or("disk_store requires a network config")?; self.db_path = Some(hot_path.into()); self.freezer_db_path = Some(cold_path.into()); - let genesis_state_root = network_config - .genesis_state_root::() - .map_err(|e| format!("error determining genesis state root: {e:?}"))?; + // Optionally grab the genesis state root. + // This will only be required if a DB upgrade to V22 is needed. + let genesis_state_root = context + .eth2_network_config + .as_ref() + .and_then(|config| config.genesis_state_root::().transpose()) + .transpose()?; let schema_upgrade = |db, from, to| { migrate_schema::>( diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index bbec70330b7..8a2a0a87127 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -26,7 +26,6 @@ fn build_node(env: &mut Environment) -> LocalBeaconNode { fn http_server_genesis_state() { let mut env = env_builder() .null_logger() - //.async_logger("debug", None) .expect("should build env logger") .multi_threaded_tokio_runtime() .expect("should start tokio runtime") From a4582c5d0b533640b3ee2bf1a891a31aa3d6c48d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 26 Aug 2024 17:08:23 +1000 Subject: [PATCH 22/54] Fix null anchor migration --- .../src/schema_change/migration_schema_v22.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 717d2874fc5..9269fe44dac 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -41,11 +41,14 @@ pub fn upgrade_to_v22( ) -> Result, Error> { info!(log, "Upgrading from v21 to v22"); - let anchor = db.get_anchor_info().ok_or(Error::NoAnchorInfo)?; + let anchor = db.get_anchor_info(); let split_slot = db.get_split_slot(); let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; - if !db.get_config().allow_tree_states_migration && !anchor.no_historic_states_stored(split_slot) + if !db.get_config().allow_tree_states_migration + && anchor + .as_ref() + .map_or(true, |anchor| !anchor.no_historic_states_stored(split_slot)) { error!( log, @@ -61,7 +64,8 @@ pub fn upgrade_to_v22( let mut ops = vec![]; - rewrite_block_roots::(&db, anchor.oldest_block_slot, split_slot, &mut ops, &log)?; + let oldest_block_slot = anchor.map_or(Slot::new(0), |a| a.oldest_block_slot); + rewrite_block_roots::(&db, oldest_block_slot, split_slot, &mut ops, &log)?; let mut genesis_state = load_old_schema_frozen_state::(&db, genesis_state_root)? .ok_or(Error::MissingGenesisState)?; From 47afa49bcf8114de3674195e53125a30c032fa2d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Sep 2024 10:36:00 +1000 Subject: [PATCH 23/54] Fix tree states upgrade migration (#6328) * Towards crash safety * Fix compilation * Move cold summaries and state roots to new columns * Rename StateRoots chunked field * Update prune states --- beacon_node/beacon_chain/src/schema_change.rs | 4 +- .../src/schema_change/migration_schema_v22.rs | 144 ++++++++++++++++-- beacon_node/store/src/chunked_vector.rs | 16 +- beacon_node/store/src/hot_cold_store.rs | 35 +++-- beacon_node/store/src/lib.rs | 35 ++++- beacon_node/store/src/partial_beacon_state.rs | 8 +- 6 files changed, 194 insertions(+), 48 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 2f9ba87c1bd..8b0895e423f 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -55,9 +55,7 @@ pub fn migrate_schema( db.store_schema_version_atomically(to, ops) } (SchemaVersion(21), SchemaVersion(22)) => { - let ops = - migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log)?; - db.store_schema_version_atomically(to, ops) + migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log) } // FIXME(sproul): consider downgrade // Anything else is an error. diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 9269fe44dac..83c9625705b 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -4,8 +4,11 @@ use slog::{info, Logger}; use std::sync::Arc; use store::chunked_iter::ChunkedVectorIter; use store::{ - chunked_vector::BlockRoots, get_key_for_col, partial_beacon_state::PartialBeaconState, - DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, + chunked_vector::BlockRootsChunked, + get_key_for_col, + metadata::{SchemaVersion, STATE_UPPER_LIMIT_NO_RETAIN}, + partial_beacon_state::PartialBeaconState, + AnchorInfo, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, }; use types::{BeaconState, Hash256, Slot}; @@ -38,15 +41,15 @@ pub fn upgrade_to_v22( db: Arc>, genesis_state_root: Option, log: Logger, -) -> Result, Error> { +) -> Result<(), Error> { info!(log, "Upgrading from v21 to v22"); - let anchor = db.get_anchor_info(); + let old_anchor = db.get_anchor_info(); let split_slot = db.get_split_slot(); let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; if !db.get_config().allow_tree_states_migration - && anchor + && old_anchor .as_ref() .map_or(true, |anchor| !anchor.no_historic_states_stored(split_slot)) { @@ -62,30 +65,143 @@ pub fn upgrade_to_v22( return Err(Error::DestructiveFreezerUpgrade); } - let mut ops = vec![]; - - let oldest_block_slot = anchor.map_or(Slot::new(0), |a| a.oldest_block_slot); - rewrite_block_roots::(&db, oldest_block_slot, split_slot, &mut ops, &log)?; + let mut cold_ops = vec![]; + // Load the genesis state in the previous chunked format, BEFORE we go deleting or rewriting + // anything. let mut genesis_state = load_old_schema_frozen_state::(&db, genesis_state_root)? .ok_or(Error::MissingGenesisState)?; let genesis_state_root = genesis_state.update_tree_hash_cache()?; + let genesis_block_root = genesis_state.get_latest_block_root(genesis_state_root); + + // Store the genesis state in the new format, prior to updating the schema version on disk. + // In case of a crash no data is lost because we will re-load it in the old format and re-do + // this write. + if split_slot > 0 { + info!( + log, + "Re-storing genesis state"; + "state_root" => ?genesis_state_root, + ); + db.store_cold_state(&genesis_state_root, &genesis_state, &mut cold_ops)?; + } + + // Write the block roots in the new format. Similar to above, we do this separately from + // deleting the old format block roots so that this is crash safe. + let oldest_block_slot = old_anchor + .as_ref() + .map_or(Slot::new(0), |a| a.oldest_block_slot); + rewrite_block_roots::( + &db, + genesis_block_root, + oldest_block_slot, + split_slot, + &mut cold_ops, + &log, + )?; + + // Commit this first batch of non-destructive cold database ops. + db.cold_db.do_atomically(cold_ops)?; + + // Now we update the anchor and the schema version atomically in the hot database. + // + // If we crash after commiting this change, then there will be some leftover cruft left in the + // freezer database, but no corruption because all the new-format data has already been written + // above. + let new_anchor = if let Some(old_anchor) = &old_anchor { + AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() + } + } else { + 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), + } + }; + let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, Some(new_anchor))?]; + db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; + + // Finally, clean up the old-format data from the freezer database. + delete_old_schema_freezer_data::(&db, &log)?; + + Ok(()) +} + +pub fn delete_old_schema_freezer_data( + db: &Arc>, + log: &Logger, +) -> Result<(), Error> { + let mut cold_ops = vec![]; + + let columns = [ + DBColumn::BeaconState, + // Cold state summaries indexed by state root were stored in this column. + DBColumn::BeaconStateSummary, + // Mapping from restore point number to state root was stored in this column. + DBColumn::BeaconRestorePoint, + // Chunked vector values were stored in these columns. + DBColumn::BeaconHistoricalRoots, + DBColumn::BeaconRandaoMixes, + DBColumn::BeaconHistoricalSummaries, + DBColumn::BeaconBlockRootsChunked, + DBColumn::BeaconStateRootsChunked, + ]; + + for column in columns { + for res in db.cold_db.iter_column_keys::>(column) { + let key = res?; + cold_ops.push(KeyValueStoreOp::DeleteKey(get_key_for_col( + column.as_str(), + &key, + ))); + } + } + let delete_ops = cold_ops.len(); + + info!( + log, + "Deleting historic states"; + "delete_ops" => delete_ops, + ); + db.cold_db.do_atomically(cold_ops)?; - db.prune_historic_states(genesis_state_root, &genesis_state)?; + // In order to reclaim space, we need to compact the freezer DB as well. + db.cold_db.compact()?; - Ok(ops) + Ok(()) } pub fn rewrite_block_roots( db: &HotColdDB, + genesis_block_root: Hash256, oldest_block_slot: Slot, split_slot: Slot, - ops: &mut Vec, + cold_ops: &mut Vec, log: &Logger, ) -> Result<(), Error> { + info!( + log, + "Starting beacon block root migration"; + "oldest_block_slot" => oldest_block_slot, + "genesis_block_root" => ?genesis_block_root, + ); + + // Store the genesis block root if it would otherwise not be stored. + if oldest_block_slot != 0 { + cold_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &0u64.to_be_bytes()), + genesis_block_root.as_bytes().to_vec(), + )); + } + // Block roots are available from the `oldest_block_slot` to the `split_slot`. let start_vindex = oldest_block_slot.as_usize(); - let block_root_iter = ChunkedVectorIter::::new( + let block_root_iter = ChunkedVectorIter::::new( db, start_vindex, split_slot, @@ -94,7 +210,7 @@ pub fn rewrite_block_roots( // OK to hold these in memory (10M slots * 43 bytes per KV ~= 430 MB). for (i, (slot, block_root)) in block_root_iter.enumerate() { - ops.push(KeyValueStoreOp::PutKeyValue( + cold_ops.push(KeyValueStoreOp::PutKeyValue( get_key_for_col( DBColumn::BeaconBlockRoots.into(), &(slot as u64).to_be_bytes(), diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs index 4450989d590..83b8da2a189 100644 --- a/beacon_node/store/src/chunked_vector.rs +++ b/beacon_node/store/src/chunked_vector.rs @@ -322,11 +322,11 @@ macro_rules! field { } field!( - BlockRoots, + BlockRootsChunked, FixedLengthField, Hash256, E::SlotsPerHistoricalRoot, - DBColumn::BeaconBlockRoots, + DBColumn::BeaconBlockRootsChunked, |_| OncePerNSlots { n: 1, activation_slot: Some(Slot::new(0)), @@ -336,11 +336,11 @@ field!( ); field!( - StateRoots, + StateRootsChunked, FixedLengthField, Hash256, E::SlotsPerHistoricalRoot, - DBColumn::BeaconStateRoots, + DBColumn::BeaconStateRootsChunked, |_| OncePerNSlots { n: 1, activation_slot: Some(Slot::new(0)), @@ -859,8 +859,8 @@ mod test { 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(BlockRootsChunked, true); + test_fixed_length(StateRootsChunked, true); test_fixed_length(HistoricalRoots, false); test_fixed_length(RandaoMixes, true); } @@ -880,12 +880,12 @@ mod test { #[test] fn needs_genesis_value_block_roots() { - needs_genesis_value_once_per_slot(BlockRoots); + needs_genesis_value_once_per_slot(BlockRootsChunked); } #[test] fn needs_genesis_value_state_roots() { - needs_genesis_value_once_per_slot(StateRoots); + needs_genesis_value_once_per_slot(StateRootsChunked); } #[test] diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 2176b7409a3..cbde408d5b2 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1515,7 +1515,7 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } - pub fn load_cold_state_bytes_as_snapshot(&self, slot: Slot) -> Result>, Error> { + 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(), @@ -1535,7 +1535,7 @@ impl, Cold: ItemStore> HotColdDB } } - pub fn load_cold_state_as_snapshot(&self, slot: Slot) -> Result>, Error> { + 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)) @@ -2631,20 +2631,31 @@ impl, Cold: ItemStore> HotColdDB // migrating to the tree-states schema (delete everything in the freezer then start afresh). let mut cold_ops = vec![]; - // This function works for both pre-tree-states and post-tree-states pruning. It deletes - // everything related to historic states from either DB! - let columns = [ - DBColumn::BeaconState, - DBColumn::BeaconStateSummary, + let current_schema_columns = vec![ + DBColumn::BeaconColdStateSummary, DBColumn::BeaconStateSnapshot, DBColumn::BeaconStateDiff, - DBColumn::BeaconRestorePoint, DBColumn::BeaconStateRoots, + ]; + + // This function is intended to be able to clean up leftover V21 freezer database stuff in + // the case where the V22 schema upgrade failed *after* commiting the version increment but + // *before* cleaning up the freezer DB. + // + // We can remove this once schema V21 has been gone for a while. + let previous_schema_columns = vec![ + DBColumn::BeaconStateSummary, + DBColumn::BeaconBlockRootsChunked, + DBColumn::BeaconStateRootsChunked, + DBColumn::BeaconRestorePoint, DBColumn::BeaconHistoricalRoots, DBColumn::BeaconRandaoMixes, DBColumn::BeaconHistoricalSummaries, ]; + let mut columns = current_schema_columns; + columns.extend(previous_schema_columns); + for column in columns { for res in self.cold_db.iter_column_keys::>(column) { let key = res?; @@ -2656,11 +2667,7 @@ impl, Cold: ItemStore> HotColdDB } let delete_ops = cold_ops.len(); - // If we just deleted the the genesis state, re-store it using the *current* schema, which - // may be different from the schema of the genesis state we just deleted. - // - // During the tree-states migration this will re-store the genesis state as compressed - // beacon state SSZ, which is different from the previous `PartialBeaconState` format. + // If we just deleted the the genesis state, re-store it using the current* schema. if self.get_split_slot() > 0 { info!( self.log, @@ -2992,7 +2999,7 @@ pub(crate) struct ColdStateSummary { impl StoreItem for ColdStateSummary { fn db_column() -> DBColumn { - DBColumn::BeaconStateSummary + DBColumn::BeaconColdStateSummary } fn as_store_bytes(&self) -> Vec { diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 697e932b4c6..8b2a4b232c9 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -268,9 +268,15 @@ pub enum DBColumn { /// For compact `BeaconStateDiff`s in the freezer DB. #[strum(serialize = "bsd")] BeaconStateDiff, - /// For the mapping from state roots to their slots or summaries. + /// Mapping from state root to `HotStateSummary` in the hot DB. + /// + /// Previously this column also served a role in the freezer DB, mapping state roots to + /// `ColdStateSummary`. However that role is now filled by `BeaconColdStateSummary`. #[strum(serialize = "bss")] BeaconStateSummary, + /// Mapping from state root to `ColdStateSummary` in the cold DB. + #[strum(serialize = "bcs")] + BeaconColdStateSummary, /// For the list of temporary states stored during block import, /// and then made non-temporary by the deletion of their state root from this column. #[strum(serialize = "bst")] @@ -294,12 +300,28 @@ pub enum DBColumn { /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "brp")] BeaconRestorePoint, - /// Mapping from slot to beacon block root in the freezer DB. - #[strum(serialize = "bbr")] - BeaconBlockRoots, /// Mapping from slot to beacon state root in the freezer DB. - #[strum(serialize = "bsr")] + /// + /// This new column was created to replace the previous `bsr` column. The replacement was + /// necessary to guarantee atomicity of the upgrade migration. + #[strum(serialize = "bsx")] BeaconStateRoots, + /// DEPRECATED. This is the previous column for beacon state roots stored by "chunk index". + /// + /// Can be removed once schema v22 is buried by a hard fork. + #[strum(serialize = "bsr")] + BeaconStateRootsChunked, + /// Mapping from slot to beacon block root in the freezer DB. + /// + /// This new column was created to replace the previous `bbr` column. The replacement was + /// necessary to guarantee atomicity of the upgrade migration. + #[strum(serialize = "bbx")] + BeaconBlockRoots, + /// DEPRECATED. This is the previous column for beacon block roots stored by "chunk index". + /// + /// Can be removed once schema v22 is buried by a hard fork. + #[strum(serialize = "bbr")] + BeaconBlockRootsChunked, /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "bhr")] BeaconHistoricalRoots, @@ -347,6 +369,7 @@ impl DBColumn { | Self::BeaconState | Self::BeaconBlob | Self::BeaconStateSummary + | Self::BeaconColdStateSummary | Self::BeaconStateTemporary | Self::ExecPayload | Self::BeaconChain @@ -358,7 +381,9 @@ impl DBColumn { | Self::DhtEnrs | Self::OptimisticTransitionBlock => 32, Self::BeaconBlockRoots + | Self::BeaconBlockRootsChunked | Self::BeaconStateRoots + | Self::BeaconStateRootsChunked | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries | Self::BeaconRandaoMixes diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index c646fe276ba..2eb40f47b18 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -1,6 +1,6 @@ use crate::chunked_vector::{ - load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots, - HistoricalSummaries, RandaoMixes, StateRoots, + load_variable_list_from_db, load_vector_from_db, BlockRootsChunked, HistoricalRoots, + HistoricalSummaries, RandaoMixes, StateRootsChunked, }; use crate::{Error, KeyValueStore}; use ssz::{Decode, DecodeError}; @@ -173,7 +173,7 @@ impl PartialBeaconState { spec: &ChainSpec, ) -> Result<(), Error> { if self.block_roots().is_none() { - *self.block_roots_mut() = Some(load_vector_from_db::( + *self.block_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, @@ -188,7 +188,7 @@ impl PartialBeaconState { spec: &ChainSpec, ) -> Result<(), Error> { if self.state_roots().is_none() { - *self.state_roots_mut() = Some(load_vector_from_db::( + *self.state_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, From 1e6b2d66330c6c9652842e25fcfb9df6356099a4 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 5 Sep 2024 17:27:23 +1000 Subject: [PATCH 24/54] Clean hdiff CLI flag and metrics --- beacon_node/src/cli.rs | 14 ++++++- beacon_node/src/config.rs | 18 +++++++-- beacon_node/store/src/config.rs | 10 ++--- beacon_node/store/src/hot_cold_store.rs | 46 +++++++++-------------- beacon_node/store/src/metrics.rs | 50 +++++++++++++------------ lighthouse/tests/beacon_node.rs | 20 ++++++---- 6 files changed, 86 insertions(+), 72 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 76655eee655..79691240367 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -802,11 +802,23 @@ pub fn cli_app() -> Command { Arg::new("historic-state-cache-size") .long("historic-state-cache-size") .value_name("SIZE") - .help("Specifies how many states from the freezer database should cache in memory") + .help("This cache is currently inactive. Please use hdiff-buffer-cache-size instead.") .default_value("1") .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("hdiff-buffer-cache-size") + .long("hdiff-buffer-cache-size") + .value_name("SIZE") + .help("Number of hierarchical diff (hdiff) buffers to cache in memory. Each buffer \ + is around the size of a BeaconState so you should be cautious about setting \ + this value too high. This flag is irrelevant for most nodes, which run with \ + state pruning enabled.") + .default_value("16") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("state-cache-size") .long("state-cache-size") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index ebe6ba8b1b1..9af7ce537e9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -434,11 +434,21 @@ pub fn get_config( .map_err(|_| "state-cache-size is not a valid integer".to_string())?; } - if let Some(historic_state_cache_size) = cli_args.get_one::("historic-state-cache-size") + if cli_args + .get_one::("historic-state-cache-size") + .is_some() { - client_config.store.historic_state_cache_size = historic_state_cache_size - .parse() - .map_err(|_| "historic-state-cache-size is not a valid integer".to_string())?; + warn!( + log, + "Historic state cache is currently disabled. \ + Please use hdiff-buffer-cache-size instead" + ); + } + + if let Some(hdiff_buffer_cache_size) = + clap_utils::parse_optional(cli_args, "hdiff-buffer-cache-size")? + { + client_config.store.hdiff_buffer_cache_size = hdiff_buffer_cache_size; } client_config.store.compact_on_init = cli_args.get_flag("compact-db"); diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 7bc5a7cd536..bb94a1e4f1e 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -15,9 +15,8 @@ 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); +pub const DEFAULT_HDIFF_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; @@ -33,9 +32,7 @@ pub struct StoreConfig { /// 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, + pub hdiff_buffer_cache_size: NonZeroUsize, /// Whether to compact the database on initialization. pub compact_on_init: bool, /// Whether to compact the database during database pruning. @@ -109,9 +106,8 @@ impl Default for StoreConfig { 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, + hdiff_buffer_cache_size: DEFAULT_HDIFF_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, diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index cbde408d5b2..28e5cfcd5fa 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -74,12 +74,8 @@ pub struct HotColdDB, Cold: ItemStore> { /// /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. state_cache: Mutex>, - /// LRU cache of replayed states. - // FIXME(sproul): re-enable historic state cache - #[allow(dead_code)] - historic_state_cache: Mutex>>, /// Cache of hierarchical diff buffers. - diff_buffer_cache: Mutex>, + hdiff_buffer_cache: Mutex>, /// Chain spec. pub(crate) spec: ChainSpec, /// Logger. @@ -216,8 +212,7 @@ impl HotColdDB, MemoryStore> { hot_db: MemoryStore::open(), block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), - diff_buffer_cache: Mutex::new(LruCache::new(config.diff_buffer_cache_size)), + hdiff_buffer_cache: Mutex::new(LruCache::new(config.hdiff_buffer_cache_size)), config, hierarchy, spec, @@ -257,8 +252,7 @@ impl HotColdDB, LevelDB> { hot_db: LevelDB::open(hot_path)?, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), - diff_buffer_cache: Mutex::new(LruCache::new(config.diff_buffer_cache_size)), + hdiff_buffer_cache: Mutex::new(LruCache::new(config.hdiff_buffer_cache_size)), config, hierarchy, spec, @@ -439,13 +433,13 @@ impl, Cold: ItemStore> HotColdDB } pub fn register_metrics(&self) { - let diff_buffer_cache = self.diff_buffer_cache.lock(); - let diff_buffer_cache_byte_size = diff_buffer_cache + let hdiff_buffer_cache = self.hdiff_buffer_cache.lock(); + let hdiff_buffer_cache_byte_size = hdiff_buffer_cache .iter() .map(|(_, diff)| diff.size()) .sum::(); - let diff_buffer_cache_len = diff_buffer_cache.len(); - drop(diff_buffer_cache); + let hdiff_buffer_cache_len = hdiff_buffer_cache.len(); + drop(hdiff_buffer_cache); metrics::set_gauge( &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, @@ -460,16 +454,12 @@ impl, Cold: ItemStore> HotColdDB self.state_cache.lock().len() as i64, ); metrics::set_gauge( - &metrics::STORE_BEACON_HISTORIC_STATE_CACHE_SIZE, - self.historic_state_cache.lock().len() as i64, + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE, + hdiff_buffer_cache_len as i64, ); metrics::set_gauge( - &metrics::STORE_BEACON_DIFF_BUFFER_CACHE_SIZE, - diff_buffer_cache_len as i64, - ); - metrics::set_gauge( - &metrics::STORE_BEACON_DIFF_BUFFER_CACHE_BYTE_SIZE, - diff_buffer_cache_byte_size as i64, + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, + hdiff_buffer_cache_byte_size as i64, ); } @@ -1552,7 +1542,7 @@ impl, Cold: ItemStore> HotColdDB let (_, base_buffer) = self.load_hdiff_buffer_for_slot(from_slot, 0)?; let target_buffer = HDiffBuffer::from_state(state.clone()); let diff = { - let _timer = metrics::start_timer(&metrics::STORE_BEACON_DIFF_BUFFER_COMPUTE_TIME); + let _timer = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME); HDiff::compute(&base_buffer, &target_buffer, &self.config)? }; let diff_bytes = diff.as_ssz_bytes(); @@ -1617,16 +1607,16 @@ impl, Cold: ItemStore> HotColdDB slot: Slot, recursion: usize, ) -> Result<(Slot, HDiffBuffer), Error> { - if let Some(buffer) = self.diff_buffer_cache.lock().get(&slot) { + if let Some(buffer) = self.hdiff_buffer_cache.lock().get(&slot) { debug!( self.log, "Hit diff buffer cache"; "slot" => slot ); - metrics::inc_counter(&metrics::STORE_BEACON_DIFF_BUFFER_CACHE_HIT); + metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT); return Ok((slot, buffer.clone())); } else { - metrics::inc_counter(&metrics::STORE_BEACON_DIFF_BUFFER_CACHE_MISS); + metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); } // Do not time recursive calls into load_hdiff_buffer_for_slot to not double count @@ -1644,7 +1634,7 @@ impl, Cold: ItemStore> HotColdDB .ok_or(Error::MissingSnapshot(slot))?; let buffer = HDiffBuffer::from_state(state); - self.diff_buffer_cache.lock().put(slot, buffer.clone()); + self.hdiff_buffer_cache.lock().put(slot, buffer.clone()); debug!( self.log, "Added diff buffer to cache"; @@ -1663,11 +1653,11 @@ impl, Cold: ItemStore> HotColdDB let diff = self.load_hdiff_for_slot(slot)?; { let _timer = - metrics::start_timer(&metrics::STORE_BEACON_DIFF_BUFFER_APPLY_TIME); + metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_APPLY_TIME); diff.apply(&mut buffer, &self.config)?; } - self.diff_buffer_cache.lock().put(slot, buffer.clone()); + self.hdiff_buffer_cache.lock().put(slot, buffer.clone()); debug!( self.log, "Added diff buffer to cache"; diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 095085a747a..5284b7f3b66 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -180,17 +180,17 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: LazyLock> = "Current count of items in beacon store historic state cache", ) }); -pub static STORE_BEACON_DIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( - "store_beacon_diff_buffer_cache_size", - "Current count of items in beacon store diff buffer cache", + "store_beacon_hdiff_buffer_cache_size", + "Current count of items in beacon store hdiff buffer cache", ) }); -pub static STORE_BEACON_DIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( - "store_beacon_diff_buffer_cache_byte_size", - "Current byte size sum of all elements in beacon store diff buffer cache", + "store_beacon_hdiff_buffer_cache_byte_size", + "Current byte size sum of all elements in beacon store hdiff buffer cache", ) }); pub static STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: LazyLock> = @@ -207,17 +207,18 @@ pub static STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "store_beacon_diff_buffer_apply_seconds", - "Time taken to apply diff buffer to a state buffer", - ) -}); -pub static STORE_BEACON_DIFF_BUFFER_COMPUTE_TIME: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_APPLY_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_apply_seconds", + "Time taken to apply hdiff buffer to a state buffer", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( - "store_beacon_diff_buffer_compute_seconds", - "Time taken to compute diff buffer to a state buffer", + "store_beacon_hdiff_buffer_compute_seconds", + "Time taken to compute hdiff buffer to a state buffer", ) }); pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = LazyLock::new(|| { @@ -226,17 +227,18 @@ pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = La "Time taken to load an hdiff buffer", ) }); -pub static STORE_BEACON_DIFF_BUFFER_CACHE_HIT: LazyLock> = LazyLock::new(|| { - try_create_int_counter( - "store_beacon_diff_buffer_cache_hit_total", - "Total count of diff buffer cache hits", - ) -}); -pub static STORE_BEACON_DIFF_BUFFER_CACHE_MISS: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_hdiff_buffer_cache_hit_total", + "Total count of hdiff buffer cache hits", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "store_beacon_diff_buffer_cache_miss_total", - "Total count of diff buffer cache miss", + "store_beacon_hdiff_buffer_cache_miss_total", + "Total count of hdiff buffer cache miss", ) }); pub static STORE_BEACON_REPLAYED_BLOCKS: LazyLock> = LazyLock::new(|| { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 290d3426a20..3568e829e3d 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1886,27 +1886,31 @@ fn state_cache_size_flag() { .run_with_zero_port() .with_config(|config| assert_eq!(config.store.state_cache_size, new_non_zero_usize(64))); } +// This flag is deprecated but should not cause a crash. #[test] fn historic_state_cache_size_flag() { CommandLineTest::new() .flag("historic-state-cache-size", Some("4")) + .run_with_zero_port(); +} +#[test] +fn hdiff_buffer_cache_size_flag() { + CommandLineTest::new() + .flag("hdiff-buffer-cache-size", Some("1")) .run_with_zero_port() .with_config(|config| { - assert_eq!( - config.store.historic_state_cache_size, - new_non_zero_usize(4) - ) + assert_eq!(config.store.hdiff_buffer_cache_size, 1); }); } #[test] -fn historic_state_cache_size_default() { - use beacon_node::beacon_chain::store::config::DEFAULT_HISTORIC_STATE_CACHE_SIZE; +fn hdiff_buffer_cache_size_default() { + use beacon_node::beacon_chain::store::config::DEFAULT_HDIFF_BUFFER_CACHE_SIZE; CommandLineTest::new() .run_with_zero_port() .with_config(|config| { assert_eq!( - config.store.historic_state_cache_size, - DEFAULT_HISTORIC_STATE_CACHE_SIZE + config.store.hdiff_buffer_cache_size, + DEFAULT_HDIFF_BUFFER_CACHE_SIZE ); }); } From 45a07627b56dbdfa364e191ee8b70fb888267aed Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 11:29:30 +1000 Subject: [PATCH 25/54] Fix "staged reconstruction" --- beacon_node/beacon_chain/src/migrate.rs | 60 +++++++++++++++++-------- beacon_node/store/src/errors.rs | 2 +- beacon_node/store/src/reconstruct.rs | 25 ++++++++--- lighthouse/tests/beacon_node.rs | 2 +- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index f83535e92c5..ed49c9e762c 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -192,7 +192,9 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator>, log: &Logger) { - // FIXME(sproul): still need to port more changes here - if let Err(e) = db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { - error!( - log, - "State reconstruction failed"; - "error" => ?e, - ); + pub fn run_reconstruction( + db: Arc>, + opt_tx: Option>, + log: &Logger, + ) { + match db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { + Ok(()) => { + // Schedule another reconstruction batch if required and we have access to the + // channel for requeueing. + if let Some(tx) = opt_tx { + if db.get_anchor_info().is_some() { + if let Err(e) = tx.send(Notification::Reconstruction) { + error!( + log, + "Unable to requeue reconstruction notification"; + "error" => ?e + ); + } + } + } + } + Err(e) => { + error!( + log, + "State reconstruction failed"; + "error" => ?e, + ); + } } } @@ -393,6 +415,7 @@ impl, Cold: ItemStore> BackgroundMigrator (mpsc::Sender, thread::JoinHandle<()>) { let (tx, rx) = mpsc::channel(); + let inner_tx = tx.clone(); let thread = thread::spawn(move || { while let Ok(notif) = rx.recv() { let mut reconstruction_notif = None; @@ -423,16 +446,17 @@ impl, Cold: ItemStore> BackgroundMigrator anchor.state_lower_limit, ); @@ -142,11 +142,26 @@ where Some(anchor.clone()), )?; } + + // If this is the end of the batch, return Ok. The caller will run another + // batch when there is idle capacity. + if num_blocks.map_or(false, |n_blocks| { + slot + 1 == lower_limit_slot + n_blocks as u64 + }) { + debug!( + self.log, + "Finished state reconstruction batch"; + "start_slot" => lower_limit_slot, + "end_slot" => slot, + ); + return Ok(()); + } } } - // Should always reach the `upper_limit_slot` and return early above. - Err(Error::StateReconstructionDidNotComplete) + // Should always reach the `upper_limit_slot` or the end of the batch and return early + // above. + Err(Error::StateReconstructionLogicError) })??; // Check that the split point wasn't mutated during the state reconstruction process. diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 3568e829e3d..0ccc3386a2c 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1899,7 +1899,7 @@ fn hdiff_buffer_cache_size_flag() { .flag("hdiff-buffer-cache-size", Some("1")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.store.hdiff_buffer_cache_size, 1); + assert_eq!(config.store.hdiff_buffer_cache_size.get(), 1); }); } #[test] From e00f6399a772925916dada6282b861e9e1c2a398 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 11:40:35 +1000 Subject: [PATCH 26/54] Fix alloy issues --- beacon_node/beacon_chain/src/historical_blocks.rs | 4 ++-- .../src/schema_change/migration_schema_v22.rs | 8 ++++---- beacon_node/store/src/config.rs | 2 +- beacon_node/store/src/hot_cold_store.rs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 2f0dcbbfd96..bb9b57a25c6 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -148,7 +148,7 @@ impl BeaconChain { for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { cold_batch.push(KeyValueStoreOp::PutKeyValue( get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), - block_root.as_bytes().to_vec(), + block_root.as_slice().to_vec(), )); } @@ -164,7 +164,7 @@ impl BeaconChain { 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(), + self.genesis_block_root.as_slice().to_vec(), )); } prev_block_slot = genesis_slot; diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 83c9625705b..7c3c7d1f1ac 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -20,7 +20,7 @@ fn load_old_schema_frozen_state( ) -> Result>, Error> { let Some(partial_state_bytes) = db .cold_db - .get_bytes(DBColumn::BeaconState.into(), state_root.as_bytes())? + .get_bytes(DBColumn::BeaconState.into(), state_root.as_slice())? else { return Ok(None); }; @@ -118,7 +118,7 @@ pub fn upgrade_to_v22( AnchorInfo { anchor_slot: Slot::new(0), oldest_block_slot: Slot::new(0), - oldest_block_parent: Hash256::zero(), + oldest_block_parent: Hash256::ZERO, state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, state_lower_limit: Slot::new(0), } @@ -195,7 +195,7 @@ pub fn rewrite_block_roots( if oldest_block_slot != 0 { cold_ops.push(KeyValueStoreOp::PutKeyValue( get_key_for_col(DBColumn::BeaconBlockRoots.into(), &0u64.to_be_bytes()), - genesis_block_root.as_bytes().to_vec(), + genesis_block_root.as_slice().to_vec(), )); } @@ -215,7 +215,7 @@ pub fn rewrite_block_roots( DBColumn::BeaconBlockRoots.into(), &(slot as u64).to_be_bytes(), ), - block_root.as_bytes().to_vec(), + block_root.as_slice().to_vec(), )); if i > 0 && i % LOG_EVERY == 0 { diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index bb94a1e4f1e..b58c3fb8fe0 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -309,7 +309,7 @@ mod test { let anchor = AnchorInfo { anchor_slot: Slot::new(0), oldest_block_slot: Slot::new(0), - oldest_block_parent: Hash256::zero(), + oldest_block_parent: Hash256::ZERO, state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, state_lower_limit: Slot::new(0), }; diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index a6c8943a2fb..2a9a3eac5b9 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1432,7 +1432,7 @@ impl, Cold: ItemStore> HotColdDB DBColumn::BeaconStateRoots.into(), &slot.as_u64().to_be_bytes(), ), - state_root.as_bytes().to_vec(), + state_root.as_slice().to_vec(), )); Ok(()) } @@ -2285,7 +2285,7 @@ impl, Cold: ItemStore> HotColdDB 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.as_slice().to_vec(), )); } Ok(ops) @@ -2793,7 +2793,7 @@ pub fn migrate_database, Cold: ItemStore>( DBColumn::BeaconBlockRoots.into(), &slot.as_u64().to_be_bytes(), ), - block_root.as_bytes().to_vec(), + block_root.as_slice().to_vec(), )); // Delete the old summary, and the full state if we lie on an epoch boundary. From 907a7c052871ece7771bb39d1849371a75c00539 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 12:46:56 +1000 Subject: [PATCH 27/54] Fix staged reconstruction logic --- beacon_node/store/src/reconstruct.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 428eaf89c51..655ecb2008f 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -104,8 +104,16 @@ where // Stage state for storage in freezer DB. self.store_cold_state(&state_root, &state, &mut io_batch)?; + let batch_complete = num_blocks.map_or(false, |n_blocks| { + slot + 1 == lower_limit_slot + n_blocks as u64 + }); + let reconstruction_complete = slot + 1 == upper_limit_slot; + // If the slot lies on an epoch boundary, commit the batch and update the anchor. - if self.hierarchy.should_commit_immediately(slot)? || slot + 1 == upper_limit_slot { + if self.hierarchy.should_commit_immediately(slot)? + || batch_complete + || reconstruction_complete + { info!( self.log, "State reconstruction in progress"; @@ -118,7 +126,7 @@ where // Update anchor. let old_anchor = Some(anchor.clone()); - if slot + 1 == upper_limit_slot { + if reconstruction_complete { // The two limits have met in the middle! We're done! // Perform one last integrity check on the state reached. let computed_state_root = state.update_tree_hash_cache()?; @@ -145,9 +153,7 @@ where // If this is the end of the batch, return Ok. The caller will run another // batch when there is idle capacity. - if num_blocks.map_or(false, |n_blocks| { - slot + 1 == lower_limit_slot + n_blocks as u64 - }) { + if batch_complete { debug!( self.log, "Finished state reconstruction batch"; From 024843e5bacfb808d11cc72126ab54713954e756 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 13:22:43 +1000 Subject: [PATCH 28/54] Prevent weird slot drift --- beacon_node/store/src/reconstruct.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 655ecb2008f..cb55abe798a 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -43,12 +43,14 @@ where 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. + // If `num_blocks` is not specified iterate all blocks. Add 1 so that we end on an epoch + // boundary when `num_blocks` is a multiple of an epoch boundary. We want to be *inclusive* + // of the state at slot `lower_limit_slot + num_blocks`. let block_root_iter = self .forwards_block_roots_iterator_until(lower_limit_slot, upper_limit_slot - 1, || { Err(Error::StateShouldNotBeRequired(upper_limit_slot - 1)) })? - .take(num_blocks.unwrap_or(usize::MAX)); + .take(num_blocks.map_or(usize::MAX, |n| n + 1)); // The state to be advanced. let mut state = self @@ -104,12 +106,15 @@ where // Stage state for storage in freezer DB. self.store_cold_state(&state_root, &state, &mut io_batch)?; - let batch_complete = num_blocks.map_or(false, |n_blocks| { - slot + 1 == lower_limit_slot + n_blocks as u64 - }); + let batch_complete = + num_blocks.map_or(false, |n_blocks| slot == lower_limit_slot + n_blocks as u64); let reconstruction_complete = slot + 1 == upper_limit_slot; - // If the slot lies on an epoch boundary, commit the batch and update the anchor. + // Commit the I/O batch if: + // + // - The diff/snapshot for this slot is required for future slots, or + // - The reconstruction batch is complete (we are about to return), or + // - Reconstruction is complete. if self.hierarchy.should_commit_immediately(slot)? || batch_complete || reconstruction_complete From bdf04c8652576b7cf9ddaa3bf790543599894639 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 15:44:25 +1000 Subject: [PATCH 29/54] Remove "allow" flag --- .../src/schema_change/migration_schema_v22.rs | 18 ------------------ beacon_node/src/cli.rs | 10 ---------- beacon_node/src/config.rs | 6 ------ beacon_node/store/src/config.rs | 5 ----- beacon_node/store/src/errors.rs | 1 - lighthouse/tests/beacon_node.rs | 7 ------- 6 files changed, 47 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 7c3c7d1f1ac..76ce1ca9a31 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -1,5 +1,4 @@ use crate::beacon_chain::BeaconChainTypes; -use slog::error; use slog::{info, Logger}; use std::sync::Arc; use store::chunked_iter::ChunkedVectorIter; @@ -48,23 +47,6 @@ pub fn upgrade_to_v22( let split_slot = db.get_split_slot(); let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; - if !db.get_config().allow_tree_states_migration - && old_anchor - .as_ref() - .map_or(true, |anchor| !anchor.no_historic_states_stored(split_slot)) - { - error!( - log, - "You are attempting to migrate to tree-states but this is a destructive operation. \ - Upgrading will require FIXME(sproul) minutes of downtime before Lighthouse starts again. \ - All current historic states will be deleted. Reconstructing the states in the new \ - schema will take up to 2 weeks. \ - \ - To proceed add the flag --allow-tree-states-migration OR run lighthouse db prune-states" - ); - return Err(Error::DestructiveFreezerUpgrade); - } - let mut cold_ops = vec![]; // Load the genesis state in the previous chunked format, BEFORE we go deleting or rewriting diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index a1a6447bc7a..4e32784c837 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1034,16 +1034,6 @@ pub fn cli_app() -> Command { .default_value("0") .display_order(0) ) - .arg( - Arg::new("allow-tree-states-migration") - .long("allow-tree-states-migration") - .value_name("BOOLEAN") - .help("Whether to allow a destructive freezer DB migration for hierarchical state diffs") - .action(ArgAction::Set) - .default_value("false") - .display_order(0) - ) - /* * Misc. */ diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 9af7ce537e9..2d7bd81d300 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -488,12 +488,6 @@ pub fn get_config( client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; } - if let Some(allow_tree_states_migration) = - clap_utils::parse_optional(cli_args, "allow-tree-states-migration")? - { - client_config.store.allow_tree_states_migration = allow_tree_states_migration; - } - if let Some(malicious_withhold_count) = clap_utils::parse_optional(cli_args, "malicious-withhold-count")? { diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index b58c3fb8fe0..b42aaa31dfb 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -48,10 +48,6 @@ pub struct StoreConfig { /// The margin for blob pruning in epochs. The oldest blobs are pruned up until /// data_availability_boundary - blob_prune_margin_epochs. Default: 0. pub blob_prune_margin_epochs: u64, - /// Whether to allow a destructive freezer DB migration for hierarchical state diffs. - /// - /// i.e. "on-disk tree-states" - pub allow_tree_states_migration: bool, } /// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params. @@ -115,7 +111,6 @@ impl Default for StoreConfig { prune_blobs: true, epochs_per_blob_prune: DEFAULT_EPOCHS_PER_BLOB_PRUNE, blob_prune_margin_epochs: DEFAULT_BLOB_PUNE_MARGIN_EPOCHS, - allow_tree_states_migration: false, } } } diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index e5d7c020f0f..1692ef56444 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -81,7 +81,6 @@ pub enum Error { ForwardsIterGap(DBColumn, Slot, Slot), StateShouldNotBeRequired(Slot), MissingBlock(Hash256), - DestructiveFreezerUpgrade, NoAnchorInfo, RandaoMixOutOfBounds, GenesisStateUnknown, diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 0ccc3386a2c..93a70a88d78 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1982,13 +1982,6 @@ fn blob_prune_margin_epochs_on_startup_ten() { .with_config(|config| assert!(config.store.blob_prune_margin_epochs == 10)); } #[test] -fn allow_tree_states_migration_on_startup_true() { - CommandLineTest::new() - .flag("allow-tree-states-migration", Some("true")) - .run_with_zero_port() - .with_config(|config| assert!(config.store.allow_tree_states_migration == true)); -} -#[test] fn reconstruct_historic_states_flag() { CommandLineTest::new() .flag("reconstruct-historic-states", None) From 2d9ce8f1282cfe2a05a2c2a3446731e8d0cbe957 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 15:57:32 +1000 Subject: [PATCH 30/54] Update CLI help --- book/src/help_bn.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 847192d7482..3b932c106ed 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -8,9 +8,6 @@ beacon chain and publishing messages to the network. Usage: lighthouse beacon_node [OPTIONS] Options: - --allow-tree-states-migration - Whether to allow a destructive freezer DB migration for hierarchical - state diffs [default: false] --auto-compact-db Enable or disable automatic compaction of the database on finalization. [default: true] @@ -169,6 +166,11 @@ Options: --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated to fit in 32 bytes. + --hdiff-buffer-cache-size + Number of hierarchical diff (hdiff) buffers to cache in memory. Each + buffer is around the size of a BeaconState so you should be cautious + about setting this value too high. This flag is irrelevant for most + nodes, which run with state pruning enabled. [default: 16] --hierarchy-exponents Specifies the frequency for storing full state snapshots and hierarchical diffs in the freezer DB. Accepts a comma-separated list @@ -179,8 +181,8 @@ Options: 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] + This cache is currently inactive. Please use hdiff-buffer-cache-size + instead. [default: 1] --http-address
Set the listen address for the RESTful HTTP API server. --http-allow-origin From 9de88fd8c55008251f53730388852d54b7531de1 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 9 Sep 2024 16:19:31 +1000 Subject: [PATCH 31/54] Remove FIXME about downgrade --- beacon_node/beacon_chain/src/schema_change.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 8b0895e423f..4344ddadf9a 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -57,7 +57,6 @@ pub fn migrate_schema( (SchemaVersion(21), SchemaVersion(22)) => { migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log) } - // FIXME(sproul): consider downgrade // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, From 8a50f2a0b74a92f3f2e2c6484bdef42666ee20fa Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 11 Sep 2024 10:08:31 +1000 Subject: [PATCH 32/54] Remove some unnecessary error variants --- beacon_node/store/src/errors.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 88a4c4d42c0..3bc44701b64 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -65,23 +65,15 @@ pub enum Error { }, AddPayloadLogicError, SlotClockUnavailableForMigration, - MissingValidator(usize), - ValidatorPubkeyCacheError(String), - DuplicateValidatorPublicKey, - InvalidValidatorPubkeyBytes(bls::Error), - ValidatorPubkeyCacheUninitialized, InvalidKey, InvalidBytes, - UnableToDowngrade, - Hdiff(hdiff::Error), InconsistentFork(InconsistentFork), - ZeroCacheSize, + Hdiff(hdiff::Error), CacheBuildError(EpochCacheError), ForwardsIterInvalidColumn(DBColumn), ForwardsIterGap(DBColumn, Slot, Slot), StateShouldNotBeRequired(Slot), MissingBlock(Hash256), - NoAnchorInfo, RandaoMixOutOfBounds, GenesisStateUnknown, ArithError(safe_arith::ArithError), From bcbf9b830550d8a6e22c65cd516ca1591ad33886 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 11 Sep 2024 10:32:04 +1000 Subject: [PATCH 33/54] Fix new test --- beacon_node/beacon_chain/tests/store_tests.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 667c8bffc47..ddf5b101c10 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -114,14 +114,7 @@ async fn light_client_bootstrap_test() { let log = test_logger(); let seconds_per_slot = spec.seconds_per_slot; - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); + let store = get_store_generic(&db_path, StoreConfig::default(), test_spec::()); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); let num_initial_slots = E::slots_per_epoch() * 7; From cf75901ccb82e68874ce560fefd7dc840be4bf26 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 Sep 2024 08:02:33 +0200 Subject: [PATCH 34/54] Tree states archive - review comments and metrics (#6386) * Review PR comments and metrics * Comments * Add anchor metrics * drop prev comment * Update metadata.rs * Apply suggestions from code review --------- Co-authored-by: Michael Sproul --- beacon_node/beacon_chain/src/schema_change.rs | 2 + .../src/schema_change/migration_schema_v22.rs | 8 +-- beacon_node/store/src/config.rs | 1 + beacon_node/store/src/hot_cold_store.rs | 56 ++++++++++++------- beacon_node/store/src/metadata.rs | 38 +++++++++++-- beacon_node/store/src/metrics.rs | 34 +++++++++++ beacon_node/store/src/reconstruct.rs | 3 + 7 files changed, 115 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 4344ddadf9a..95049012292 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -55,6 +55,8 @@ pub fn migrate_schema( db.store_schema_version_atomically(to, ops) } (SchemaVersion(21), SchemaVersion(22)) => { + // This migration needs to sync data between hot and cold DBs. The schema version is + // bumped inside the upgrade_to_v22 fn migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log) } // Anything else is an error. diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 76ce1ca9a31..4668f1bc333 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -68,12 +68,12 @@ pub fn upgrade_to_v22( db.store_cold_state(&genesis_state_root, &genesis_state, &mut cold_ops)?; } - // Write the block roots in the new format. Similar to above, we do this separately from - // deleting the old format block roots so that this is crash safe. + // Write the block roots in the new format in a new column. Similar to above, we do this + // separately from deleting the old format block roots so that this is crash safe. let oldest_block_slot = old_anchor .as_ref() .map_or(Slot::new(0), |a| a.oldest_block_slot); - rewrite_block_roots::( + write_new_schema_block_roots::( &db, genesis_block_root, oldest_block_slot, @@ -158,7 +158,7 @@ pub fn delete_old_schema_freezer_data( Ok(()) } -pub fn rewrite_block_roots( +pub fn write_new_schema_block_roots( db: &HotColdDB, genesis_block_root: Hash256, oldest_block_slot: Slot, diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index b42aaa31dfb..40c33528d9e 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -127,6 +127,7 @@ impl StoreConfig { anchor: Option<&AnchorInfo>, ) -> Result<(), StoreConfigError> { // Allow changing the hierarchy exponents if no historic states are stored. + // anchor == None implies full archive node thus all historic states let no_historic_states_stored = anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot)); let hierarchy_config_changed = diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index c6d98ffddfa..d38876ed2ee 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -77,6 +77,9 @@ pub struct HotColdDB, Cold: ItemStore> { /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. state_cache: Mutex>, /// Cache of hierarchical diff buffers. + /// + /// This cache is never pruned. It is only populated in response to historical queries from the + /// HTTP API. hdiff_buffer_cache: Mutex>, /// Chain spec. pub(crate) spec: ChainSpec, @@ -463,6 +466,21 @@ impl, Cold: ItemStore> HotColdDB &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, hdiff_buffer_cache_byte_size as i64, ); + + if let Some(anchor_info) = self.get_anchor_info() { + metrics::set_gauge( + &metrics::STORE_BEACON_ANCHOR_SLOT, + anchor_info.anchor_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_OLDEST_BLOCK_SLOT, + anchor_info.oldest_block_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_STATE_LOWER_LIMIT, + anchor_info.state_lower_limit.as_u64() as i64, + ); + } } /// Store a block and update the LRU cache. @@ -1605,6 +1623,7 @@ impl, Cold: ItemStore> HotColdDB "from_slot" => from, "slot" => state.slot(), ); + // Already have persisted the state summary, don't persist anything else } StorageStrategy::Snapshot => { debug!( @@ -1688,7 +1707,10 @@ impl, Cold: ItemStore> HotColdDB ops: &mut Vec, ) -> Result<(), Error> { // Load diff base state bytes. - let (_, base_buffer) = self.load_hdiff_buffer_for_slot(from_slot, 0)?; + let (_, base_buffer) = { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME); + self.load_hdiff_buffer_for_slot(from_slot)? + }; let target_buffer = HDiffBuffer::from_state(state.clone()); let diff = { let _timer = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME); @@ -1718,7 +1740,10 @@ 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> { - let (base_slot, hdiff_buffer) = self.load_hdiff_buffer_for_slot(slot, 0)?; + let (base_slot, hdiff_buffer) = { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME); + self.load_hdiff_buffer_for_slot(slot)? + }; let base_state = hdiff_buffer.into_state(&self.spec)?; debug_assert_eq!(base_slot, base_state.slot()); @@ -1728,7 +1753,8 @@ impl, Cold: ItemStore> HotColdDB let blocks = self.load_cold_blocks(base_state.slot() + 1, slot)?; - // Include state root for base state as it is required by block processing. + // Include state root for base state as it is required by block processing to not have to + // hash the state. let state_root_iter = self.forwards_state_roots_iterator_until(base_state.slot(), slot, || { Err(Error::StateShouldNotBeRequired(slot)) @@ -1751,11 +1777,7 @@ impl, Cold: ItemStore> HotColdDB /// 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, - recursion: usize, - ) -> Result<(Slot, HDiffBuffer), Error> { + fn load_hdiff_buffer_for_slot(&self, slot: Slot) -> Result<(Slot, HDiffBuffer), Error> { if let Some(buffer) = self.hdiff_buffer_cache.lock().get(&slot) { debug!( self.log, @@ -1768,10 +1790,6 @@ impl, Cold: ItemStore> HotColdDB metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); } - // Do not time recursive calls into load_hdiff_buffer_for_slot to not double count - let _timer = (recursion == 0) - .then(|| metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME)); - // Load buffer for the previous state. // This amount of recursion (<10 levels) should be OK. let t = std::time::Instant::now(); @@ -1795,8 +1813,7 @@ impl, Cold: ItemStore> HotColdDB } // Recursive case. StorageStrategy::DiffFrom(from) => { - let (_buffer_slot, mut buffer) = - self.load_hdiff_buffer_for_slot(from, recursion + 1)?; + let (_buffer_slot, mut buffer) = self.load_hdiff_buffer_for_slot(from)?; // Load diff and apply it to buffer. let diff = self.load_hdiff_for_slot(slot)?; @@ -1816,9 +1833,7 @@ impl, Cold: ItemStore> HotColdDB Ok((slot, buffer)) } - StorageStrategy::ReplayFrom(from) => { - self.load_hdiff_buffer_for_slot(from, recursion + 1) - } + StorageStrategy::ReplayFrom(from) => self.load_hdiff_buffer_for_slot(from), } } @@ -2920,6 +2935,7 @@ pub fn migrate_database, Cold: ItemStore>( let mut epoch_boundary_blocks = HashSet::new(); let mut non_checkpoint_block_roots = HashSet::new(); + // Iterate in descending order until the current split slot let state_roots = RootsIterator::new(&store, finalized_state) .take_while(|result| match result { Ok((_, _, slot)) => { @@ -2930,7 +2946,7 @@ pub fn migrate_database, Cold: ItemStore>( }) .collect::, _>>()?; - // Iterate states in slot ascending order, as they are stored wrt previous states. + // Then, 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 @@ -2982,7 +2998,9 @@ pub fn migrate_database, Cold: ItemStore>( let mut cold_db_ops = vec![]; - // Only store the cold state if it's on a diff boundary + // Only store the cold state if it's on a diff boundary. + // Calling `store_cold_state_summary` instead of `store_cold_state` for those allows us + // to skip loading many hot states. if matches!( store.hierarchy.storage_strategy(slot)?, StorageStrategy::ReplayFrom(..) diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 9422eb16dec..081203ea6de 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -88,17 +88,47 @@ impl StoreItem for CompactionTimestamp { /// Database parameters relevant to weak subjectivity sync. #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize)] pub struct AnchorInfo { - /// The slot at which the anchor state is present and which we cannot revert. + /// The slot at which the anchor state is present and which we cannot revert. Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the finalized checkpoint block + /// + /// Immutable pub anchor_slot: Slot, - /// The slot from which historical blocks are available (>=). + /// All blocks with slots greater than or equal to this value are available in the database. + /// Additionally, the genesis block is always available. + /// + /// Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the finalized checkpoint block + /// + /// Progressively decreases during backfill sync until reaching 0. pub oldest_block_slot: Slot, /// The block root of the next block that needs to be added to fill in the history. /// /// Zero if we know all blocks back to genesis. pub oldest_block_parent: Hash256, - /// The slot from which historical states are available (>=). + /// All states with slots _greater than or equal to_ `min(split.slot, state_upper_limit)` are + /// available in the database. If `state_upper_limit` is higher than `split.slot`, states are + /// not being written to the freezer database. + /// + /// Values on start if state reconstruction is enabled: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the next scheduled snapshot + /// + /// Value on start if state reconstruction is disabled: + /// - 2^64 - 1 representing no historic state storage. + /// + /// Immutable until state reconstruction completes. pub state_upper_limit: Slot, - /// The slot before which historical states are available (<=). + /// All states with slots _less than or equal to_ this value are available in the database. + /// The minimum value is 0, indicating that the genesis state is always available. + /// + /// Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: 0 + /// + /// When full block backfill completes (`oldest_block_slot == 0`) state reconstruction starts and + /// this value will progressively increase until reaching `state_upper_limit`. pub state_lower_limit: Slot, } diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 5284b7f3b66..944e887b453 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -73,6 +73,27 @@ pub static DISK_DB_DELETE_COUNT: LazyLock> = LazyLock::new &["col"], ) }); +/* + * Anchor Info + */ +pub static STORE_BEACON_ANCHOR_SLOT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_anchor_slot", + "Current anchor info anchor_slot value", + ) +}); +pub static STORE_BEACON_OLDEST_BLOCK_SLOT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_oldest_block_slot", + "Current anchor info oldest_block_slot value", + ) +}); +pub static STORE_BEACON_STATE_LOWER_LIMIT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_state_lower_limit", + "Current anchor info state_lower_limit value", + ) +}); /* * Beacon State */ @@ -227,6 +248,13 @@ pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = La "Time taken to load an hdiff buffer", ) }); +pub static STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_load_for_store_seconds", + "Time taken to load an hdiff buffer to store another hdiff", + ) + }); pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -247,6 +275,12 @@ pub static STORE_BEACON_REPLAYED_BLOCKS: LazyLock> = LazyLock "Total count of replayed blocks", ) }); +pub static STORE_BEACON_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_reconstruction_time_seconds", + "Time taken to run a reconstruct historic states batch", + ) +}); pub static BEACON_DATA_COLUMNS_CACHE_HIT_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter( diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index cb55abe798a..856660ae8b9 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -1,5 +1,6 @@ //! Implementation of historic state reconstruction (given complete block history). use crate::hot_cold_store::{HotColdDB, HotColdDBError}; +use crate::metrics; use crate::{Error, ItemStore}; use itertools::{process_results, Itertools}; use slog::{debug, info}; @@ -38,6 +39,8 @@ where "start_slot" => anchor.state_lower_limit, ); + let _t = metrics::start_timer(&metrics::STORE_BEACON_RECONSTRUCTION_TIME); + // Iterate blocks from the state lower limit to the upper limit. let split = self.get_split_info(); let lower_limit_slot = anchor.state_lower_limit; From dbd52f343d1e8225db4c68702928275a45dce464 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 16 Sep 2024 16:04:02 +1000 Subject: [PATCH 35/54] Update beacon_node/store/src/hot_cold_store.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/store/src/hot_cold_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index d38876ed2ee..4311f234f71 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2821,7 +2821,7 @@ impl, Cold: ItemStore> HotColdDB } let delete_ops = cold_ops.len(); - // If we just deleted the the genesis state, re-store it using the current* schema. + // If we just deleted the genesis state, re-store it using the current* schema. if self.get_split_slot() > 0 { info!( self.log, From 3d90ac68239a0dfee853f2fc2d4ef28a22a701b2 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 16 Sep 2024 16:34:28 +1000 Subject: [PATCH 36/54] Clarify comment and remove anchor_slot garbage --- beacon_node/store/src/hdiff.rs | 8 ++++++-- beacon_node/store/src/hot_cold_store.rs | 6 +----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 4f508a88b78..d1f8fecb3a6 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -340,8 +340,12 @@ impl HierarchyModuli { /// 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. + /// This is the case for all diffs aside from the ones in the leaf layer. To store a diff + /// might require loading the state at the previous layer, in which case the diff for that + /// layer must already have been stored. + /// + /// In future we may be able to handle this differently (with proper transaction semantics + /// rather than LevelDB's "write batches"). 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( diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 4311f234f71..02553b5e76c 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2914,7 +2914,6 @@ pub fn migrate_database, Cold: ItemStore>( // boundary (in order for the hot state summary scheme to work). let current_split_slot = store.split.read_recursive().slot; let anchor_info = store.anchor_info.read_recursive().clone(); - let anchor_slot = anchor_info.as_ref().map(|a| a.anchor_slot); if finalized_state.slot() < current_split_slot { return Err(HotColdDBError::FreezeSlotError { @@ -2938,10 +2937,7 @@ pub fn migrate_database, Cold: ItemStore>( // Iterate in descending order until the current split slot 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) - } + Ok((_, _, slot)) => *slot >= current_split_slot, Err(_) => true, }) .collect::, _>>()?; From 1890278d900f229e4aaf57b9d324b99dd0779e6d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 19 Sep 2024 17:19:49 +1000 Subject: [PATCH 37/54] Simplify database anchor (#6397) * Simplify database anchor * Update beacon_node/store/src/reconstruct.rs * Add migration for anchor * Fix and simplify light_client store tests * Fix incompatible config test --- .../beacon_chain/src/block_verification.rs | 19 --- .../beacon_chain/src/historical_blocks.rs | 9 +- beacon_node/beacon_chain/src/migrate.rs | 2 +- .../src/schema_change/migration_schema_v22.rs | 37 ++-- beacon_node/beacon_chain/tests/store_tests.rs | 157 ++--------------- beacon_node/client/src/notifier.rs | 36 ++-- .../lighthouse_network/src/types/globals.rs | 2 +- .../src/types/sync_state.rs | 2 - .../network_beacon_processor/sync_methods.rs | 10 -- .../network/src/sync/backfill_sync/mod.rs | 94 ++++------- beacon_node/store/src/config.rs | 28 +-- beacon_node/store/src/forwards_iter.rs | 32 ++-- beacon_node/store/src/hot_cold_store.rs | 159 +++++++----------- beacon_node/store/src/metadata.rs | 31 ++++ beacon_node/store/src/reconstruct.rs | 21 ++- common/eth2/src/lighthouse.rs | 2 +- database_manager/src/lib.rs | 17 +- 17 files changed, 219 insertions(+), 439 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 55547aaa18c..960dd79a729 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -905,9 +905,6 @@ impl GossipVerifiedBlock { let block_root = get_block_header_root(block_header); - // Disallow blocks that conflict with the anchor (weak subjectivity checkpoint), if any. - check_block_against_anchor_slot(block.message(), chain)?; - // Do not gossip a block from a finalized slot. check_block_against_finalized_slot(block.message(), block_root, chain)?; @@ -1138,9 +1135,6 @@ impl SignatureVerifiedBlock { .fork_name(&chain.spec) .map_err(BlockError::InconsistentFork)?; - // Check the anchor slot before loading the parent, to avoid spurious lookups. - check_block_against_anchor_slot(block.message(), chain)?; - let (mut parent, block) = load_parent(block, chain)?; let state = cheap_state_advance_to_obtain_committees::<_, BlockError>( @@ -1775,19 +1769,6 @@ impl ExecutionPendingBlock { } } -/// Returns `Ok(())` if the block's slot is greater than the anchor block's slot (if any). -fn check_block_against_anchor_slot( - block: BeaconBlockRef<'_, T::EthSpec>, - chain: &BeaconChain, -) -> Result<(), BlockError> { - if let Some(anchor_slot) = chain.store.get_anchor_slot() { - if block.slot() <= anchor_slot { - return Err(BlockError::WeakSubjectivityConflict); - } - } - Ok(()) -} - /// Returns `Ok(())` if the block is later than the finalized slot on `chain`. /// /// Returns an error if the block is earlier or equal to the finalized slot, or there was an error diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index bb9b57a25c6..7ff09035036 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -33,8 +33,6 @@ pub enum HistoricalBlockError { InvalidSignature, /// Transitory error, caller should retry with the same blocks. ValidatorPubkeyCacheTimeout, - /// No historical sync needed. - NoAnchorInfo, /// Logic error: should never occur. IndexOutOfBounds, } @@ -62,10 +60,7 @@ impl BeaconChain { &self, mut blocks: Vec>, ) -> Result { - let anchor_info = self - .store - .get_anchor_info() - .ok_or(HistoricalBlockError::NoAnchorInfo)?; + let anchor_info = self.store.get_anchor_info(); let blob_info = self.store.get_blob_info(); let data_column_info = self.store.get_data_column_info(); @@ -263,7 +258,7 @@ impl BeaconChain { let backfill_complete = new_anchor.block_backfill_complete(self.genesis_backfill_slot); anchor_and_blob_batch.push( self.store - .compare_and_set_anchor_info(Some(anchor_info), Some(new_anchor))?, + .compare_and_set_anchor_info(anchor_info, new_anchor)?, ); self.store.hot_db.do_atomically(anchor_and_blob_batch)?; diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 529f69a4353..37a2e8917ba 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -216,7 +216,7 @@ impl, Cold: ItemStore> BackgroundMigrator( ) -> Result<(), Error> { info!(log, "Upgrading from v21 to v22"); - let old_anchor = db.get_anchor_info(); + let mut old_anchor = db.get_anchor_info(); + + // If the anchor was uninitialized in the old schema (`None`), this represents a full archive + // node. + if old_anchor == ANCHOR_UNINITIALIZED { + old_anchor = ANCHOR_FOR_ARCHIVE_NODE; + } + let split_slot = db.get_split_slot(); let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; @@ -70,9 +79,7 @@ pub fn upgrade_to_v22( // Write the block roots in the new format in a new column. Similar to above, we do this // separately from deleting the old format block roots so that this is crash safe. - let oldest_block_slot = old_anchor - .as_ref() - .map_or(Slot::new(0), |a| a.oldest_block_slot); + let oldest_block_slot = old_anchor.oldest_block_slot; write_new_schema_block_roots::( &db, genesis_block_root, @@ -90,22 +97,12 @@ pub fn upgrade_to_v22( // If we crash after commiting this change, then there will be some leftover cruft left in the // freezer database, but no corruption because all the new-format data has already been written // above. - let new_anchor = if let Some(old_anchor) = &old_anchor { - AnchorInfo { - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - ..old_anchor.clone() - } - } else { - 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), - } + let new_anchor = AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() }; - let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, Some(new_anchor))?]; + let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, new_anchor)?]; db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; // Finally, clean up the old-format data from the freezer database. diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ddf5b101c10..4a772bf5173 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -109,12 +109,8 @@ async fn light_client_bootstrap_test() { return; }; - let checkpoint_slot = Slot::new(E::slots_per_epoch() * 6); let db_path = tempdir().unwrap(); - let log = test_logger(); - - let seconds_per_slot = spec.seconds_per_slot; - let store = get_store_generic(&db_path, StoreConfig::default(), test_spec::()); + let store = get_store_generic(&db_path, StoreConfig::default(), spec.clone()); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); let num_initial_slots = E::slots_per_epoch() * 7; @@ -132,71 +128,6 @@ async fn light_client_bootstrap_test() { ) .await; - let wss_block_root = harness - .chain - .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) - .unwrap() - .unwrap(); - let wss_state_root = harness - .chain - .state_root_at_slot(checkpoint_slot) - .unwrap() - .unwrap(); - let wss_block = harness - .chain - .store - .get_full_block(&wss_block_root) - .unwrap() - .unwrap(); - let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); - let wss_state = store - .get_state(&wss_state_root, Some(checkpoint_slot)) - .unwrap() - .unwrap(); - - let kzg = spec.deneb_fork_epoch.map(|_| KZG.clone()); - - let mock = - mock_execution_layer_from_parts(&harness.spec, harness.runtime.task_executor.clone()); - - // Initialise a new beacon chain from the finalized checkpoint. - // The slot clock must be set to a time ahead of the checkpoint state. - let slot_clock = TestingSlotClock::new( - Slot::new(0), - Duration::from_secs(harness.chain.genesis_time), - Duration::from_secs(seconds_per_slot), - ); - slot_clock.set_slot(harness.get_current_slot().as_u64()); - - let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); - - let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec) - .store(store.clone()) - .custom_spec(test_spec::()) - .task_executor(harness.chain.task_executor.clone()) - .logger(log.clone()) - .weak_subjectivity_state( - wss_state, - wss_block.clone(), - wss_blobs_opt.clone(), - genesis_state, - ) - .unwrap() - .store_migrator_config(MigratorConfig::default().blocking()) - .dummy_eth1_backend() - .expect("should build dummy backend") - .slot_clock(slot_clock) - .shutdown_sender(shutdown_tx) - .chain_config(ChainConfig::default()) - .event_handler(Some(ServerSentEventHandler::new_with_capacity( - log.clone(), - 1, - ))) - .execution_layer(Some(mock.el)) - .kzg(kzg) - .build() - .expect("should build"); - let current_state = harness.get_current_state(); if ForkName::Electra == current_state.fork_name_unchecked() { @@ -204,7 +135,8 @@ async fn light_client_bootstrap_test() { return; } - let finalized_checkpoint = beacon_chain + let finalized_checkpoint = harness + .chain .canonical_head .cached_head() .finalized_checkpoint(); @@ -239,11 +171,7 @@ async fn light_client_updates_test() { }; let num_final_blocks = E::slots_per_epoch() * 2; - let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let db_path = tempdir().unwrap(); - let log = test_logger(); - - let seconds_per_slot = spec.seconds_per_slot; let store = get_store_generic(&db_path, StoreConfig::default(), test_spec::()); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); @@ -260,33 +188,6 @@ async fn light_client_updates_test() { ) .await; - let wss_block_root = harness - .chain - .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) - .unwrap() - .unwrap(); - let wss_state_root = harness - .chain - .state_root_at_slot(checkpoint_slot) - .unwrap() - .unwrap(); - let wss_block = harness - .chain - .store - .get_full_block(&wss_block_root) - .unwrap() - .unwrap(); - let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); - let wss_state = store - .get_state(&wss_state_root, Some(checkpoint_slot)) - .unwrap() - .unwrap(); - - let kzg = spec.deneb_fork_epoch.map(|_| KZG.clone()); - - let mock = - mock_execution_layer_from_parts(&harness.spec, harness.runtime.task_executor.clone()); - harness.advance_slot(); harness .extend_chain_with_light_client_data( @@ -296,46 +197,6 @@ async fn light_client_updates_test() { ) .await; - // Initialise a new beacon chain from the finalized checkpoint. - // The slot clock must be set to a time ahead of the checkpoint state. - let slot_clock = TestingSlotClock::new( - Slot::new(0), - Duration::from_secs(harness.chain.genesis_time), - Duration::from_secs(seconds_per_slot), - ); - slot_clock.set_slot(harness.get_current_slot().as_u64()); - - let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); - - let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec) - .store(store.clone()) - .custom_spec(test_spec::()) - .task_executor(harness.chain.task_executor.clone()) - .logger(log.clone()) - .weak_subjectivity_state( - wss_state, - wss_block.clone(), - wss_blobs_opt.clone(), - genesis_state, - ) - .unwrap() - .store_migrator_config(MigratorConfig::default().blocking()) - .dummy_eth1_backend() - .expect("should build dummy backend") - .slot_clock(slot_clock) - .shutdown_sender(shutdown_tx) - .chain_config(ChainConfig::default()) - .event_handler(Some(ServerSentEventHandler::new_with_capacity( - log.clone(), - 1, - ))) - .execution_layer(Some(mock.el)) - .kzg(kzg) - .build() - .expect("should build"); - - let beacon_chain = Arc::new(beacon_chain); - let current_state = harness.get_current_state(); if ForkName::Electra == current_state.fork_name_unchecked() { @@ -351,7 +212,8 @@ async fn light_client_updates_test() { // fetch a range of light client updates. right now there should only be one light client update // in the db. - let lc_updates = beacon_chain + let lc_updates = harness + .chain .get_light_client_updates(sync_period, 100) .unwrap(); @@ -371,7 +233,8 @@ async fn light_client_updates_test() { .await; // we should now have two light client updates in the db - let lc_updates = beacon_chain + let lc_updates = harness + .chain .get_light_client_updates(sync_period, 100) .unwrap(); @@ -2646,11 +2509,11 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { } // Anchor slot is still set to the slot of the checkpoint block. - assert_eq!(store.get_anchor_slot(), Some(wss_block.slot())); + assert_eq!(store.get_anchor_info().anchor_slot, wss_block.slot()); // Reconstruct states. store.clone().reconstruct_historic_states(None).unwrap(); - assert_eq!(store.get_anchor_slot(), None); + assert_eq!(store.get_anchor_info().anchor_slot, 0); } /// Test that blocks and attestations that refer to states around an unaligned split state are @@ -3489,7 +3352,7 @@ async fn prune_historic_states() { .unwrap(); // Check that anchor info is updated. - let anchor_info = store.get_anchor_info().unwrap(); + let anchor_info = store.get_anchor_info(); assert_eq!(anchor_info.state_lower_limit, 0); assert_eq!(anchor_info.state_upper_limit, STATE_UPPER_LIMIT_NO_RETAIN); diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 632188014eb..4330c45bcde 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -45,10 +45,7 @@ pub fn spawn_notifier( let mut current_sync_state = network.sync_state(); // Store info if we are required to do a backfill sync. - let original_anchor_slot = beacon_chain - .store - .get_anchor_info() - .map(|ai| ai.oldest_block_slot); + let original_oldest_block_slot = beacon_chain.store.get_anchor_info().oldest_block_slot; let interval_future = async move { // Perform pre-genesis logging. @@ -141,22 +138,17 @@ pub fn spawn_notifier( match current_sync_state { SyncState::BackFillSyncing { .. } => { // Observe backfilling sync info. - if let Some(oldest_slot) = original_anchor_slot { - if let Some(current_anchor_slot) = beacon_chain - .store - .get_anchor_info() - .map(|ai| ai.oldest_block_slot) - { - sync_distance = current_anchor_slot - .saturating_sub(beacon_chain.genesis_backfill_slot); - speedo - // For backfill sync use a fake slot which is the distance we've progressed from the starting `oldest_block_slot`. - .observe( - oldest_slot.saturating_sub(current_anchor_slot), - Instant::now(), - ); - } - } + let current_oldest_block_slot = + beacon_chain.store.get_anchor_info().oldest_block_slot; + sync_distance = current_oldest_block_slot + .saturating_sub(beacon_chain.genesis_backfill_slot); + speedo + // For backfill sync use a fake slot which is the distance we've progressed + // from the starting `original_oldest_block_slot`. + .observe( + original_oldest_block_slot.saturating_sub(current_oldest_block_slot), + Instant::now(), + ); } SyncState::SyncingFinalized { .. } | SyncState::SyncingHead { .. } @@ -213,14 +205,14 @@ pub fn spawn_notifier( "Downloading historical blocks"; "distance" => distance, "speed" => sync_speed_pretty(speed), - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_anchor_slot.unwrap_or(current_slot).saturating_sub(beacon_chain.genesis_backfill_slot))), + "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), ); } else { info!( log, "Downloading historical blocks"; "distance" => distance, - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_anchor_slot.unwrap_or(current_slot).saturating_sub(beacon_chain.genesis_backfill_slot))), + "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), ); } } else if !is_backfilling && last_backfill_log_slot.is_some() { diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index ac78e2cb01e..f739a7c952d 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -72,7 +72,7 @@ impl NetworkGlobals { peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring, log)), gossipsub_subscriptions: RwLock::new(HashSet::new()), sync_state: RwLock::new(SyncState::Stalled), - backfill_state: RwLock::new(BackFillState::NotRequired), + backfill_state: RwLock::new(BackFillState::Paused), custody_subnets, custody_columns, spec, diff --git a/beacon_node/lighthouse_network/src/types/sync_state.rs b/beacon_node/lighthouse_network/src/types/sync_state.rs index b82e63bd9c0..aa8a11e6674 100644 --- a/beacon_node/lighthouse_network/src/types/sync_state.rs +++ b/beacon_node/lighthouse_network/src/types/sync_state.rs @@ -35,8 +35,6 @@ pub enum BackFillState { Syncing, /// A backfill sync has completed. Completed, - /// A backfill sync is not required. - NotRequired, /// Too many failed attempts at backfilling. Consider it failed. Failed, } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index c21054dab50..43d3c71940b 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -656,16 +656,6 @@ impl NetworkBeaconProcessor { peer_action: None, } } - HistoricalBlockError::NoAnchorInfo => { - warn!(self.log, "Backfill not required"); - - ChainSegmentFailed { - message: String::from("no_anchor_info"), - // There is no need to do a historical sync, this is not a fault of - // the peer. - peer_action: None, - } - } HistoricalBlockError::IndexOutOfBounds => { error!( self.log, diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 946d25237bf..8031f843dce 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -163,21 +163,18 @@ impl BackFillSync { // If, for some reason a backfill has already been completed (or we've used a trusted // genesis root) then backfill has been completed. - let (state, current_start) = match beacon_chain.store.get_anchor_info() { - Some(anchor_info) => { - if anchor_info.block_backfill_complete(beacon_chain.genesis_backfill_slot) { - (BackFillState::Completed, Epoch::new(0)) - } else { - ( - BackFillState::Paused, - anchor_info - .oldest_block_slot - .epoch(T::EthSpec::slots_per_epoch()), - ) - } - } - None => (BackFillState::NotRequired, Epoch::new(0)), - }; + let anchor_info = beacon_chain.store.get_anchor_info(); + let (state, current_start) = + if anchor_info.block_backfill_complete(beacon_chain.genesis_backfill_slot) { + (BackFillState::Completed, Epoch::new(0)) + } else { + ( + BackFillState::Paused, + anchor_info + .oldest_block_slot + .epoch(T::EthSpec::slots_per_epoch()), + ) + }; let bfs = BackFillSync { batches: BTreeMap::new(), @@ -253,25 +250,13 @@ impl BackFillSync { self.set_state(BackFillState::Syncing); // Obtain a new start slot, from the beacon chain and handle possible errors. - match self.reset_start_epoch() { - Err(ResetEpochError::SyncCompleted) => { - error!(self.log, "Backfill sync completed whilst in failed status"); - self.set_state(BackFillState::Completed); - return Err(BackFillError::InvalidSyncState(String::from( - "chain completed", - ))); - } - Err(ResetEpochError::NotRequired) => { - error!( - self.log, - "Backfill sync not required whilst in failed status" - ); - self.set_state(BackFillState::NotRequired); - return Err(BackFillError::InvalidSyncState(String::from( - "backfill not required", - ))); - } - Ok(_) => {} + if let Err(e) = self.reset_start_epoch() { + let ResetEpochError::SyncCompleted = e; + error!(self.log, "Backfill sync completed whilst in failed status"); + self.set_state(BackFillState::Completed); + return Err(BackFillError::InvalidSyncState(String::from( + "chain completed", + ))); } debug!(self.log, "Resuming a failed backfill sync"; "start_epoch" => self.current_start); @@ -279,9 +264,7 @@ impl BackFillSync { // begin requesting blocks from the peer pool, until all peers are exhausted. self.request_batches(network)?; } - BackFillState::Completed | BackFillState::NotRequired => { - return Ok(SyncStart::NotSyncing) - } + BackFillState::Completed => return Ok(SyncStart::NotSyncing), } Ok(SyncStart::Syncing { @@ -313,10 +296,7 @@ impl BackFillSync { peer_id: &PeerId, network: &mut SyncNetworkContext, ) -> Result<(), BackFillError> { - if matches!( - self.state(), - BackFillState::Failed | BackFillState::NotRequired - ) { + if matches!(self.state(), BackFillState::Failed) { return Ok(()); } @@ -1142,17 +1122,14 @@ impl BackFillSync { /// This errors if the beacon chain indicates that backfill sync has already completed or is /// not required. fn reset_start_epoch(&mut self) -> Result<(), ResetEpochError> { - if let Some(anchor_info) = self.beacon_chain.store.get_anchor_info() { - if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { - Err(ResetEpochError::SyncCompleted) - } else { - self.current_start = anchor_info - .oldest_block_slot - .epoch(T::EthSpec::slots_per_epoch()); - Ok(()) - } + let anchor_info = self.beacon_chain.store.get_anchor_info(); + if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { + Err(ResetEpochError::SyncCompleted) } else { - Err(ResetEpochError::NotRequired) + self.current_start = anchor_info + .oldest_block_slot + .epoch(T::EthSpec::slots_per_epoch()); + Ok(()) } } @@ -1160,13 +1137,12 @@ impl BackFillSync { fn check_completed(&mut self) -> bool { if self.would_complete(self.current_start) { // Check that the beacon chain agrees - if let Some(anchor_info) = self.beacon_chain.store.get_anchor_info() { - // Conditions that we have completed a backfill sync - if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { - return true; - } else { - error!(self.log, "Backfill out of sync with beacon chain"); - } + let anchor_info = self.beacon_chain.store.get_anchor_info(); + // Conditions that we have completed a backfill sync + if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { + return true; + } else { + error!(self.log, "Backfill out of sync with beacon chain"); } } false @@ -1195,6 +1171,4 @@ impl BackFillSync { enum ResetEpochError { /// The chain has already completed. SyncCompleted, - /// Backfill is not required. - NotRequired, } diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 40c33528d9e..6a9f4f2a91d 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -124,12 +124,10 @@ impl StoreConfig { &self, on_disk_config: &OnDiskStoreConfig, split: &Split, - anchor: Option<&AnchorInfo>, + anchor: &AnchorInfo, ) -> Result<(), StoreConfigError> { // Allow changing the hierarchy exponents if no historic states are stored. - // anchor == None implies full archive node thus all historic states - let no_historic_states_stored = - anchor.map_or(false, |anchor| anchor.no_historic_states_stored(split.slot)); + let no_historic_states_stored = anchor.no_historic_states_stored(split.slot); let hierarchy_config_changed = if let Ok(on_disk_hierarchy_config) = on_disk_config.hierarchy_config() { *on_disk_hierarchy_config != self.hierarchy_config @@ -247,7 +245,10 @@ impl StoreItem for OnDiskStoreConfig { #[cfg(test)] mod test { use super::*; - use crate::{metadata::STATE_UPPER_LIMIT_NO_RETAIN, AnchorInfo, Split}; + use crate::{ + metadata::{ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN}, + AnchorInfo, Split, + }; use ssz::DecodeError; use types::{Hash256, Slot}; @@ -261,7 +262,7 @@ mod test { )); let split = Split::default(); assert!(store_config - .check_compatibility(&on_disk_config, &split, None) + .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) .is_ok()); } @@ -275,21 +276,22 @@ mod test { }); let split = Split::default(); assert!(store_config - .check_compatibility(&on_disk_config, &split, None) + .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) .is_ok()); } #[test] fn check_compatibility_hierarchy_config_incompatible() { - let store_config = StoreConfig { - ..Default::default() - }; + let store_config = StoreConfig::default(); let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { exponents: vec![5, 8, 11, 13, 16, 18, 21], })); - let split = Split::default(); + let split = Split { + slot: Slot::new(32), + ..Default::default() + }; assert!(store_config - .check_compatibility(&on_disk_config, &split, None) + .check_compatibility(&on_disk_config, &split, &ANCHOR_FOR_ARCHIVE_NODE) .is_err()); } @@ -310,7 +312,7 @@ mod test { state_lower_limit: Slot::new(0), }; assert!(store_config - .check_compatibility(&on_disk_config, &split, Some(&anchor)) + .check_compatibility(&on_disk_config, &split, &anchor) .is_ok()); } diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index adffc576dd8..e0f44f3affb 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -99,27 +99,19 @@ impl, Cold: ItemStore> HotColdDB fn freezer_upper_bound_for_state_roots(&self, start_slot: Slot) -> Option { let split_slot = self.get_split_slot(); - let anchor_info = self.get_anchor_info(); + let anchor = self.get_anchor_info(); - match anchor_info { - Some(anchor) => { - if start_slot <= anchor.state_lower_limit { - // Starting slot is prior to lower limit, so that's the upper limit. We can't - // iterate past the lower limit into the gap. The +1 accounts for exclusivity. - Some(anchor.state_lower_limit + 1) - } else if start_slot >= anchor.state_upper_limit { - // Starting slot is after the upper limit, so the split is the upper limit. - // The split state's root is not available in the freezer so this is exclusive. - Some(split_slot) - } else { - // In the gap, nothing is available. - None - } - } - None => { - // No anchor indicates that all state roots up to the split slot are available. - Some(split_slot) - } + if start_slot >= anchor.state_upper_limit { + // Starting slot is after the upper limit, so the split is the upper limit. + // The split state's root is not available in the freezer so this is exclusive. + Some(split_slot) + } else if start_slot <= anchor.state_lower_limit { + // Starting slot is prior to lower limit, so that's the upper limit. We can't + // iterate past the lower limit into the gap. The +1 accounts for exclusivity. + Some(anchor.state_lower_limit + 1) + } else { + // In the gap, nothing is available. + None } } } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 02553b5e76c..be42f57a610 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -7,9 +7,9 @@ use crate::leveldb_store::{BytesKey, LevelDB}; use crate::memory_store::MemoryStore; use crate::metadata::{ AnchorInfo, BlobInfo, CompactionTimestamp, DataColumnInfo, PruningCheckpoint, SchemaVersion, - ANCHOR_INFO_KEY, BLOB_INFO_KEY, COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, - DATA_COLUMN_INFO_KEY, PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, - STATE_UPPER_LIMIT_NO_RETAIN, + ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_INFO_KEY, ANCHOR_UNINITIALIZED, BLOB_INFO_KEY, + COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, DATA_COLUMN_INFO_KEY, + PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ @@ -55,7 +55,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// greater than or equal are in the hot DB. pub(crate) split: RwLock, /// The starting slots for the range of blocks & states stored in the database. - anchor_info: RwLock>, + anchor_info: RwLock, /// The starting slots for the range of blobs stored in the database. blob_info: RwLock, /// The starting slots for the range of data columns stored in the database. @@ -209,7 +209,7 @@ impl HotColdDB, MemoryStore> { let db = HotColdDB { split: RwLock::new(Split::default()), - anchor_info: RwLock::new(None), + anchor_info: RwLock::new(ANCHOR_UNINITIALIZED), blob_info: RwLock::new(BlobInfo::default()), data_column_info: RwLock::new(DataColumnInfo::default()), cold_db: MemoryStore::open(), @@ -247,14 +247,17 @@ impl HotColdDB, LevelDB> { let hierarchy = config.hierarchy_config.to_moduli()?; + let hot_db = LevelDB::open(hot_path)?; + let anchor_info = RwLock::new(Self::load_anchor_info(&hot_db)?); + let db = HotColdDB { split: RwLock::new(Split::default()), - anchor_info: RwLock::new(None), + anchor_info, blob_info: RwLock::new(BlobInfo::default()), data_column_info: RwLock::new(DataColumnInfo::default()), cold_db: LevelDB::open(cold_path)?, blobs_db: LevelDB::open(blobs_db_path)?, - hot_db: LevelDB::open(hot_path)?, + hot_db, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), hdiff_buffer_cache: Mutex::new(LruCache::new(config.hdiff_buffer_cache_size)), @@ -274,7 +277,6 @@ impl HotColdDB, LevelDB> { // because some migrations load states and depend on the split. if let Some(split) = db.load_split()? { *db.split.write() = split; - *db.anchor_info.write() = db.load_anchor_info()?; info!( db.log, @@ -370,7 +372,7 @@ impl HotColdDB, LevelDB> { let split = db.get_split_info(); let anchor = db.get_anchor_info(); db.config - .check_compatibility(&disk_config, &split, anchor.as_ref())?; + .check_compatibility(&disk_config, &split, &anchor)?; // Inform user if hierarchy config is changing. if let Ok(hierarchy_config) = disk_config.hierarchy_config() { @@ -467,20 +469,19 @@ impl, Cold: ItemStore> HotColdDB hdiff_buffer_cache_byte_size as i64, ); - if let Some(anchor_info) = self.get_anchor_info() { - metrics::set_gauge( - &metrics::STORE_BEACON_ANCHOR_SLOT, - anchor_info.anchor_slot.as_u64() as i64, - ); - metrics::set_gauge( - &metrics::STORE_BEACON_OLDEST_BLOCK_SLOT, - anchor_info.oldest_block_slot.as_u64() as i64, - ); - metrics::set_gauge( - &metrics::STORE_BEACON_STATE_LOWER_LIMIT, - anchor_info.state_lower_limit.as_u64() as i64, - ); - } + let anchor_info = self.get_anchor_info(); + metrics::set_gauge( + &metrics::STORE_BEACON_ANCHOR_SLOT, + anchor_info.anchor_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_OLDEST_BLOCK_SLOT, + anchor_info.oldest_block_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_STATE_LOWER_LIMIT, + anchor_info.state_lower_limit.as_u64() as i64, + ); } /// Store a block and update the LRU cache. @@ -2071,23 +2072,23 @@ impl, Cold: ItemStore> HotColdDB }; let anchor_info = if state_upper_limit == 0 && anchor_slot == 0 { // Genesis archive node: no anchor because we *will* store all states. - None + ANCHOR_FOR_ARCHIVE_NODE } else { - Some(AnchorInfo { + AnchorInfo { anchor_slot, oldest_block_slot: anchor_slot, oldest_block_parent: block.parent_root(), state_upper_limit, state_lower_limit: self.spec.genesis_slot, - }) + } }; - self.compare_and_set_anchor_info(None, anchor_info) + self.compare_and_set_anchor_info(ANCHOR_UNINITIALIZED, anchor_info) } /// Get a clone of the store's anchor info. /// /// To do mutations, use `compare_and_set_anchor_info`. - pub fn get_anchor_info(&self) -> Option { + pub fn get_anchor_info(&self) -> AnchorInfo { self.anchor_info.read_recursive().clone() } @@ -2100,8 +2101,8 @@ impl, Cold: ItemStore> HotColdDB /// is not correct. pub fn compare_and_set_anchor_info( &self, - prev_value: Option, - new_value: Option, + prev_value: AnchorInfo, + new_value: AnchorInfo, ) -> Result { let mut anchor_info = self.anchor_info.write(); if *anchor_info == prev_value { @@ -2116,39 +2117,26 @@ impl, Cold: ItemStore> HotColdDB /// As for `compare_and_set_anchor_info`, but also writes the anchor to disk immediately. pub fn compare_and_set_anchor_info_with_write( &self, - prev_value: Option, - new_value: Option, + prev_value: AnchorInfo, + new_value: AnchorInfo, ) -> Result<(), Error> { let kv_store_op = self.compare_and_set_anchor_info(prev_value, new_value)?; self.hot_db.do_atomically(vec![kv_store_op]) } - /// Load the anchor info from disk, but do not set `self.anchor_info`. - fn load_anchor_info(&self) -> Result, Error> { - self.hot_db.get(&ANCHOR_INFO_KEY) + /// Load the anchor info from disk. + fn load_anchor_info(hot_db: &Hot) -> Result { + Ok(hot_db + .get(&ANCHOR_INFO_KEY)? + .unwrap_or(ANCHOR_UNINITIALIZED)) } /// Store the given `anchor_info` to disk. /// /// The argument is intended to be `self.anchor_info`, but is passed manually to avoid issues /// with recursive locking. - fn store_anchor_info_in_batch(&self, anchor_info: &Option) -> KeyValueStoreOp { - if let Some(ref anchor_info) = anchor_info { - anchor_info.as_kv_store_op(ANCHOR_INFO_KEY) - } else { - KeyValueStoreOp::DeleteKey(get_key_for_col( - DBColumn::BeaconMeta.into(), - ANCHOR_INFO_KEY.as_slice(), - )) - } - } - - /// If an anchor exists, return its `anchor_slot` field. - pub fn get_anchor_slot(&self) -> Option { - self.anchor_info - .read_recursive() - .as_ref() - .map(|a| a.anchor_slot) + fn store_anchor_info_in_batch(&self, anchor_info: &AnchorInfo) -> KeyValueStoreOp { + anchor_info.as_kv_store_op(ANCHOR_INFO_KEY) } /// Initialize the `BlobInfo` when starting from genesis or a checkpoint. @@ -2296,7 +2284,7 @@ impl, Cold: ItemStore> HotColdDB /// instance. pub fn get_historic_state_limits(&self) -> (Slot, Slot) { // If checkpoint sync is used then states in the hot DB will always be available, but may - // become unavailable as finalisation advances due to the lack of a restore point in the + // become unavailable as finalisation advances due to the lack of a snapshot in the // database. For this reason we take the minimum of the split slot and the // restore-point-aligned `state_upper_limit`, which should be set _ahead_ of the checkpoint // slot during initialisation. @@ -2307,20 +2295,16 @@ impl, Cold: ItemStore> HotColdDB // a new restore point will be created at that slot, making all states from 4096 onwards // permanently available. let split_slot = self.get_split_slot(); - self.anchor_info - .read_recursive() - .as_ref() - .map_or((split_slot, self.spec.genesis_slot), |a| { - (a.state_lower_limit, min(a.state_upper_limit, split_slot)) - }) + let anchor = self.anchor_info.read_recursive(); + ( + anchor.state_lower_limit, + min(anchor.state_upper_limit, split_slot), + ) } /// Return the minimum slot such that blocks are available for all subsequent slots. pub fn get_oldest_block_slot(&self) -> Slot { - self.anchor_info - .read_recursive() - .as_ref() - .map_or(self.spec.genesis_slot, |anchor| anchor.oldest_block_slot) + self.anchor_info.read_recursive().oldest_block_slot } /// Return the in-memory configuration used by the database. @@ -2502,7 +2486,7 @@ impl, Cold: ItemStore> HotColdDB "Pruning finalized payloads"; "info" => "you may notice degraded I/O performance while this runs" ); - let anchor_slot = self.get_anchor_info().map(|info| info.anchor_slot); + let anchor_slot = self.get_anchor_info().anchor_slot; let mut ops = vec![]; let mut last_pruned_block_root = None; @@ -2543,7 +2527,7 @@ impl, Cold: ItemStore> HotColdDB ops.push(StoreOp::DeleteExecutionPayload(block_root)); } - if Some(slot) == anchor_slot { + if slot == anchor_slot { info!( self.log, "Payload pruning reached anchor state"; @@ -2650,16 +2634,15 @@ impl, Cold: ItemStore> HotColdDB } // Sanity checks. - if let Some(anchor) = self.get_anchor_info() { - if oldest_blob_slot < anchor.oldest_block_slot { - error!( - self.log, - "Oldest blob is older than oldest block"; - "oldest_blob_slot" => oldest_blob_slot, - "oldest_block_slot" => anchor.oldest_block_slot - ); - return Err(HotColdDBError::BlobPruneLogicError.into()); - } + let anchor = self.get_anchor_info(); + if oldest_blob_slot < anchor.oldest_block_slot { + error!( + self.log, + "Oldest blob is older than oldest block"; + "oldest_blob_slot" => oldest_blob_slot, + "oldest_block_slot" => anchor.oldest_block_slot + ); + return Err(HotColdDBError::BlobPruneLogicError.into()); } // Iterate block roots forwards from the oldest blob slot. @@ -2760,25 +2743,15 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { // 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() { - AnchorInfo { - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - ..old_anchor.clone() - } - } else { - 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), - } + let new_anchor = AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() }; // Commit the anchor change immediately: if the cold database ops fail they can always be // retried, and we can't do them atomically with this change anyway. - self.compare_and_set_anchor_info_with_write(old_anchor, Some(new_anchor))?; + self.compare_and_set_anchor_info_with_write(old_anchor, new_anchor)?; // Stage freezer data for deletion. Do not bother loading and deserializing values as this // wastes time and is less schema-agnostic. My hope is that this method will be useful for @@ -2983,11 +2956,7 @@ pub fn migrate_database, Cold: ItemStore>( // 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. - if slot != 0 - && anchor_info - .as_ref() - .map_or(false, |anchor| slot < anchor.state_upper_limit) - { + if slot != 0 && slot < anchor_info.state_upper_limit { debug!(store.log, "Pruning finalized state"; "slot" => slot); continue; } diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 081203ea6de..3f076a767ac 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -21,6 +21,27 @@ pub const DATA_COLUMN_INFO_KEY: Hash256 = Hash256::repeat_byte(7); /// State upper limit value used to indicate that a node is not storing historic states. pub const STATE_UPPER_LIMIT_NO_RETAIN: Slot = Slot::new(u64::MAX); +/// The `AnchorInfo` encoding full availability of all historic blocks & states. +pub const ANCHOR_FOR_ARCHIVE_NODE: AnchorInfo = AnchorInfo { + anchor_slot: Slot::new(0), + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: Slot::new(0), + state_lower_limit: Slot::new(0), +}; + +/// The `AnchorInfo` encoding an uninitialized anchor. +/// +/// This value should never exist except on initial start-up prior to the anchor being initialised +/// by `init_anchor_info`. +pub const ANCHOR_UNINITIALIZED: AnchorInfo = AnchorInfo { + anchor_slot: Slot::new(u64::MAX), + oldest_block_slot: Slot::new(u64::MAX), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: Slot::new(u64::MAX), + state_lower_limit: Slot::new(0), +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SchemaVersion(pub u64); @@ -140,10 +161,20 @@ impl AnchorInfo { self.oldest_block_slot <= target_slot } + /// Return true if all historic states are stored, i.e. if state reconstruction is complete. + pub fn all_historic_states_stored(&self) -> bool { + self.state_lower_limit == self.state_upper_limit + } + /// 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 } + + /// Return true if no historic states other than genesis *will ever be stored*. + pub fn full_state_pruning_enabled(&self) -> bool { + self.state_lower_limit == 0 && self.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN + } } impl StoreItem for AnchorInfo { diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 856660ae8b9..56ab04249c2 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -1,5 +1,6 @@ //! Implementation of historic state reconstruction (given complete block history). use crate::hot_cold_store::{HotColdDB, HotColdDBError}; +use crate::metadata::ANCHOR_FOR_ARCHIVE_NODE; use crate::metrics; use crate::{Error, ItemStore}; use itertools::{process_results, Itertools}; @@ -21,10 +22,12 @@ where self: &Arc, num_blocks: Option, ) -> Result<(), Error> { - let Some(mut anchor) = self.get_anchor_info() else { - // Nothing to do, history is complete. + let mut anchor = self.get_anchor_info(); + + // Nothing to do, history is complete. + if anchor.all_historic_states_stored() { return Ok(()); - }; + } // Check that all historic blocks are known. if anchor.oldest_block_slot != 0 { @@ -132,7 +135,7 @@ where self.cold_db.do_atomically(std::mem::take(&mut io_batch))?; // Update anchor. - let old_anchor = Some(anchor.clone()); + let old_anchor = anchor.clone(); if reconstruction_complete { // The two limits have met in the middle! We're done! @@ -146,17 +149,17 @@ where }); } - self.compare_and_set_anchor_info_with_write(old_anchor, None)?; + self.compare_and_set_anchor_info_with_write( + old_anchor, + ANCHOR_FOR_ARCHIVE_NODE, + )?; return Ok(()); } else { // The lower limit has been raised, store it. anchor.state_lower_limit = slot; - self.compare_and_set_anchor_info_with_write( - old_anchor, - Some(anchor.clone()), - )?; + self.compare_and_set_anchor_info_with_write(old_anchor, anchor.clone())?; } // If this is the end of the batch, return Ok. The caller will run another diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index e978d922450..309d8228aaf 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -361,7 +361,7 @@ pub struct DatabaseInfo { pub schema_version: u64, pub config: StoreConfig, pub split: Split, - pub anchor: Option, + pub anchor: AnchorInfo, pub blob_info: BlobInfo, } diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index a439ceb69b4..04e0df193f4 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -16,7 +16,6 @@ use slog::{info, warn, Logger}; use std::fs; use std::io::Write; use std::path::PathBuf; -use store::metadata::STATE_UPPER_LIMIT_NO_RETAIN; use store::{ errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, @@ -433,18 +432,12 @@ pub fn prune_states( // Check that the user has confirmed they want to proceed. if !prune_config.confirm { - match db.get_anchor_info() { - Some(anchor_info) - if anchor_info.state_lower_limit == 0 - && anchor_info.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN => - { - info!(log, "States have already been pruned"); - return Ok(()); - } - _ => { - info!(log, "Ready to prune states"); - } + if db.get_anchor_info().full_state_pruning_enabled() { + info!(log, "States have already been pruned"); + return Ok(()); } + + info!(log, "Ready to prune states"); warn!( log, "Pruning states is irreversible"; From b66aa9a8abc21e31cee4d22bb7bbf7290f4701a4 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 27 Sep 2024 15:53:30 +1000 Subject: [PATCH 38/54] More metrics --- beacon_node/http_api/src/lib.rs | 5 ++++- beacon_node/http_api/src/metrics.rs | 12 ++++++++++++ beacon_node/http_api/src/state_id.rs | 2 ++ beacon_node/store/src/hdiff.rs | 6 ++++-- beacon_node/store/src/hot_cold_store.rs | 22 +++++++++++++-------- beacon_node/store/src/metrics.rs | 26 +++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index ffcfda46803..dbd2f28dac8 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2711,9 +2711,12 @@ pub fn serve( let fork_name = state .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; + let timer = metrics::start_timer(&metrics::HTTP_API_STATE_SSZ_ENCODE_TIMES); + let response_bytes = state.as_ssz_bytes(); + drop(timer); Response::builder() .status(200) - .body(state.as_ssz_bytes().into()) + .body(response_bytes.into()) .map(|res: Response| add_ssz_content_type_header(res)) .map(|resp: warp::reply::Response| { add_consensus_version_header(resp, fork_name) diff --git a/beacon_node/http_api/src/metrics.rs b/beacon_node/http_api/src/metrics.rs index 970eef8dd07..ebb46d30f3e 100644 --- a/beacon_node/http_api/src/metrics.rs +++ b/beacon_node/http_api/src/metrics.rs @@ -39,3 +39,15 @@ pub static HTTP_API_BLOCK_GOSSIP_TIMES: LazyLock> = LazyLoc &["provenance"], ) }); +pub static HTTP_API_STATE_SSZ_ENCODE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "http_api_state_ssz_encode_times", + "Time to SSZ encode a BeaconState for a response", + ) +}); +pub static HTTP_API_STATE_ROOT_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "http_api_state_root_times", + "Time to load a state root for a request", + ) +}); diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index fdc99fa954e..ddacde9a3fc 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -1,3 +1,4 @@ +use crate::metrics; use crate::ExecutionOptimistic; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::StateId as CoreStateId; @@ -23,6 +24,7 @@ impl StateId { &self, chain: &BeaconChain, ) -> Result<(Hash256, ExecutionOptimistic, Finalized), warp::Rejection> { + let _t = metrics::start_timer(&metrics::HTTP_API_STATE_ROOT_TIMES); let (slot, execution_optimistic, finalized) = match &self.0 { CoreStateId::Head => { let (cached_head, execution_status) = chain diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index d1f8fecb3a6..d9fd20e5733 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -1,5 +1,5 @@ //! Hierarchical diff implementation. -use crate::{DBColumn, StoreConfig, StoreItem}; +use crate::{metrics, DBColumn, StoreConfig, StoreItem}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; @@ -114,6 +114,7 @@ pub struct CompressedU64Diff { impl HDiffBuffer { pub fn from_state(mut beacon_state: BeaconState) -> Self { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_FROM_STATE_TIME); // Set state.balances to empty list, and then serialize state as ssz let balances_list = std::mem::take(beacon_state.balances_mut()); @@ -124,6 +125,7 @@ impl HDiffBuffer { } pub fn into_state(self, spec: &ChainSpec) -> Result, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME); let mut state = BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSszState)?; *state.balances_mut() = @@ -133,7 +135,7 @@ impl HDiffBuffer { /// Byte size of this instance pub fn size(&self) -> usize { - self.state.len() + self.balances.len() + self.state.len() + self.balances.len() * std::mem::size_of::() } } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 18134f14b26..16a6bcfbe2a 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1766,14 +1766,20 @@ impl, Cold: ItemStore> HotColdDB } 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) + let bytes = { + let _t = metrics::start_timer(&metrics::BEACON_HDIFF_READ_TIMES); + self.cold_db + .get_bytes( + DBColumn::BeaconStateDiff.into(), + &slot.as_u64().to_be_bytes(), + )? + .ok_or(HotColdDBError::MissingHDiff(slot))? + }; + let hdiff = { + let _t = metrics::start_timer(&metrics::BEACON_HDIFF_DECODE_TIMES); + HDiff::from_ssz_bytes(&bytes)? + }; + Ok(hdiff) } /// Returns `HDiffBuffer` for the specified slot, or `HDiffBuffer` for the `ReplayFrom` slot if diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 944e887b453..36559d768f0 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -151,6 +151,18 @@ pub static BEACON_STATE_WRITE_BYTES: LazyLock> = LazyLock::ne "Total number of beacon state bytes written to the DB", ) }); +pub static BEACON_HDIFF_READ_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_read_seconds", + "Time required to read the hierarchical diff bytes from the database", + ) +}); +pub static BEACON_HDIFF_DECODE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_decode_seconds", + "Time required to decode hierarchical diff bytes", + ) +}); /* * Beacon Block */ @@ -269,6 +281,20 @@ pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock> = "Total count of hdiff buffer cache miss", ) }); +pub static STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_into_state_seconds", + "Time taken to recreate a BeaconState from an hdiff buffer", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_FROM_STATE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_from_state_seconds", + "Time taken to create an hdiff buffer from a BeaconState", + ) + }); pub static STORE_BEACON_REPLAYED_BLOCKS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "store_beacon_replayed_blocks_total", From ab9c275b88098bff43f9d0924dbb40a11fab7623 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 17 Oct 2024 10:19:24 +1100 Subject: [PATCH 39/54] New historic state cache (#6475) * New historic state cache * Add more metrics * State cache hit rate metrics * Fix store metrics * More logs and metrics * Fix logger * Ensure cached states have built caches :O * Replay blocks in preference to diffing * Two separate caches * Distribute cache build time to next slot * Re-plumb historic-state-cache flag * Clean up metrics * Update book * Update beacon_node/store/src/hdiff.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update beacon_node/store/src/historic_state_cache.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/metrics.rs | 2 + beacon_node/http_api/src/lib.rs | 12 +- beacon_node/src/cli.rs | 3 +- beacon_node/src/config.rs | 11 +- beacon_node/store/src/config.rs | 4 + beacon_node/store/src/hdiff.rs | 36 +++- beacon_node/store/src/historic_state_cache.rs | 92 +++++++++ beacon_node/store/src/hot_cold_store.rs | 175 ++++++++++++++---- beacon_node/store/src/lib.rs | 1 + beacon_node/store/src/metrics.rs | 58 +++++- beacon_node/store/src/reconstruct.rs | 4 +- book/src/help_bn.md | 4 +- common/lighthouse_metrics/src/lib.rs | 8 + .../update_progressive_balances_cache.rs | 4 +- consensus/state_processing/src/epoch_cache.rs | 3 + consensus/state_processing/src/metrics.rs | 14 ++ database_manager/src/lib.rs | 3 +- lighthouse/tests/beacon_node.rs | 21 ++- 18 files changed, 389 insertions(+), 66 deletions(-) create mode 100644 beacon_node/store/src/historic_state_cache.rs diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 0654e4e2881..2316a815c1e 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1992,6 +1992,8 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { .canonical_head .fork_choice_read_lock() .scrape_for_metrics(); + + beacon_chain.store.register_metrics(); } /// Scrape the given `state` assuming it's the head state, updating the `DEFAULT_REGISTRY`. diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index dbd2f28dac8..72c9b40d056 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2696,17 +2696,20 @@ pub fn serve( .and(warp::header::optional::("accept")) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) + .and(log_filter.clone()) .then( |endpoint_version: EndpointVersion, state_id: StateId, accept_header: Option, task_spawner: TaskSpawner, - chain: Arc>| { + chain: Arc>, + log: Logger| { task_spawner.blocking_response_task(Priority::P1, move || match accept_header { Some(api_types::Accept::Ssz) => { // We can ignore the optimistic status for the "fork" since it's a // specification constant that doesn't change across competing heads of the // beacon chain. + let t = std::time::Instant::now(); let (state, _execution_optimistic, _finalized) = state_id.state(&chain)?; let fork_name = state .fork_name(&chain.spec) @@ -2714,6 +2717,13 @@ pub fn serve( let timer = metrics::start_timer(&metrics::HTTP_API_STATE_SSZ_ENCODE_TIMES); let response_bytes = state.as_ssz_bytes(); drop(timer); + debug!( + log, + "HTTP state load"; + "load_time_ms" => t.elapsed().as_millis(), + "target_slot" => state.slot() + ); + Response::builder() .status(200) .body(response_bytes.into()) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 4e32784c837..38ef64eb45e 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -803,7 +803,8 @@ pub fn cli_app() -> Command { Arg::new("historic-state-cache-size") .long("historic-state-cache-size") .value_name("SIZE") - .help("This cache is currently inactive. Please use hdiff-buffer-cache-size instead.") + .help("Specifies how many states from the freezer database should be cached in \ + memory") .default_value("1") .action(ArgAction::Set) .display_order(0) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 561c67f154b..ba760d02897 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -436,15 +436,10 @@ pub fn get_config( .map_err(|_| "state-cache-size is not a valid integer".to_string())?; } - if cli_args - .get_one::("historic-state-cache-size") - .is_some() + if let Some(historic_state_cache_size) = + clap_utils::parse_optional(cli_args, "historic-state-cache-size")? { - warn!( - log, - "Historic state cache is currently disabled. \ - Please use hdiff-buffer-cache-size instead" - ); + client_config.store.historic_state_cache_size = historic_state_cache_size; } if let Some(hdiff_buffer_cache_size) = diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 6a9f4f2a91d..03beda514d1 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -15,6 +15,7 @@ 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_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1); pub const DEFAULT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); const EST_COMPRESSION_FACTOR: usize = 2; pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1; @@ -31,6 +32,8 @@ pub struct StoreConfig { pub state_cache_size: NonZeroUsize, /// Compression level for blocks, state diffs and other compressed values. pub compression_level: i32, + /// Maximum number of historic states to store in the in-memory historic state cache. + pub historic_state_cache_size: NonZeroUsize, /// Maximum number of `HDiffBuffer`s to store in memory. pub hdiff_buffer_cache_size: NonZeroUsize, /// Whether to compact the database on initialization. @@ -102,6 +105,7 @@ impl Default for StoreConfig { epochs_per_state_diff: DEFAULT_EPOCHS_PER_STATE_DIFF, block_cache_size: DEFAULT_BLOCK_CACHE_SIZE, state_cache_size: DEFAULT_STATE_CACHE_SIZE, + historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE, hdiff_buffer_cache_size: DEFAULT_HDIFF_BUFFER_CACHE_SIZE, compression_level: DEFAULT_COMPRESSION_LEVEL, compact_on_init: false, diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index d9fd20e5733..49b29de28ea 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::io::{Read, Write}; +use std::ops::RangeInclusive; use std::str::FromStr; use types::{BeaconState, ChainSpec, EthSpec, List, Slot}; use zstd::{Decoder, Encoder}; @@ -124,12 +125,12 @@ impl HDiffBuffer { HDiffBuffer { state, balances } } - pub fn into_state(self, spec: &ChainSpec) -> Result, Error> { + pub fn as_state(&self, spec: &ChainSpec) -> Result, Error> { let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME); let mut state = BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSszState)?; - *state.balances_mut() = - List::new(self.balances).map_err(|_| Error::InvalidBalancesLength)?; + *state.balances_mut() = List::try_from_iter(self.balances.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; Ok(state) } @@ -357,6 +358,35 @@ impl HierarchyModuli { } } +impl StorageStrategy { + /// For the state stored with this `StorageStrategy` at `slot`, return the range of slots which + /// should be checked for ancestor states in the historic state cache. + /// + /// The idea is that for states which need to be built by replaying blocks we should scan + /// for any viable ancestor state between their `from` slot and `slot`. If we find such a + /// state it will save us from the slow reconstruction of the `from` state using diffs. + /// + /// Similarly for `DiffFrom` and `Snapshot` states, loading the prior state and replaying 1 + /// block is often going to be faster than loading and applying diffs/snapshots, so we may as + /// well check the cache for that 1 slot prior (in case the caller is iterating sequentially). + pub fn replay_from_range( + &self, + slot: Slot, + ) -> std::iter::Map, fn(u64) -> Slot> { + match self { + Self::ReplayFrom(from) => from.as_u64()..=slot.as_u64(), + Self::Snapshot | Self::DiffFrom(_) => { + if slot > 0 { + (slot - 1).as_u64()..=slot.as_u64() + } else { + slot.as_u64()..=slot.as_u64() + } + } + } + .map(Slot::from) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/beacon_node/store/src/historic_state_cache.rs b/beacon_node/store/src/historic_state_cache.rs new file mode 100644 index 00000000000..c0e8f8346c9 --- /dev/null +++ b/beacon_node/store/src/historic_state_cache.rs @@ -0,0 +1,92 @@ +use crate::hdiff::{Error, HDiffBuffer}; +use crate::metrics; +use lru::LruCache; +use std::num::NonZeroUsize; +use types::{BeaconState, ChainSpec, EthSpec, Slot}; + +/// Holds a combination of finalized states in two formats: +/// - `hdiff_buffers`: Format close to an SSZ serialized state for rapid application of diffs on top +/// of it +/// - `states`: Deserialized states for direct use or for rapid application of blocks (replay) +/// +/// An example use: when requesting state data for consecutive slots, this cache allows the node to +/// apply diffs once on the first request, and latter just apply blocks one at a time. +#[derive(Debug)] +pub struct HistoricStateCache { + hdiff_buffers: LruCache, + states: LruCache>, +} + +#[derive(Debug, Default)] +pub struct Metrics { + pub num_hdiff: usize, + pub num_state: usize, + pub hdiff_byte_size: usize, +} + +impl HistoricStateCache { + pub fn new(hdiff_buffer_cache_size: NonZeroUsize, state_cache_size: NonZeroUsize) -> Self { + Self { + hdiff_buffers: LruCache::new(hdiff_buffer_cache_size), + states: LruCache::new(state_cache_size), + } + } + + pub fn get_hdiff_buffer(&mut self, slot: Slot) -> Option { + if let Some(buffer_ref) = self.hdiff_buffers.get(&slot) { + let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + Some(buffer_ref.clone()) + } else if let Some(state) = self.states.get(&slot) { + let buffer = HDiffBuffer::from_state(state.clone()); + let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + let cloned = buffer.clone(); + drop(_timer); + self.hdiff_buffers.put(slot, cloned); + Some(buffer) + } else { + None + } + } + + pub fn get_state( + &mut self, + slot: Slot, + spec: &ChainSpec, + ) -> Result>, Error> { + if let Some(state) = self.states.get(&slot) { + Ok(Some(state.clone())) + } else if let Some(buffer) = self.hdiff_buffers.get(&slot) { + let state = buffer.as_state(spec)?; + self.states.put(slot, state.clone()); + Ok(Some(state)) + } else { + Ok(None) + } + } + + pub fn put_state(&mut self, slot: Slot, state: BeaconState) { + self.states.put(slot, state); + } + + pub fn put_hdiff_buffer(&mut self, slot: Slot, buffer: HDiffBuffer) { + self.hdiff_buffers.put(slot, buffer); + } + + pub fn put_both(&mut self, slot: Slot, state: BeaconState, buffer: HDiffBuffer) { + self.put_state(slot, state); + self.put_hdiff_buffer(slot, buffer); + } + + pub fn metrics(&self) -> Metrics { + let hdiff_byte_size = self + .hdiff_buffers + .iter() + .map(|(_, buffer)| buffer.size()) + .sum::(); + Metrics { + num_hdiff: self.hdiff_buffers.len(), + num_state: self.states.len(), + hdiff_byte_size, + } + } +} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index d9efdad2149..f9bc31eabca 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,6 +1,7 @@ use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; +use crate::historic_state_cache::HistoricStateCache; use crate::impls::beacon_state::{get_full_state, store_full_state}; use crate::iter::{BlockRootsIterator, ParentRootBlockIterator, RootsIterator}; use crate::leveldb_store::{BytesKey, LevelDB}; @@ -76,11 +77,11 @@ pub struct HotColdDB, Cold: ItemStore> { /// /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. state_cache: Mutex>, - /// Cache of hierarchical diff buffers. + /// Cache of historic states and hierarchical diff buffers. /// /// This cache is never pruned. It is only populated in response to historical queries from the /// HTTP API. - hdiff_buffer_cache: Mutex>, + historic_state_cache: Mutex>, /// Chain spec. pub(crate) spec: Arc, /// Logger. @@ -177,7 +178,6 @@ pub enum HotColdDBError { BlockReplayBeaconError(BeaconStateError), BlockReplaySlotError(SlotProcessingError), BlockReplayBlockError(BlockProcessingError), - MissingLowerLimitState(Slot), InvalidSlotsPerRestorePoint { slots_per_restore_point: u64, slots_per_historical_root: u64, @@ -217,7 +217,10 @@ impl HotColdDB, MemoryStore> { hot_db: MemoryStore::open(), block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - hdiff_buffer_cache: Mutex::new(LruCache::new(config.hdiff_buffer_cache_size)), + historic_state_cache: Mutex::new(HistoricStateCache::new( + config.hdiff_buffer_cache_size, + config.historic_state_cache_size, + )), config, hierarchy, spec, @@ -260,7 +263,10 @@ impl HotColdDB, LevelDB> { hot_db, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - hdiff_buffer_cache: Mutex::new(LruCache::new(config.hdiff_buffer_cache_size)), + historic_state_cache: Mutex::new(HistoricStateCache::new( + config.hdiff_buffer_cache_size, + config.historic_state_cache_size, + )), config, hierarchy, spec, @@ -440,13 +446,7 @@ impl, Cold: ItemStore> HotColdDB } pub fn register_metrics(&self) { - let hdiff_buffer_cache = self.hdiff_buffer_cache.lock(); - let hdiff_buffer_cache_byte_size = hdiff_buffer_cache - .iter() - .map(|(_, diff)| diff.size()) - .sum::(); - let hdiff_buffer_cache_len = hdiff_buffer_cache.len(); - drop(hdiff_buffer_cache); + let hsc_metrics = self.historic_state_cache.lock().metrics(); metrics::set_gauge( &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, @@ -460,13 +460,17 @@ impl, Cold: ItemStore> HotColdDB &metrics::STORE_BEACON_STATE_CACHE_SIZE, self.state_cache.lock().len() as i64, ); + metrics::set_gauge( + &metrics::STORE_BEACON_HISTORIC_STATE_CACHE_SIZE, + hsc_metrics.num_state as i64, + ); metrics::set_gauge( &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE, - hdiff_buffer_cache_len as i64, + hsc_metrics.num_hdiff as i64, ); metrics::set_gauge( &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, - hdiff_buffer_cache_byte_size as i64, + hsc_metrics.hdiff_byte_size as i64, ); let anchor_info = self.get_anchor_info(); @@ -1140,7 +1144,7 @@ impl, Cold: ItemStore> HotColdDB 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) + self.load_cold_state_by_slot(epoch_boundary_slot).map(Some) } None => Ok(None), } @@ -1581,6 +1585,7 @@ impl, Cold: ItemStore> HotColdDB }; let blocks = self.load_blocks_to_replay(boundary_state.slot(), slot, latest_block_root)?; + let _t = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_HOT_BLOCKS_TIME); self.replay_blocks( boundary_state, blocks, @@ -1741,7 +1746,7 @@ impl, Cold: ItemStore> HotColdDB /// Return `None` if no state with `state_root` lies in the freezer. pub fn load_cold_state(&self, state_root: &Hash256) -> Result>, Error> { match self.load_cold_state_slot(state_root)? { - Some(slot) => self.load_cold_state_by_slot(slot), + Some(slot) => self.load_cold_state_by_slot(slot).map(Some), None => Ok(None), } } @@ -1749,29 +1754,110 @@ impl, Cold: ItemStore> HotColdDB /// Load a pre-finalization state from the freezer database. /// /// Will reconstruct the state if it lies between restore points. - pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result>, Error> { - let (base_slot, hdiff_buffer) = { - let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME); - self.load_hdiff_buffer_for_slot(slot)? - }; - let base_state = hdiff_buffer.into_state(&self.spec)?; - debug_assert_eq!(base_slot, base_state.slot()); + pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result, Error> { + let storage_strategy = self.hierarchy.storage_strategy(slot)?; + + // Search for a state from this slot or a recent prior slot in the historic state cache. + let mut historic_state_cache = self.historic_state_cache.lock(); + + let cached_state = itertools::process_results( + storage_strategy + .replay_from_range(slot) + .rev() + .map(|prior_slot| historic_state_cache.get_state(prior_slot, &self.spec)), + |mut iter| iter.find_map(|cached_state| cached_state), + )?; + drop(historic_state_cache); + + if let Some(cached_state) = cached_state { + if cached_state.slot() == slot { + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_HIT); + return Ok(cached_state); + } + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_MISS); + + return self.load_cold_state_by_slot_using_replay(cached_state, slot); + } + + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_MISS); + + // Load using the diff hierarchy. For states that require replay we recurse into this + // function so that we can try to get their pre-state *as a state* rather than an hdiff + // buffer. + match self.hierarchy.storage_strategy(slot)? { + StorageStrategy::Snapshot | StorageStrategy::DiffFrom(_) => { + let buffer_timer = + metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME); + let (_, buffer) = self.load_hdiff_buffer_for_slot(slot)?; + drop(buffer_timer); + let state = buffer.as_state(&self.spec)?; + + self.historic_state_cache + .lock() + .put_both(slot, state.clone(), buffer); + Ok(state) + } + StorageStrategy::ReplayFrom(from) => { + let base_state = if let Some(state) = cached_state { + // Found a prior state in the historic state cache. + state + } else { + // No prior state found, need to load by diffing. + self.load_cold_state_by_slot(from)? + }; + self.load_cold_state_by_slot_using_replay(base_state, slot) + } + } + } + + fn load_cold_state_by_slot_using_replay( + &self, + mut base_state: BeaconState, + slot: Slot, + ) -> Result, Error> { + if !base_state.all_caches_built() { + // Build all caches and update the historic state cache so that these caches may be used + // at future slots. We do this lazily here rather than when populating the cache in + // order to speed up queries at snapshot/diff slots, which are already slow. + let cache_timer = + metrics::start_timer(&metrics::STORE_BEACON_COLD_BUILD_BEACON_CACHES_TIME); + base_state.build_all_caches(&self.spec)?; + debug!( + self.log, + "Built caches for historic state"; + "target_slot" => slot, + "build_time_ms" => metrics::stop_timer_with_duration(cache_timer).as_millis() + ); + self.historic_state_cache + .lock() + .put_state(base_state.slot(), base_state.clone()); + } if base_state.slot() == slot { - return Ok(Some(base_state)); + return Ok(base_state); } let blocks = self.load_cold_blocks(base_state.slot() + 1, slot)?; - // Include state root for base state as it is required by block processing to not have to - // hash the state. + // Include state root for base state as it is required by block processing to not + // have to hash the state. + let replay_timer = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_COLD_BLOCKS_TIME); let state_root_iter = self.forwards_state_roots_iterator_until(base_state.slot(), slot, || { Err(Error::StateShouldNotBeRequired(slot)) })?; + let state = self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None)?; + debug!( + self.log, + "Replayed blocks for historic state"; + "target_slot" => slot, + "replay_time_ms" => metrics::stop_timer_with_duration(replay_timer).as_millis() + ); - self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None) - .map(Some) + self.historic_state_cache + .lock() + .put_state(slot, state.clone()); + Ok(state) } fn load_hdiff_for_slot(&self, slot: Slot) -> Result { @@ -1794,17 +1880,16 @@ impl, Cold: ItemStore> HotColdDB /// 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.hdiff_buffer_cache.lock().get(&slot) { + if let Some(buffer) = self.historic_state_cache.lock().get_hdiff_buffer(slot) { debug!( self.log, - "Hit diff buffer cache"; + "Hit hdiff buffer cache"; "slot" => slot ); metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT); - return Ok((slot, buffer.clone())); - } else { - metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); + return Ok((slot, buffer)); } + metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); // Load buffer for the previous state. // This amount of recursion (<10 levels) should be OK. @@ -1815,13 +1900,17 @@ impl, Cold: ItemStore> HotColdDB let state = self .load_cold_state_as_snapshot(slot)? .ok_or(Error::MissingSnapshot(slot))?; - let buffer = HDiffBuffer::from_state(state); + let buffer = HDiffBuffer::from_state(state.clone()); - self.hdiff_buffer_cache.lock().put(slot, buffer.clone()); + self.historic_state_cache + .lock() + .put_both(slot, state, buffer.clone()); + + let load_time_ms = t.elapsed().as_millis(); debug!( self.log, - "Added diff buffer to cache"; - "load_time_ms" => t.elapsed().as_millis(), + "Cached state and hdiff buffer"; + "load_time_ms" => load_time_ms, "slot" => slot ); @@ -1839,11 +1928,15 @@ impl, Cold: ItemStore> HotColdDB diff.apply(&mut buffer, &self.config)?; } - self.hdiff_buffer_cache.lock().put(slot, buffer.clone()); + self.historic_state_cache + .lock() + .put_hdiff_buffer(slot, buffer.clone()); + + let load_time_ms = t.elapsed().as_millis(); debug!( self.log, - "Added diff buffer to cache"; - "load_time_ms" => t.elapsed().as_millis(), + "Cached hdiff buffer"; + "load_time_ms" => load_time_ms, "slot" => slot ); @@ -1859,6 +1952,7 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, ) -> Result>, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_COLD_BLOCKS_TIME); let block_root_iter = self.forwards_block_roots_iterator_until(start_slot, end_slot, || { Err(Error::StateShouldNotBeRequired(end_slot)) @@ -1884,6 +1978,7 @@ impl, Cold: ItemStore> HotColdDB end_slot: Slot, end_block_hash: Hash256, ) -> Result>>, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_HOT_BLOCKS_TIME); let mut blocks = ParentRootBlockIterator::new(self, end_block_hash) .map(|result| result.map(|(_, block)| block)) // Include the block at the end slot (if any), it needs to be diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index c4971eef57b..0498c7c1e2c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -15,6 +15,7 @@ pub mod errors; mod forwards_iter; mod garbage_collection; pub mod hdiff; +pub mod historic_state_cache; pub mod hot_cold_store; mod impls; mod leveldb_store; diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 36559d768f0..cab205c9811 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -163,6 +163,12 @@ pub static BEACON_HDIFF_DECODE_TIMES: LazyLock> = LazyLock::ne "Time required to decode hierarchical diff bytes", ) }); +pub static BEACON_HDIFF_BUFFER_CLONE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_buffer_clone_seconds", + "Time required to clone hierarchical diff buffer bytes", + ) +}); /* * Beacon Block */ @@ -210,20 +216,20 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_historic_state_cache_size", - "Current count of items in beacon store historic state cache", + "Current count of states in the historic state cache", ) }); pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_hdiff_buffer_cache_size", - "Current count of items in beacon store hdiff buffer cache", + "Current count of hdiff buffers in the historic state cache", ) }); pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_hdiff_buffer_cache_byte_size", - "Current byte size sum of all elements in beacon store hdiff buffer cache", + "Memory consumed by hdiff buffers in the historic state cache", ) }); pub static STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: LazyLock> = @@ -267,6 +273,20 @@ pub static STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_historic_state_cache_hit_total", + "Total count of historic state cache hits for full states", + ) + }); +pub static STORE_BEACON_HISTORIC_STATE_CACHE_MISS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_historic_state_cache_miss_total", + "Total count of historic state cache misses for full states", + ) + }); pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -301,6 +321,38 @@ pub static STORE_BEACON_REPLAYED_BLOCKS: LazyLock> = LazyLock "Total count of replayed blocks", ) }); +pub static STORE_BEACON_LOAD_COLD_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_load_cold_blocks_time", + "Time spent loading blocks to replay for historic states", + ) +}); +pub static STORE_BEACON_LOAD_HOT_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_load_hot_blocks_time", + "Time spent loading blocks to replay for hot states", + ) +}); +pub static STORE_BEACON_REPLAY_COLD_BLOCKS_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_replay_cold_blocks_time", + "Time spent replaying blocks for historic states", + ) + }); +pub static STORE_BEACON_COLD_BUILD_BEACON_CACHES_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_cold_build_beacon_caches_time", + "Time spent building caches on historic states", + ) + }); +pub static STORE_BEACON_REPLAY_HOT_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_replay_hot_blocks_time", + "Time spent replaying blocks for hot states", + ) +}); pub static STORE_BEACON_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( "store_beacon_reconstruction_time_seconds", diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 56ab04249c2..9bec83a35ca 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -59,9 +59,7 @@ where .take(num_blocks.map_or(usize::MAX, |n| n + 1)); // The state to be advanced. - let mut state = self - .load_cold_state_by_slot(lower_limit_slot)? - .ok_or(HotColdDBError::MissingLowerLimitState(lower_limit_slot))?; + let mut state = self.load_cold_state_by_slot(lower_limit_slot)?; state.build_caches(&self.spec)?; diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 3b932c106ed..6294ef612a1 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -181,8 +181,8 @@ Options: 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 - This cache is currently inactive. Please use hdiff-buffer-cache-size - instead. [default: 1] + Specifies how many states from the freezer database should be cached + in memory [default: 1] --http-address
Set the listen address for the RESTful HTTP API server. --http-allow-origin diff --git a/common/lighthouse_metrics/src/lib.rs b/common/lighthouse_metrics/src/lib.rs index 2a1e99defaf..20927952946 100644 --- a/common/lighthouse_metrics/src/lib.rs +++ b/common/lighthouse_metrics/src/lib.rs @@ -283,6 +283,14 @@ pub fn stop_timer(timer: Option) { } } +/// Stops a timer created with `start_timer(..)`. +/// +/// Return the duration that the timer was running for, or 0.0 if it was `None` due to incorrect +/// initialisation. +pub fn stop_timer_with_duration(timer: Option) -> Duration { + Duration::from_secs_f64(timer.map_or(0.0, |t| t.stop_and_record())) +} + pub fn observe_vec(vec: &Result, name: &[&str], value: f64) { if let Some(h) = get_histogram(vec, name) { h.observe(value) 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..1bd7504c826 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -1,6 +1,6 @@ /// A collection of all functions that mutates the `ProgressiveBalancesCache`. use crate::metrics::{ - PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + self, PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, }; use crate::{BlockProcessingError, EpochProcessingError}; @@ -21,6 +21,8 @@ pub fn initialize_progressive_balances_cache( return Ok(()); } + let _timer = metrics::start_timer(&metrics::BUILD_PROGRESSIVE_BALANCES_CACHE_TIME); + // Calculate the total flag balances for previous & current epoch in a single iteration. // This calculates `get_total_balance(unslashed_participating_indices(..))` for each flag in // the current and previous epoch. diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index 5af5e639fd2..dc1d79709e7 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -1,6 +1,7 @@ use crate::common::altair::BaseRewardPerIncrement; use crate::common::base::SqrtTotalActiveBalance; use crate::common::{altair, base}; +use crate::metrics; use safe_arith::SafeArith; use types::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; use types::{ @@ -138,6 +139,8 @@ pub fn initialize_epoch_cache( return Ok(()); } + let _timer = metrics::start_timer(&metrics::BUILD_EPOCH_CACHE_TIME); + let current_epoch = state.current_epoch(); let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; let decision_block_root = state diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index e6fe483776f..80c1adf329e 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -41,6 +41,20 @@ pub static PROCESS_EPOCH_TIME: LazyLock> = LazyLock::new(|| { "Time required for process_epoch", ) }); +pub static BUILD_EPOCH_CACHE_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "beacon_state_processing_epoch_cache", + "Time required to build the epoch cache", + ) +}); +pub static BUILD_PROGRESSIVE_BALANCES_CACHE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "beacon_state_processing_progressive_balances_cache", + "Time required to build the progressive balances cache", + ) + }); + /* * Participation Metrics (progressive balances) */ diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 43278f057e9..869605263f1 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -420,8 +420,7 @@ pub fn prune_states( // correct network, and that we don't end up storing the wrong genesis state. let genesis_from_db = db .load_cold_state_by_slot(Slot::new(0)) - .map_err(|e| format!("Error reading genesis state: {e:?}"))? - .ok_or("Error: genesis state missing from database. Check schema version.")?; + .map_err(|e| format!("Error reading genesis state: {e:?}"))?; if genesis_from_db.genesis_validators_root() != genesis_state.genesis_validators_root() { return Err(format!( diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 93a70a88d78..a51501e7f46 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1886,12 +1886,29 @@ fn state_cache_size_flag() { .run_with_zero_port() .with_config(|config| assert_eq!(config.store.state_cache_size, new_non_zero_usize(64))); } -// This flag is deprecated but should not cause a crash. #[test] fn historic_state_cache_size_flag() { CommandLineTest::new() .flag("historic-state-cache-size", Some("4")) - .run_with_zero_port(); + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.store.historic_state_cache_size, + new_non_zero_usize(4) + ) + }); +} +#[test] +fn historic_state_cache_size_default() { + use beacon_node::beacon_chain::store::config::DEFAULT_HISTORIC_STATE_CACHE_SIZE; + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.store.historic_state_cache_size, + DEFAULT_HISTORIC_STATE_CACHE_SIZE + ); + }); } #[test] fn hdiff_buffer_cache_size_flag() { From e87a618a29d1cce7060dee8607e5eb3b3c29eb13 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 18 Oct 2024 17:36:21 +1100 Subject: [PATCH 40/54] Update database docs --- book/src/advanced_database.md | 109 ++++++++++++++++------------ book/src/imgs/db-freezer-layout.png | Bin 0 -> 137451 bytes watch/README.md | 2 - 3 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 book/src/imgs/db-freezer-layout.png diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 345fff69815..e40deacbe65 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -7,59 +7,72 @@ the _freezer_ or _cold DB_, and the portion storing recent states as the _hot DB In both the hot and cold DBs, full `BeaconState` data structures are only stored periodically, and intermediate states are reconstructed by quickly replaying blocks on top of the nearest state. For example, to fetch a state at slot 7 the database might fetch a full state from slot 0, and replay -blocks from slots 1-7 while omitting redundant signature checks and Merkle root calculations. The -full states upon which blocks are replayed are referred to as _restore points_ in the case of the +blocks from slots 1-7 while omitting redundant signature checks and Merkle root calculations. In +the freezer DB, Lighthouse also uses hierarchical state diffs to jump larger distances (described in +more detail below). + +The full states upon which blocks are replayed are referred to as _snapshots_ in the case of the freezer DB, and _epoch boundary states_ in the case of the hot DB. The frequency at which the hot database stores full `BeaconState`s is fixed to one-state-per-epoch in order to keep loads of recent states performant. For the freezer DB, the frequency is -configurable via the `--slots-per-restore-point` CLI flag, which is the topic of the next section. - -## Freezer DB Space-time Trade-offs - -Frequent restore points use more disk space but accelerate the loading of historical states. -Conversely, infrequent restore points use much less space, but cause the loading of historical -states to slow down dramatically. A lower _slots per restore point_ value (SPRP) corresponds to more -frequent restore points, while a higher SPRP corresponds to less frequent. The table below shows -some example values. - -| Use Case | SPRP | Yearly Disk Usage*| Load Historical State | -|----------------------------|------|-------------------|-----------------------| -| Research | 32 | more than 10 TB | 155 ms | -| Enthusiast (prev. default) | 2048 | hundreds of GB | 10.2 s | -| Validator only (default) | 8192 | tens of GB | 41 s | - -*Last update: Dec 2023. - -As we can see, it's a high-stakes trade-off! The relationships to disk usage and historical state -load time are both linear – doubling SPRP halves disk usage and doubles load time. The minimum SPRP -is 32, and the maximum is 8192. - -The default value is 8192 for databases synced from scratch using Lighthouse v2.2.0 or later, or -2048 for prior versions. Please see the section on [Defaults](#defaults) below. - -The values shown in the table are approximate, calculated using a simple heuristic: each -`BeaconState` consumes around 145MB of disk space, and each block replayed takes around 5ms. The -**Yearly Disk Usage** column shows the approximate size of the freezer DB _alone_ (hot DB not included), calculated proportionally using the total freezer database disk usage. -The **Load Historical State** time is the worst-case load time for a state in the last slot -before a restore point. - -To run a full archival node with fast access to beacon states and a SPRP of 32, the disk usage will be more than 10 TB per year, which is impractical for many users. As such, users may consider running the [tree-states](https://github.com/sigp/lighthouse/releases/tag/v5.0.111-exp) release, which only uses less than 200 GB for a full archival node. The caveat is that it is currently experimental and in alpha release (as of Dec 2023), thus not recommended for running mainnet validators. Nevertheless, it is suitable to be used for analysis purposes, and if you encounter any issues in tree-states, we do appreciate any feedback. We plan to have a stable release of tree-states in 1H 2024. - -### Defaults - -As of Lighthouse v2.2.0, the default slots-per-restore-point value has been increased from 2048 -to 8192 in order to conserve disk space. Existing nodes will continue to use SPRP=2048 unless -re-synced. Note that it is currently not possible to change the SPRP without re-syncing, although -fast re-syncing may be achieved with [Checkpoint Sync](./checkpoint-sync.md). +configurable via the `--hierarchy-exponents` CLI flag, which is the topic of the next section. + +## Hierarchical State Diffs + +Since v6.0.0, Lighthouse's freezer database uses _hierarchical state diffs_ or _hdiffs_ for short. +These diffs allow Lighthouse to reconstruct any historic state relatively quickly from a very +compact database. The essence of the hdiffs is that full states (snapshots) are stored only around +once per year. To reconstruct a particular state, Lighthouse fetches the last snapshot prior to that +state, and then applies several _layers_ of diffs. For example, to access a state from November +2022, we might fetch the yearly snapshot for the start of 2022, then apply a monthly diff to jump to +November, and then further more granular diffs to reach the particular week, day and epoch desired. +Usually for the last stretch between the start of the epoch and the state requested, some blocks +will be _replayed_. + +The following diagram shows part of the layout of diffs in the default configuration. There is a +full snapshot stored every `2^21` slots. In the next layer there are diffs every `2^18` slots which +approximately correspond to "monthly" diffs. Following this are more granular diffs every `2^16` +slots, every `2^13` slots, and so on down to the per-epoch diffs every `2^5` slots. + +<-- FIXME(sproul): update this diagram with Lion's sauce --> + +![Tree diagram displaying hierarchical state diffs](./imgs/db-freezer-layout.png) + +The number of layers and frequency of diffs is configurable via the `--hierarchy-exponents` flag, +which has a default value of `5,9,11,13,16,18,21`. The hierarchy exponents must be provided in order +from smallest to largest. The smallest exponent determines the frequency of the "closest" layer +of diffs, with the default value of 5 corresponding to a diff every `2^5` slots (every epoch). +The largest number determines the frequency of full snapshots, with the default value of 21 +corresponding to a snapshot every `2^21` slots. + +The number of possible `--hierarchy-exponents` configurations is extremely large and our exploration +of possible configurations is still in its relative infancy. If you experiment with non-default +values of `--hierarchy-exponents` we would be interested to hear how it goes. A few rules of thumb +that we have observed are: + +- **More frequent snapshots = more space**. This is quite intuitive - if you store full states more + often then these will take up more space than diffs. However what you lose in space efficiency you + may gain in speed. It would be possible to achieve a configuration similar to Lighthouse's + previous `--slots-per-restore-point 32` using `--hierarchy-exponents 5`, although this would use + _a lot_ of space. It's even possible to push beyond that with `--hierarchy-exponents 0` which + would store a full state every single slot (NOT RECOMMENDED). +- **Less diff layers are not necessarily faster**. One might expect that the fewer diff layers there + are, the less work Lighthouse would have to do to reconstruct any particular state. In practise + this seems to be offset by the increased size of diffs in each layer making the diffs take longer + to apply. We observed no significant performance benefit from `--hierarchy-exponents 5,7,11`, and + a substantial increase in space consumed. + +If in doubt, we recommend running with the default configuration! It takes a long time to +reconstruct states in any given configuration, so it might be some time before the optimal +configuration is determined. ### CLI Configuration -To configure your Lighthouse node's database with a non-default SPRP, run your Beacon Node with -the `--slots-per-restore-point` flag: +To configure your Lighthouse node's database, run your beacon node with the `--hierarchy-exponents` flag: ```bash -lighthouse beacon_node --slots-per-restore-point 32 +lighthouse beacon_node --hierarchy-exponents "5,7,11" ``` ### Historic state cache @@ -72,17 +85,19 @@ The historical state cache size can be specified with the flag `--historic-state lighthouse beacon_node --historic-state-cache-size 4 ``` -> Note: This feature will cause high memory usage. +> Note: Use a large cache limit can lead to high memory usage. ## Glossary * _Freezer DB_: part of the database storing finalized states. States are stored in a sparser format, and usually less frequently than in the hot DB. * _Cold DB_: see _Freezer DB_. +* _HDiff_: hierarchical state diff. +* _Hierarchy Exponents_: configuration for hierarchical state diffs, which determines the density + of stored diffs and snapshots in the freezer DB. * _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full states are stored every epoch. -* _Restore Point_: a full `BeaconState` stored periodically in the freezer DB. -* _Slots Per Restore Point (SPRP)_: the number of slots between restore points in the freezer DB. +* _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Yearly by default. * _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states from slots less than the split slot are in the freezer, while all states with slots greater than or equal to the split slot are in the hot DB. diff --git a/book/src/imgs/db-freezer-layout.png b/book/src/imgs/db-freezer-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..826e6ebd89bc468e95b1721adf0e896011d3ac8b GIT binary patch literal 137451 zcmeFZhg(zIwmys~pdxlbK#B!Ws(^rW5CxTDp%aSqPUxY9A}T6UrS~E=gbo2hh}e)` z6KV)ZCxp;KfKa~0-sjxozQ42Y`Tl^z^MtiB*X(nwG2iiyG4t-JmKxJZwv#k8G)#{k z-hWO*!w^VAbBy*lJ&>Y;FwCK$IVEqeqVn{SiVF8rcd(7UlQj*^!*}ue38X3aqJQaa`h#-H_m?j# zg_)PyQZM#+nQf16H`PjBsB9CjXotP319S>~RAx6F&AT^u(-XTpL$}V-_*~Mz zAxsl|bEdO^`R%96l36@&PTXsJ`d)md?(zHn8K2A!K7|B2HvyW*{8u9HUZBC8{@Kr1 zpZrv%hRcHQ>J#o`N)e|kz^3C?oZsxvjw*4T`}Q2Fx^qS*IjCQQ3p?#97FH1;ET(<( z;&iyXbpJuO`!2Y>J^s}9@|T)C>Q~qV(#1gC z#|by?C@)8`xvv|R+bA(u@Nx3pzG9*BHuQCU%(;^a@+xOL-drv{>zR@j%yFiI^Bd## zwzAvF%+6n}74Hq~pl_1C>#WR4TNoPKmoUVp9m~IW;KiH??`hEqul)6qJ73A^+0BO^ zUKw1nXxUAxbA8>Nlq0A&9UWzGUZ4|C`^%u|_6?=ds7p3)LoF}uTl;RStbdoZJuwt> zG9ZgvKtacpt0cZXX6DS?ORtsJ9j~r(1n$?p&8}bB3Q=CGx0DYszn6AzF0_!FTTg+x z;H`V^rTscqMXCF3smethx0Lirn@!RB7UE)cXdr}HOEQIpjwVAfb}hn;MwEz zO0*!Eq*Ik4S*Wt?Gp4QnFOM+1Rpy;I%0MT6=h`Fe^;a!-pB~$P_4dqKSPR`xzwNBE zYi41F+r3Aue)=(5Uw8kk{dQgH?$6hQ=Z9}jx>ny`IVScp=>FX2AFsD(zwiE`!%?=?SsbFB~aM(&;RHzkXkqDgk)>=R9OQk+Mk_RH%hPK|lkapLz*JUNm6O6?Wl zmGBqhqn8$|PYfgJOr9zlJPY68atr|}Yf8z{P(l`;vJN&g1s`2LId$7j@$xWJrQn0{ zzT%#szWvctfpzRB6)P{W@(^P8S|t^ubZWG{*!5ztJ1sjuJv6yWr5x+#=C(?>in~MS zRx;%)UJofRUGYAN4CeUGM#CW+B4Ob?BzlX})Z~d3%<-mCiD2fH!s}aVjoGpSaH}S5 zxOQ1Ti?7*{pp*>`CWDU;`oF*ID4~_P5O{9Ovo@M=H+K2g^ske_8`Nt-P}I8^sMOi? zTGH6ABF&1l=-szc;z>YjE@_8mRa#4|AQG5^em z5b5%eb+;qSpV_xUxzEz)D9JsfzhJ&;=$anhtaJN9$I-m3D}qO1&3@%4oakhmIbCV) z1m{j%yvryYa@v(+>@}=_y^&^bf@A9g_wf_i*Y4cs5>*yF=l_k)oV)4a+4CXauC+d7 zi+y+e+zGJ@$x(6F1)|A6nLp7FN8Pzv5Y70I&z;@(?Z&mTm?t;i6lWO~o^yB|d5`tx z1tztr-V=7bGIT=s)O&d5jjGvj5!l><9`$*zYDQ?t=@&HGKRj=1cO0>NAt-oKy;P*2d)}jke{eZ(&*sr;uhw0 z;a=v+xZ!=C>jJ~W8?g$l8Ph87uem*>`}Dj|rq8ACMBk<7QVXH6m9g()!=INwe;@m_ z$e`$+ePW+mp1|YoIE9ax)&wgN_lwDD8Cs@~$5mZ;&3GXn6}9i rijEXR{77FC(> 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 Date: Mon, 21 Oct 2024 12:22:48 +1100 Subject: [PATCH 41/54] Update diagram --- book/src/advanced_database.md | 6 ++---- book/src/imgs/db-freezer-layout.png | Bin 137451 -> 159462 bytes 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index e40deacbe65..6ce2c1db2f2 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -26,7 +26,7 @@ compact database. The essence of the hdiffs is that full states (snapshots) are once per year. To reconstruct a particular state, Lighthouse fetches the last snapshot prior to that state, and then applies several _layers_ of diffs. For example, to access a state from November 2022, we might fetch the yearly snapshot for the start of 2022, then apply a monthly diff to jump to -November, and then further more granular diffs to reach the particular week, day and epoch desired. +November, and then more granular diffs to reach the particular week, day and epoch desired. Usually for the last stretch between the start of the epoch and the state requested, some blocks will be _replayed_. @@ -35,8 +35,6 @@ full snapshot stored every `2^21` slots. In the next layer there are diffs every approximately correspond to "monthly" diffs. Following this are more granular diffs every `2^16` slots, every `2^13` slots, and so on down to the per-epoch diffs every `2^5` slots. -<-- FIXME(sproul): update this diagram with Lion's sauce --> - ![Tree diagram displaying hierarchical state diffs](./imgs/db-freezer-layout.png) The number of layers and frequency of diffs is configurable via the `--hierarchy-exponents` flag, @@ -44,7 +42,7 @@ which has a default value of `5,9,11,13,16,18,21`. The hierarchy exponents must from smallest to largest. The smallest exponent determines the frequency of the "closest" layer of diffs, with the default value of 5 corresponding to a diff every `2^5` slots (every epoch). The largest number determines the frequency of full snapshots, with the default value of 21 -corresponding to a snapshot every `2^21` slots. +corresponding to a snapshot every `2^21` slots (every 291 days). The number of possible `--hierarchy-exponents` configurations is extremely large and our exploration of possible configurations is still in its relative infancy. If you experiment with non-default diff --git a/book/src/imgs/db-freezer-layout.png b/book/src/imgs/db-freezer-layout.png index 826e6ebd89bc468e95b1721adf0e896011d3ac8b..1870eb42674f452335c3bc0de179b9e5e6941402 100644 GIT binary patch literal 159462 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

-fUZu+Uj3O-s3Hi#u>sViM&!}23{U>hOws{0JWTZ=P#e) zkb633z9Y|ez;o9TtFDmHKD=M2pS2|Uf`dNWxCUM(jfX4NrvBXg%g!cAf)`nb2hVTV z(KB@ERsR1DBm}GCPjJSA8p+Jx;xsJN3ko`tE3fO=wi@YI|0LWlF@1K1-a}w?|4m;U8(ie(X zmqbzD!L^MuH&*rd3#ZXzjZ=49?KA?Q#nw)D>9armz#GsC4(I}{?L6r3jNiyazu!KU z%&#}ma$7hwS|+Q;OW4O6bjqsYEl z2FOJ391t>jkj3{<4WO5r8U|HQSpn+Ws&LfD8qhpncp!Ft{Z9lyut-o>D(d8}{C_c2vc`Bmtv z`6Hyyhw2h!=ApNaui(>YPjN{7u3SI)O4idv)x$1_QNmls!oniz^X!9j#u)KX{ziuH zwS#X`V?6_*sN1OZUNpP6LjH880mG~O`16EAN~{w@;Bm%V+Oo|$b6D-qLZ&(b_)^zN zUz(Hsw~LpeTszXPoo-A}su|SzSsqzG`)W>OX`(}4b*J^upp+G47JFFR)Jcqg{VZ(H zY@hhT=@i86nR(;${`#de55*LTY7Sf@I&qgT-WLTi#EBjY^Qx(hel#c9k~Otcm+o4< zz(|oAJvo8RB8cSI>IE-8?z#3Cqo(Tnc8Aj0;(Z$fH>0Q`_QE62%>@FkE0o+yuEpvX z_F2S3+YN#LTWd%Du%ND^KiIp8sy7d0oi4oFJ20$|ids3VLgTITlb)X3v2Hmofm+}rj_cV$$)==Jy5t0P1w?2?{9-29`N z_R!Znznqbb({+>m!;SF+ptkyGp16gS-H~AWBj^&{jlQ%KRda|mOO}^7nlYf?W{{km zeAVW7+1d#>Lot>7h^%?aQ#dScKN`Dr_?|c)+GQi~umCqC1&cM62VInCg}@Kav^xc% zeKGzGz-UjQ;@bB4p#y&0rGh6jPA;#(VEr$UlDpV3o^BTO2$d#^B=PI_}ID; z>*wAV+#lz$p!XPd);RP8s>_y=@vasj{I)$#7lu935pC+_k`_+shjFuH`A+2>a^ zimjxiKEx1sC3bZdOYSb+{?3;&Y@3u+u@V0oi5w#eVXgF z`nKxV8W_IYKYR+Q^~0SSG821n#OZYbgu+)sXO=$L4%NN3rNfX)5@<^$L*>Xv;)WbHyCfik zsi$F>We}5k?g->;b=cyi6}Y<|KLq~CKr;veSjdg3i5kP zTp|Q*oH$V}@LP++@t-sb^=^x!oS7!5M-J<_JZRQcrd$I zV@KNeI^O>&Akr9<6_7w<8tcXHP3E+YSF=o>oT;fAf*X(>iBv)vx(dP8E`UP=EaB6`w+b4egRHl>edV>MEi!lP5&kPDCUFni zX(ya16IU%|4+3ewePEa_F}`y_DjfrC4$V!cLu=|BFZc?k50I~?klP5?f>eQq*H@Uw zb~zgJic3(VrX`4EI^7h*dm$$~(Z)`S*kWC4%bst7& zv_g}wzSF;-t@3%I^TR-(#D0W`u7)WP`ZI%JH~Im^kTmVPBni69lA%W)W{;5503gQS zzEvo_i3BOtAC0n8`tG`TZxNyaR56AAox=j^i$e_}G%k(_0R&RD@-i-$h^>;&g9hr` z_C~9_-dAc84Fhif`k}XfplkJCjtOgk%6o_LwklXRW6`+MLqh?}ql<9TzZSiC9Cg+G z>Id^yzE;3klyMK!6+(Qwdorb-g!VEZH*dj3WBr|u3+U)Yph)$)J%Eg`y=?_Lj^NtT69xCz z!gniT(lV>-Z`a87n}h zf{?VJ3DMW`v*%74)x0I#mn0JUWkpCG{P5)gvLzkuWT}*h+#IBrT=|5#^HtxGVnDxK zsh%w_41c__%V#JWRuh}#@73pzxTlykW%AylVn5yS zJL;*k&j(ygfW7Fa!qHVAFO$(}`r~bEw(mVPLK%l|qQtJyStkGaM)nElzn`(6a2^ad zHUNa5ecflFD_UCSD_rRyh0N8=b!8~@xCRrdgMQD`B04d1e`RSt6k&O1QYf|kOBYmSa+G}*w0Uosjda{|J?V^MCX1P z?FIrKL*P_#za8?E(F4Zq^WU2@R-aB#<7z)0^$JH(=Lyh?E8X9cW&O2a+#ycy!BtB7 z8qy%`BF5(Dbui3!`A**66+P|h17;1eSYNCb<`xe5riURFW3>Iz* z>1cLHi*?u4YuC(F6&eT3f2_aD##<5HWh{BhQW_>LA&0UbxM(2qYyW4gjaxUM~ND;E9Ry&c94WtFP? z`qMubtTFg)eq@X)fjD*2BWvLHpe8UT?StuGWtxvAI8bV!I<(a83=9ppkqeHz3`)I3 zlBhsSO2d2IgNKkQQDI{cY{OECF{Jzi6lD$8we(%bOg`Q?3$12D_!NzuiP%|m&n!rl zUw*2PI8Px6Sor~vkbbNwGysn2x7HgLlN-kH3+vax#-I9?v7Ud73OIvcyhgr&~#Wg5utmM-Z!6BNM_BRYBd_H(OQ$-jkak?a{mC z2+3#!)JmucBBvf3jJ?qyf{KYkgyFyp@U9 zaq2oJiyH8;4Gaw28QISBf2eGf35_n2i&KY_Cv4mk1jSJ9nyQv-2#6%Ei9DmMnPW*m ztsU*Qn1s}b<3w#TP|V~;0iCJ09QyF1?KKSyn~W27-&19W9Pt_Z$S`k}h6t17Cs-&tLmuRSJ$-dq=Qtf?lu z2Aco)E+=Ai#+{siSg1K*b=HI`aGa zve*twm{1yNPKz)Z*sJYi05d*+AY+vX0cXSjlYpN7k1Rn3`6?Kcmti8!R>=iYb5H1h zKHM$V&%E4CDWdq><-YizQ|<5QvP8X3{5i#0u*SD^7l|gYpR5vmN-o&z!`u(h(oh&o zWV07Njax_TjH{a3V%oKt^}$=b|MGlER1>heuo$b$sW(F>bjnC~$ba(Sv}OZ$n3!)% z?p3}DSpeDNtyt_2Tr^K}de=u}@IG*Txq$IWj+i-G^iPCHIjwluU23B{)W`LATdnae z!SE4OLKYp)u9x(1pB%V`$0hHeT&^BfR8wOEN}>3C)u~qzWY|WuBe5Xom44gZfzZKl znXA3WmGTNI3meQdn0|OP@qat6|6O}n>=}S=ncW#Kk|SYh(5lKJ@B-Ky?an2{3Phx0 zPJ%0>mSL`klvcJpWvKw_#wVm(EA^FGu$6L;zph~~anDO?LvWk;qZOJPMhqtVJ{iLR zl?x0SNa&Sda9tzbo(C=uc9l?BUs8}F?M8H`p;?Bcv$f3eHi>1*^uNRJ)JkKapl;T6AQ$ipmPr{ zXL}EaGR#8KR%Cr#p8VbQ^r;>1Ljp2(TK_&dZ3rdk`=S^rtb#5+;1TLrf6ZW>(x_QlJKIab`yS!J{^8PW3!J;ln!CUSir<7*BzlYua8)vzuPY z)b@UY*pbFSXL!oI(?fn+w-i`|7$g$~o?L*E&9l%@=_B}Nqvy8WVI}vX#i_rTEmA&y zo^X+g4+|+>08Ef~@z4Xfm$wn>AxF?0Zrri;GY=8LXDYs1>ji^jq;QzJMl4}mI>XiM z>vhbh(W0FH7zelC1wNiKF3Nv{(aHP+p=FYv5xcNN{LW=$IsN=-hn2hxGsqRuWQf4{P>j{13C$aCeKm~6YzWw+b znAS@Id+&&?TqThZJ~V@o3Q`-tuzJx*IN@_(ZVp+wPxVJY_~6_D<_%bT?pZAyy+?^C zAtfcH5|D@iJniJs87wzm$u*Cu^W{6%ThgUeyt3MVPkF_j4D^G+42HHj+K{hA!l6W0 z(&@9!(%hA9mRo1~r;OZ|?c+Yxn|m%v`!4mx6V?=KH&i#&Gd>3055n!oUj^8!)j_W# zXs?JJ_o8<4^|j6!<_95LclQvfcb&(GacZlKp(#1Zb>y_vl#fL#40KLZ$g`5qFD%jl z2VcOEdK`eoL+l6Iot48Se{s7bRg!vAS5=iYI5>F6w_8G3s12!zr(@6dCX}I?DE4Y7 z4sh`6Zr_RiiAV<|KBtxf=Ie@_Ei_8rxRh7Adaipu zPwuwX*>xyEzc_e0?q_%Kc`glk1FsZt40vnC<8PM?=IhCVpKhLu!g&7^Rtz0}lx_OD zG;d*`E0pL&WM&!mG7%%)5vj_kwEIMVHPaOn#?~oM+m3KL*^J5CMJF4>BInsYL|d}6 zw9e(Nx7&%)z*0;-Zy$b!6CbJC91k49NEPr(WJr8_q?A99k^C&l$guE7pZjZ{9?!C4 z$N)U=Qt+xWE*G&p^prmsBO^fk&0==Ol|; z*?ft_FhS>iOYiNH)zHwWPyu~C<@8Y{+Ba5nM|FbzC8K~ZM8;x#f%$0_%IV-5)8=fW9x)*X1{iIZQBc6V=RZgWmK*( zX_wiBNHyLz6gn7Zk=Rb<8iK=Udb;chBb8$0){A9)gk#DwXw6g+r>^Lv4(nTVOH?NL zSn$nEZcvjWS|}P(J^_D#(jDKmksP?+wBW0?Fm!lhT61*3{x}I5ZTB#j@0)w^WIwU6l62Q8X=M z5bI{j;SrT5QxdD0jk_pJFqMHM+HI1UcWbmbTFW&q8sJQNcje;Rsh-CXdj%hF?N+du z9)%3reVIDC~xi?+f9CKR59NHtq4Q1M3Hq+Wa@ zw@W;$)@~`T9vcEe1 zNg>94r$gV5b?Dze$)DTPG1Hmf^!@wQ=#gcGRI7{07N)BH2y05htSUHxWeKVTSec&5 z(Lw*oXDvljHgF;&2hu_D-*e&?NzYYc+;K){NCMB)3Y2fEQE)Kg3X`G9UloR@K*g&XkAYB=q!-E=-k&%$ezMkwQOqNIxGrJ}BSuZ7}xN3R%jF=g(n z?XXIUF@XI-;I44}A)CJmT8af!{I%5?ro=snJrO|qP;#$?bKdrEYza5z;aCqQw;KiO zHy^QH4EjooLXY5QR;uZ!`gp}5fWsw>}K>OQq)@KIsvzXpi9)ek(>T%eVvgH zDr37YPK_r>{V3p4u~Cun5R-zq%fRc2)LOXrpHEtL2v(2&6I43(IP=b|$H!p8XCd{g zWEM&PU*EcnB`r?jJbtk^lqQy8?G-jbj<%lXjUS*P9~RJmevmSB52Nos&#(IkiTkxs z_L<@GZ7{MplnS?p-H7~cQ_#PDY_XomLrjLK<|&I8uQ*@NJ_^rmdwj>fYJ3rXRtX#l zM}5gk77S(J3&j}WCD_X~HkbJ}o&){BP9iyCiHPSwRkc&|>(Yo_7CByHrLR*K5+^IDYY||B-qC;6=kW{xuaC#!|SWq`cc1zYGMs zpFMjf12;OsY}lJk){p-A@0F6{7sZJ`24qS^!<(sM1Kb}BFSvtNm#L5shJ+RGM}T47 z=ZUovE$#%b6R+_hwUa-8{`@NNQu~j$Cgj<$y6vJmJ2xrAK&b|tlxE~SHhRJYCW-<7 zC@>9cD%_s@d9GH?5zBhMVZ)djLN>41oq*Hbb8^QGJX0uISBx2~TZWYEN8F|vsDT?d zI%m2u_kccwI3DZS^fFm4LecTWhSl{W-S0Oq?;OA#+*wriQV=_`>GR>&#km`BAvL8q zHRCE1?}mP+XDYgvgii3_V^Uiz8QtjU&7`EHDy{geO+EtICaELt?3O5;+P>ijr#vCm zV2~r!#s>=<)`I_21}iLM+dnvf8MCx)o68oSt562$PQmT-e^@{KCy~mU=Q6Xyd#>$5 zN}JuAhdZ(wWA5SIxCPi^`RD@!ub|8x(RRLNxhSP!x@lWHb7AFs|5&YanEl7|v&-S$ zwk%TAX`{@eh<K%^tQ{shjU?fgIIYJWKy`D*)Vk)7BLQglS0b*pX|dwu@jWyFx; z>S3osSRP(12aY-7u##m0s*4YdK5N8tBs=n6kG~;^UAg-}Iu%c9>?g8eVe6L=_6Lq^ zT!uZKACJLDrFta!#BOH+Ot7gD7886Pwv_$#NL=Rs*$dn@1HGokQ!i5QoCC(-HF62r zd`iEvlt$fAN^06KP&w8*Y^C03(6%S)w#GT!e(}c8T?2s^mjUVIT`Hao8Hcsg_0@-zobH}+i zF22q05hx9g?N0en6YeixGAROv5g1;YnSnu5Vc^QdX6r@=_bv9|HojxI=y5bW7UwL` zIvjik&vo}Xw-7EIF@lfPd*NDv&D)qN)+H`G9?7lzlLZ*^50cR1fM zyweJX{#^)gX&PjhZcvw?{oaSxVH=bD=5O7+e0ik2vQJ6ND(?$SZaUeRbFkDSdA|>C zUg%995vUAy^#eBp5&rG=G~eb(P6h?pf7Cj*ti+}T=)X|n#XBG~Uc?O6vtwA`$cpnd?)x8-#Cz0HB|&XS{7Bm#No5ga2dT7%dgL(g9-M9f3o)i28qjQ7XuxFko&K*8M>@O-^-KwMc9}eW8 z5BA7PdCF0DN^XOZ3t0HQ!ngvo4;s?hzdv+i=eGRG(`9!UX$2n9?P7m#9^KzXR0w6#y-n7_g({u>sHM2tz z_iZQ=2!LY0Q)k5)Y=FAD8@YqTT1VXGcJK9EJ+N)t>gqjNU*0t(xnG@o9VwggR)14X zNfjQXG$Unvf9%NA@28dL*#mbzW4e0fG1CtYHV=&Afgi=HPpc(UyWRLP|N$ea`^HC19;pf5&=YK8zYVEpnq~m0!ajoW9=Faam z0oj^pDrD*Y+U-urqlk;J)r2`YI0(CRXg7rz$d&^ON&=6WrIsBIsSoj!HDYmRzO*?2 z0u~uf*o*u#l(@eQd?tn}b-}?JhP(AyF32!$HeN9d50TJ31J|n=8b&YPKl-^Qn=6&I zU;_NiJ$R^oDH=_#v5r;Uvfg4sJBamODhD}l_%Tc%s&D35zf!|}75O1{F0No`WTU}> z?@djtS}*?dpuwabWHSQ=Rp;(@Ue#Q$;-GE10zs8zLeJhUkGq?ldyi;CdAi{ZFXcG~8g6zhe25JmtPYAl3@&6JHZi+4x@UpJF_SzI>Jojq8P8 zw99*QExvdUr_1UnVss~5?EPxq>O>QDGivZcQZ!X&;w{Zr$6gKH)e)fQc_!aQafc?Y zT2T0Ov7x-P-@BsnH|go~lvb~klOH+iMayUmWqfO!kkomx+H=6&%0ZP)E-X;Rd6q5d z8PHMrm`G=BzwO*~ySMydBX>^y*pFe~hPvAHAZPQLBW_EhM-FpEj1)O#x7^Y#Y#Yl- zZ#(_|7hJ%VFHW7>v`JW3zV?5$xUAxNBhjK)5ij4h44(TzT(4%0GE>oii4+_Z;JKPz z>!$m5G`p=Hdb;wrDHhwSoLn;uYi0EcA6UBSvW;a=P~CCU?KRbye|)x>=XZh5vqi2k z%XELg_pSArzqnIer>5ihqsE(rR_2;knA&bR&kEgcRngI3;u;$mo4;ctD% ze@eESyZNLJ-2JDo-ac%8i^-zd#wg!x<$T_URJb#{w0Jmge%9=Ya2IqJi>^G-8f8CP zn4#KS;LAI%(0{>uX&|JF>O!0PP}hqlDn>WmySeQVms%wkN8PGMbJ8c~k~6#D-s2h@ zVtubOWSQO-dTrueqw@k+#3kL%@g}F0nNz`YPQkNFazqK~ttRO$wqYa2g2C^8bByHx ziSX>wud(S-hd;ssZ8|ISekXmN$+X)2(-<|L+diHm6dqdT6j@oE-eH)X?zZ}&T0`K8 zkbK0f@@abYV*Am*f}h!*WVGpIvMOD!HYu#Dz2)3#NO3ImriQU|t28DW~jSfUM z$GexQ90gWc%|~mEqRu2HBGNQu=9RXdTFdi~laxm?X1nC`UMnw!nitmDJQO-p&DIB^ zx}gVnjzo#hC?74hP-Fh4z!h3~U|*-|N(ZUrwQp;flz| z5xN$w?9-L5vrcTw?^{;r7F|PTJncefg#~oR<=>k#joXWUbQ}9#BvgGfO0#*=eauv2 zXzzf^m&JvXwOOaz?VmfrD%$@sd)bl_)S9887A>90`t_$lwW)R$LnWpqx9OJVbz{~wv z)jbfHa|^pNCai0vzzpR3-M@f|a~KA>dUv|2>K3owi+ntnIDJOWK&#VTfb|dEVA(PC zzUOsCiz_FY)LrFUCeOGgQjDZb@3_QNZmVOV$uwsg$STqbwx0`jCF@eUop=sz4;ojRk-H6_z=IA^mv3;^no;aS`?W=-g6*(t?Walg^mo78 z)zgP~sfvfd7{_Sl+I7&+I&%llB`P(;H`K>=b)D8(eIc6w#^QADqub{SZGk66`O&+P z_uHS@_|sr1dAuo9xvD0=d%}#VXku=0wrPmBDWj@@y-CRaq#;FksYLa28>a)~DP2@H zBODPj&e?v6#rf%dqdA3xcfQ)>CnyO#3D4?dM|a3qP((Q?`01P5OIUKf+PC~TPbA*9!P-S z6(#!iGu$RW`hekdUg1O9Z|anfi<5#DVsFQZPBnixdae1{H)j^j^kWf~2dZnQuYX}f z#?zGHc2~hE%Chd=#h2+58F`~-8MAC()u);6S!W*!f4$0EUz~?|;;swcMyc-XgR1YQ z6{o^8@=R_CEj>TL^Zlz{HE)r9(@w(QJ3-X**7PfD*{yZiSg+V+ipgg zV?&k^=#@e6FH_8Q8zR;+R2K>ByPS>^X$;gni2n>X2>kabgE$!FO}IP2JNXt?POLH* z0#mIjWgZmEi19YFz|mEwmFmLi{Ws zocqEj!wkIz^VVDgsUuloY+J(24EVgK!SQssP!5;6StW1ndwpfO9>M}OayCWFO)R-$ zC2(AcTs%yU;C>fqguI`0Z_Rot8Boeql&7Qi-M=>4`HmlMmovZAMszUu{5iO>C9wU=U<*&8`=tI0 z|2Wz`SUZWt)OVVdPgw{U*q?6-PsN_YtGuLn*8aZ&tAQJdvRACbNz1x5Hhp8v6hzhs ze};X-Z|>%e!rr(sVnhGp2cC&A6YCi?gk9TIdLcCcOFMsrzHJ?^xc-vZ&AWpNYwf37 zsvotzyVK-AyQwO^jc^3blOB=&FWSr4-`&C8@cw1W4E>o}rLfa^(w=4w2=s_q!Ww8& zikfk;{!QGPm!K;3jRJa( z{3Fh}w#K7f;046n6@<`E_TMXcsdFx&<@dNjL&vM>*t{%k2@;2^5h=05o?6UVto){i z#1mrgG_D!xm>=iCUsHxi(w1-_~^9Hzo_1uRN zGScHYTa9Dw`#r6Q?IuJ)pX4O;cm=(KbT(~rpf#)^&Cx`{|9(^|)i*iL7Rv_9%|BL& z+LOoimGM2jO*@MHg8l%p_NQ;Y-tOm5DV=7Uuf+S^m~A_pZvVm*3fl#O6QH_2If7k8 zu?9FhL>7`J_(sEjej1TDcJ+kut4AyuVS=qBc;u9n#Zq0$arpct-$EFfdm&FRUcW*E zI)zVIO_Kk6(aLj~jGf`6_M=DDUZhh0d}+8Yz%_p8hlq9L4yaMTjo$8r;z1&$2dyY| z*J0D5ZSqfIn6-+>d17YL`18zY(cKAae0u61;?n#KRK60f`@R)gvHodyFdN2sr$PTY zwmcI9HR~&dZ_X#a(InY}GqE#(b$X)g#(^`*l43DXbU|g99iyY8%T)cBHg60(f-Y(X zQPoFud{n35#U-R|ZoW9UC)*YQRFX zOWPKQ4*(kMCLBJEGht#EXH(4P6F`KN^msqtSHoRk4RSI0`E4)A;(;H(b$){^1!tHt zIx#Lac7Vg85T~v>tb}8oz@LbJ(5=6B6orf6|IVU&=IHhdNQij-#9$_IMTS3?!zR3& zo&n*q$tv|?@uJfFRG{h=!e(muaVriKqp@Mw@^b>uwniF2A4)B-1K)Ed(v1N$E-fwX z-7ZDVt<|%BPXsei9+IDUvWqM6;bxl+#GA5^-nmMy5s&;-ju4c3m#^3HKE*nnLDiJRht^)H3XNgqt>ovUqO>TCLlt9oy?wzg60V9K-n zzeqWwPY+y0W_d0g!hwIceU4xc!4JYhVTZu6B3tj&#m@ra`8Ts&U*OTjHmK-KHTwO3 z6vH(rO|E@aUG$GMYRWTjb~L@I72UL$gcF0##6}qpJ1-e_zNhCinfMl-=KBHnLZoQ$ zoDNg8^}*MjI&AAe~Kk$u#1KfzuwcZH{sfc8)>!|nhr?0e8%%d zw4y$Ph4tuG6I%bg)IIpvSDctR<(CS&o^i^BdH6^52QMlp=JFqHk+l_H;B3fSH93<| zPYlo*VK1zl{1j`$ha0=XMI8fG(#?<;UE=t70cRrys9l?AkcZ79wh@{hf*Rmcs7vCD zr3niY9qY1fSTiAx-js}_->|w;kK2me$nEMH?y<{GeR@rMmiVu=^LrbV34lenN z9iB{EtW&IqIE>VL&JmZR7g-4@C~P^lh)mMvi^2fzm;O@WBZd^{@UB{FZQ1Gj$FR{y z-G0*=`mGs!|;p+J!=2r=J2b2YyLch0#aIZ*dWbO}hW}6PbDJ&P2cy3VeNqtnr zXj=6*GQ~!R;Y5Mo0#WZu&6FuR=&(r&2iAx}#f4uvTpK+hup2ZU})0=jpA2b@J z>K=IPT)$~H{_!7C2T%7(@FO=>4n`ll_pD8!gCpkE85(^-FZ9a1w#7X@I_IZ;t>)?B z3E_mB!$rCxM}$|9j|pcl4qjzGpeS zr%LkL(-Vy+E*fJbCx`Z{mq(C&f-&d$V_vnULBBFph%e7<>5~6{Zq1i0fe%YO1=zzI z&Sz=RD2>Dj+jYY^a*zLm>CVgLwC-td0$&hN$Xqhq z8MJR7Sq|1!RmOT~JeQ0p@ufaPN1TjxA8%nnr!cs83zSBu{wp+eq`4B4HeP+?ATLjJ z;Y8J5Q=VX}C}yAElfREMoa}sV?PV;YRe!n=g-=S9yyt!G+Z`%n@)TY<34g)eE`d+s zHJ<%t$1zVpQxzXm!GJfjy}f;FO4#$Ys{usu7`wF|m8P7}FSw9gebccs)V3629Y+0q z#Ck+Sc`vuzq7CRk+pdxKk;-@?fC1;yywEpr!=C2Me(S0mq?M8I3^lET)lu@qjGE3L zi})Xoh{nd7C~U*CVBrD;3_JMZyL{wXcnGN={AcgB->251DaCDs??Tvm2JHSaY@?9$ zY5vB$>qMmM0b9{{Kd!AGR18bn-{RP)iH+9?(P(qYJq4VA47{!9DtK!4U(%&CQd3Wf z&BRL29Ns7U)Y0$k(8f|gDr|xtM}R2{H`e0GzSSY;v#VRvuE#Nec0z*j`M;OcUZ3AL z-9b_2v+8TLW9`w=?Ma0A-*_uL)&7eIwBAv(*ED^v#`AJnR;?ji*Dd39)Sfco04 z-njk3x|@FsN^p>zSSgxphB)A@r5g*eOR%a7R}{}W@_@@n&uAN?!9k1iGi$ms*dIKP zW);3aiPhdPvb8^XfRcds-j^l&<#`!3bHLG(ji(d{PyR9sTMWLv{rtTx_vbP2TNmG5 zS)Z%l2#G6EZ~IrouB3|htl9~am8LJ`_k8qS!)RfO?(F5w$+*}sax0cHBcz*` z4ExSrh?nWAbhCSLB;yX_q_;J+J^LcOj%oGMapF+Xd`FS98tII^C#Rh)WE*US3K?)9 z{&(yrMhf3Xq<7G!YbP+LA&1fLmvAy|C9z_**|=!+W zN`SK{4o7TN{r+W1NR^A+&GE&-NA}%6L*CT+6L52hutLoh$LZ~oCh+G zT43`z8Tk?#1hM&k{1`S23`D^H?<9HKahsz-fiD21Hc8{-JhW4#JcXGbbfji=qh(&dzL2_3IcB~9$Es)a>zLt}5{NmdO51Ubzu>%FyQB^_ffGzZp6I+*XwaGtjuQ%?>CIt6jyC16JE!ic0T1Ey(JfKeSSq{@Gk zj}&M9HdSZ-!hM2rU_PGfga?yAk08V@7$P1GKluTX@JX&yn~&Ev!iP}6kg4Ik_2vk$ z1|piEo0FqWDFYK9CPNO0F7qbL<(RiB0ksm{nU$Gu1MOY^-Xm?#j$1=@QGBk8c4#u1 z8ODT%+p<$;##$6E1H0WBm=I;+UIBV{#AyQLJH@LH&h}RG)4`ZgyHum)JdifY!xt2p z=-ML4Ze{WsvcX1*r?BrBV; zs~9V=a|7$t5B6hhK-_Dl)(6x#$0m!G-T-^nYT$gAyYAd%SJ$uSl9xfx>;g(|S0-=h#Os3AU=JVgnSj#gST79P3fwsF z1JJcsz+6bh_K5$;xl4igm| z2R3oSY#DSv65hSLVt42<$Q(WL<|psuE?%~wIiW8p`A!8SJ{OyFUcPz5-Rsu4zaoHv zC%te^Gb=r98i`Mw1y<@8u?KILf|Sjj-iJp!UVXCxrDHDNJGX-_M+sGdjDqQX54&S@ zLy4JhBi;Wk)G7aBCS}K=(Zoph`faM~#B>wM`H0^WgSI+b$Er&8Q7y(DYp;pDG;jkF zOu5k*@60o_mAQTX!l~E2r(FtMwRy6?$^{w)j_;6g z#DwfOo79sJW=5Mhdv!n}2}wk@4Jqw@`{T6UQBp!Qxej7LS*=fn>MF>}t!}<$Q1OK6 z#B4`rEq$5@~RsnsQIQQ-Cd0fCZfs|FMz?=rpj+?Cle*#u|V2@@P zxKD3x(?38&AFh06={PsHKXEE#Tdj&O3-!~*o_rv`OKnNErV4{gcwm`LxKYM7W)N5X zNKk^h+Q`rNV$@(3e%=kA76qcLtyc^*L10S$koKc~#OBc#AgW`xI}nk@3fT=eq^iO| z*W53P+9F?@UZilQy^|$;n|Iy-us>pif;?LJ`D3dCSUyAnQGgAQ-CQ^*iex%rw3=+l z#vc5d`k(Oh7EYho`e`tKb9GSRN!*aBUArv0+_i0VS0?ZN{ym5mZdVo#`Nx$UTB>Vz$GekP5e(8TaZ07MQ{0%pB zO6REj8F6YIp~wv%1!2}(R0i(#S=_B3Hn@*YP!Tx54ECofL_HK=-C1l4#92{@2iM`n zpN_7#diRE92PJcW-r|Hl4Vgs?@N-ZU`nj$kbu~mI6M#;t#s`Nq*T}IbJJ7Rn80P@8 zwf8zRlkGVQMSpwua@rwcqy;z@9IJ~h#k~%*@QT3{3TGROEWt0w$tIMyx4pOE)Xuki z$Kwox*#odlIRS+ZZ8^7ikk%7m?ysKA;6HUr}lAA0n^ zq%uYt5RWZcJ4qP3+I8;dF%pY-Hdd}4XtpgHa_)*REgorm~yyE~9tl&Y$Q>d)k^8ae{!UX+YM`5SvW*9>{3;!Q2QxD!tNYLm+I4{r+odQOa`83iBAkyK6+@&u4mupm#J}}76Sqt0- z|E?U7zu7Cmsr$+pBkN)}a{3aKx+U+CeQ zW{%yUOO+iE)QUV19mq{PG;Sznwg``aVkVG?+EmaOmycr=GtJa|V0{t&^-Cb-nR8vP z!SFfHnVT8yRUQULybus5ijY`RHulM#t`laSxaB_bj$&No-Cb(hJ!fEL({zvIWGVR3 zp8E3q&PS0a3G{G9-A&w@@Ne_X0s&0Yy2D0hiMulke45YQvN>Q(adtjxeAc#bNL~(1q=XqR5E)Td^BY{3ph8vYE{5ZX8J}7F1R0ehoe8N4aAjw} zVfMLoqxN5HruOi>(KlzH;eMs@_Xb2dWSQ<^V<%TG0y|$>$A3)6V4?_(G)U8(7o%d! zQc?lj<|bNtz?hi%u*G!yt~Xd;LY(eV=Y7I`3X^iLX^b|ElDR-az8(z9vjBHaF7f-s z%14N}<`38GFhYdhOd&oEZ!3V)@OsgEQTsu!N!Ll|IqL$96hLd&6fcn@LW-9lE~L-! z;Wp3%o>~Bk#MEzHe=APE1WLjQ*;E?p$Wn3=O2kW5`{o-32mp6wid>V*zf1jq-XRP* z-R2)iR@0`E@qv#w3vvWzcS3-XZT=JnmH){jq_28^4#AQhZA&*-R7d+7iZ%*6CmAc2bXk1E>9SethX8;}>_>n68sqw36jK2oasGIeE@_ zvSAW<7SH9_4qc`>=agkq^3j}DZlQD1%@{6`d2=2zU87-04ZW}D{YTYItZhf+dE)d4 zW1*?Q$_$o=HIY3{pi7KR&;nDT0OLh-;wj}z>^g|$LlF8>_UmWYr@QrmmQM=F+qB(o z3H<{;J|iH09Jox`D3%I@9b>A!hp7$pBhJ_++Kqm%D$3qzS>wwzSC#J2(c^6rOA^Mulf<6=B&ubqmh(f#*_s!?Mrs}2PD zHuU9Fhu`pe`pNB**kjy3f^hgAVoZcWoZgmDHkW@%-GVPl<66{yZXXTqQiI4cxwe0~ zIgmg#o9`SHY%_*VGccWDqy2G*qPhbNB3k?`F%ixqyLC@nY4@MD>*Wafs+V||8Ys}; zhPX8*7}y;Pf#dQ}xVt;@N_p#UlB*_vEAk{OgVlA80qd{#G#@td2Y%T><6PZ7~v8wgJdEqw+G&z4^{g>#TN zy{|VrjN{EaEDWJ#tQ;(M!P|FN&8E58Pi_$zQV{d;q*);SyuE%YT3WX){Z| z5P5RfgjgMTbxsgviC$=1Ea-j2*^lnCdKfvbge&UTjm-GLIt>xvG-3nI>h}vEG6~d# z>_D0`w{T23k^h=nxy#6Y{oxE2AftM+AY>H{SgF%2{OtZ{fY%Vpb=GeN&2TjnlDd9 ze@Qy#W8Yll)?x!6H~5vl@>TWoeCPMg%TYEO$^V5*yfD-RgeY|aPO(p+D_Zx$HS&)| zC79YmAfBF1d+W%B)PwG!%uCC2ImLcgpX!%*5d0Z=6QMDcmeYj8HtVDof zd_>9FZ!Ef`k)di>KnEZ?3gz?9cU+Q{y(*c}Q)tijJW%TYpwK$?gX^h51v z?nVPa|6Ji=T3UlLuieeR&kRK}w`kvvTIo#H$doj$oN9v`lYP-8Og{7?iu=#_E-irc zWF-*xolCdfbpna2B7*Os?DK*UT)@ESVRFg_UhjYZj5m@H_Ok>-k8h^`bS`9>&-b@o zI7WJF`6OjIZ}n5jz0&1NA}<6oua{HwI^)Jj&N@I~DAaL}% z=SEEO78Y{1wXM%%A;G<%4=O*I%glkv_rxb`o$j|Tr{{Ofx!YpeMnFp>+pn+>)XHQK zZ=kI1El_hs?8r=4>PCz71Y50l1Lri8-1$$w2&(b3%DjB=yM33v zFMXb6UD+S8QK$MGqXI=!tvy2yhqad>A)EO8O1?(6@ESxSq%5Gv!>4mXRj>t-E)LDmP7xSiKYGIHigPgF#~om*G|DjuIdbj#s~zt!c|YrAs1J6e zAFT4_iLMTOc0HZng4i|1oPbgzq_e$!jox2a6+j0U572koY|0VKq)fe77jj*DKnrK(ul?Z~9 z!}YSdYB(1OFVx9gft1)skIh>meONL*d|C8C7OVZu<=Z#`=I`ltAUTAR2A$O|%9j@-i0#}#Oxs;bLAHsCiUoe>-2~kse^Wglc z=?l4_J~q~e*wYZ*^ui6G7ySjIuPTtGw2Q4S5AZeSdjEs3@%2N;Pg-ZYu;GxWtZ}}R z^%u^8GiqvT8NWAxdeS+lCuC28MofU*%;;UAWjF@IU;E57&smGqA5l%t*pqR|`0rf2 z+}e#+iFk@QZ@%|pbbHa7D4*E{7AH#YmEpZNmn#ItCT6`0oC<-K{)xW+Gf96q(?5dB zDpSjcIiGo}8zt7eFEBxhMZ|wD*J(s`q%AFW=tP^YXikXV zE0Lj%s3lRx$ocD$4u$LQk=z86NP^zP)dG+(tTiS3`Hm}Q2q$woX6GI?bsWha2vP7I za^)vqzpoU?%UozCZfQL>-bF|88uPZI+e(mfD45kvnz6={=G*PNNyz^_Ko{QpGP&jD zBZK_Enh;Ksy2#`E7=IyH8U5*_AdAGs9nY^d%t@pV8nE1tn(IfJ4p4zbEAwY<9DjlJ zgV?vzVAP)ilE>~{>F=!XfTra6Sgvs`jw#deA}_sS4KZXk(EieT-8?|`Un-BkGxNFo>dL93HM(o(m3a$4L8X7}hU{HXNX))GAuTyn3bZd-FvDGIN z37lSXJu=2l3b#SB!$r?B_S2zVQ+D5vydSYKpoL9I9mSX3fQy_3nNiJg>G)9wx`FQu;6Pj1gk+U%M!)i`$PcyNrbL)@Q>IQ zI@6?GYK8paH1|tI97(DJWz>3cedL(KDA_Lc$OX4-*boSi>BL7me~-F=_zWZpS+s?U zE~cs%i9$kN80BE;KS`QZ2qRf?NTg+b90v|^@-}2EuH^#Nt(Acn+59L{nihXj>uJQW zXqk-UazL2&bF$O(h-$9)XK}k$(dqPp3m7vbE_r0PT%!$$4K+14vmy0dQ8C4YY5P;_ z-pB*fOJpkOX@01d;CVayahc{$XfZ)8J6!8BdOHvdE>@6rPeAX&u)zvSp3c>v1L zSIx_n_!kCa){z5Yj!8qD!$geqOn-6DU-@PQ zKPh6nwFMW&(4jJ&lsnO|`&Uv3Q56bi^Czl6xAX|m=?0i**9E+R(vu&MXrun;v((Zq z>MANU1v{D!u9`n=IMLwN+%{RHCR^g7R7wR6r>Q$4>z$@rnO=a)M`p+wb>i!*`sPwT zHaXAptKUN@sW@O%_a8hQEeRNMkb!VObp3?Gs_B)USp78!8s*_X_tj6J)&&3X0n zsSsf$dtefvUO>C6Z(e3AJe$`kXwOi;GV9MgzKP^OoXn%#H9R&zZ7K;H{D427YmFin zQpMF@u6OUv7z5qmNAJhqDT;&f32*y(h=|l172So)gZ^QRt5DiQJ;1?jYgc_Zzjx2B zA{>Z>-{eo~JwkH|!SBWLpuU;k!%f+F^up84YDmjs@<-`go6dd+I37Xj!o{s#0qU&D zC^`qN(#mKNM^s#jj(MuF@>ZzDw~M6QGB`%|k?s{vds#q0pc3?((mVF8DF@=$Ln^ExUuTQQo%E_M z;uONM+^&xy#8nJ)d-CJZ8G#?Al#FvHOETBV38tg0PgF;<52P7WwX=dtcZi)`o~w8N zxV*Hmlf5eO-%H?rGo-Vx*ZlUQ50F_}tR-Y6Zc;?!W{T1bFjK9kNH4L;RDD?Y4G(}K zcx}U@W^qZ&l^>NmvA=jfjWP5mRn_kmvmn|MDgyDo>;*Hav z^&m&W7gvS@^{LbE$X9-sC8pkL(|N#aP&|ggCMgK_o;?UaTz4gL&5Bw#$fyeHfx&Z- z^dun;5=HTe}TfE9NY{_X({NoCLU+gQ{zQfJh>t?(K9N@UPEzP#NeNw{PDbkqdO=YeoTbu?YDJt8GYD$AZvf-;Q%N&i+sbg2(jdN_d=! zK5TIS2i(SBwrXct9zv>=PGcPx13=)(qVsJkPLJi^CWO#MS5EIUn!$$ zNikU&$mEhVdhP24CF%2fMU1Iga&X& zL*@G+98Fb7RBovw=gJf7&##N?Q*%hLc?<+N>hrX;usb`C47$1xqik;V5m`~6!_%r_n)n`TEV*}vDzV+NW?jW()Lp1!FzlQ{lxx~?O+uYHZ4-1(SM)ae5uNjSAfQ396};$I|jx5i$AI0wf~{EPA zXDfkUJKP2^EPQT@HyqhP)dJ~vHHgo2{3JKZ1Frw3=W5A*)k`Fx1kQZ;a)sRTxyPWK zrn}GO2QP2NEd+)^Nz^JWvzT2f6@a!#Nj$@<b#0-6wXFPw`x zu*(mUWn9oba`5S?WSzAO#sjmAip+()_d0q}$&TzuG)wp5pirs-D&_QppwR8br3v?J zHyf}dmhDj2|9bQ?S3?9n3SS)=r!#c2ZCjfd2R+;LUCxOwPD(p|{-IV%l;pab=QF@3 ztUEFlGHuXUTrT(lbn#vX_3Om!Ud=WXd{3%&0(B-Gr%=-Ehs=e)LHUz@I};Sm+qha` zt6SZnaO|yjUv}(jKRDI#O7H#0wt|@xYBlpwBl^Xp3!VZ$p0yy^2_%HDvvJ#OY8oiH zTG3NFE<%3g|DNcpkL49#J;2`+hAls>U2664xSDO=_$S4SpiuN9+H2OfXKWBQf%IS397Ik zwWW-}u%%zA^3N2K9&NZEce=x3i>tIJBX-}u-RpBNGO(}xzEyT_*)BA!IMv(R`z)p* zf_%L2uUmVTl}3gYXx6czXxDWaQn$gV4gvrz@r7s?P+1lH1w~fb=+;BWLBH**0Nbx8 z`A{%laY@uD_7lQF$8Y?>D62rR?eU%V!XUKH}l3Z z83Gz4Ox&65MD`FWzeU>f(qVMSzgqeN-kgkChTrtb&kSPMy2{Pgm> ziz|OhH5U@@KeVSs8iugzo02QvuJo~{Sn|`9*!!M5+s&u)AXg~182Pp-KjYSR6@uBt z`vSlE-=lcaZT?g2yk)BI-or}K=Cx8ioRiaO?s|{imz{?=K&#feMhgKvtgV^~y$ z<|T0IKY_wlIhYtE%Gd>`(~O2_r!UE7&-4*W#_ol$vp)vSCyjwV8jV$Yx+>Hdf2fJ} z`PAJq6uj=&3n|L1P&wZ2g*m70QafttG{*qhOf4?$x7HT5ZdX9)vZGcZDdw6Xp79)S zTcC`Y`!OD$c2x+sn~ZfT%;trzGVpFVW!3*IIydu>UQmC-xDLHbEeK=s@;B9`I% zyR&!df(-??+YZ-|g0^xTr*2NecXt()po>U&GOTbh@zg>EsLLK1igpZ`J2O}pWA!Ne zyzsTmR3vIQ>wH?GlAiR z%?yP)>5`QBe=U+@4nwkELxxd0b^@}7=L;5pDP9rhzL`HGS@9d(mqzLJ13|B z+EC?vaDTgUk`vYSzDt{t+1sM}(k^@Z$DdrcGY>0TW~A!9SI3D_nkx7SK9K^d0d#a` z--9)P60f%VqxNq1iM~?KG}sW?Sd_cyGbc6b2Ex>=rjGXY4VPY>h#hC~eca}8+r>E; z4!s#qS3!Qd$+Z(yomXAc?`7!S&gvpkuBzyWmh#^y4-YeJ4TRxSE2v>JtC` z5?^f{GI<**nsv-Vcsd~&X$+V%D1RX+V4I!Q0r-5PtN z9tP&K+bvu(rZVG7$GFwiw}6l?85YRL705_Y0|jQsy+XO|dqpom64lIL5wLTzdhetS zlB^nvYgiCigu-@q@cPJo5WG^EpB^=10ARqo;dI8jRM4D5e#AmZ0bAi@3B>?x#5eUK z*XvIHKASr?7&WFQDpmndMPtBk07@Xgux|P1+)(Uk-Hw+U+!1^xcU_gt?!!4>lce|p zu(?svjRhkqd&g_XQWeb>ziSja7=%9T1LLnPNo+Q-I|LziW0KPgVlAnkfd9q^(j!bf zmoPOfz5&mdEiElS-rWD3dNp2y*oR!PNQB>qOBVIct8ePf_denm^vJN%;?U2>uo*@6 z?!$M+lA;jGgOa!w7xGa9ID3&AX=X+S6+89ZmRK~bF=2B1^)ObGpCWv;9(3ug?rd{R ziA|1nI3tsTF=KAf5{#~Xh|ngrM6{yJrGOiV&ZY%zN=OdU>76;$k(9B&CRU<*`DSa1 zR>FZh=22pPKjEOQ0wt18D4W~ibg19mIwLLZ<(iVA{VcH^KDd+D#ZSZlaH9a&8f-YK zW?*2qf5>4dRL3?5k_#Q@8cakHYPS9T^)y#51~Gt3cV*<1Srw98!3p>ky1C{l&aKPG zP|Ir;ouZIS3S+$@#Pe102EZ}jOzy34Awe{IH6t{*Ahj3Th=Ka@PNxcvT$v>w3|rG^ zM9cf!u%(8uuXRP5E8o2rN00ot)@7@yR<^fArOh7ksyWp-N%%bG0$>j|c*kO^J$M!akf52QZs) zPl*pr+lci6Q7gapmE^kBj?_au1%PfIIyoG-y(vP$iJodGiT@s_K|z=E(IX30B)kG_ zPEh@k&71=y@Eg;m2Kxq7Qxetj^*tATfcZENJ_7P*uii{#NXvueF^l6oyYz&6#a{)& zeF1N%wibqwv}o~s)f82W+?~Np^k9}2$u!1BPrc>%$`3I~{#QKv35)yI!_(h6jup;V z-vty$ZMRYbVh)FrGXqqeDx{w&Vl%DUxJLHLxEShbX))3GH5@uO<@2t+i{Hwe#kes+ zcF=0!)2PY2rNjq&x6=xT9lgQ;u+dDtsybgDizYTEVf#3}4@io#t#CQZRaal1>)nOB z?OoMT;lJCLn+2^pV{|ejB%|Amtt28?$`9P$!-~Qckh=d|n+G8B{&ut2tOqe-<3lP2 zO6S9Qik-<8NWJASrIzz%nRVrT(N#wT_CCeSQ^6QO(@HdVVa6#6--o5`JBFMy>J@Js zr1^UOF?KTG_^GwoSzLQUWgl;hh@)v4CBvO-AFSjk^KaJ)OhO{MJ^K?ADa1ymd$TQD zK1MPn00MGm7Noi!qtx`woAxX9(S~&kOJ|+r{LPDLC8nK^S0#vp-g3rW1CX%e1CzR- z-e%yQpQ4}NAzt?(z(rw~jn~@|R8nU#{oXLcwzJH>{%JyW3c|_mqsUKl6oY;QSuftY zb6bwSzdmv8F=&h`lY*YhwIs-c)43+>T%BR%tq%EgcRGE-CT)XC#0Htz^LUdhR{o0f zx(i{=|GNG|rgRg;g|A&+o7V1bFK~VDy2tLc*B z+A`zp`ZH~8vI9X?&!7A$Q>M{qdcrSfSYjK%Vk7gQ9HnodHPpp#M7dW~AbwfQt}8*7 z;@GXGc%VYs$KhxB+^5T2e z-M?eK001+tC9Z$#r)(}j9P@4MVo?qsijkki_W>3w4=nHuuNbK}DjR9a1-q@J(;BuC z2p0`c@M|ar<=nqPHbKvk*QZ(UA)X9ic=~5?2k@6&<3UJDkfE@cc;r`dfk@4sA3^U=v1#s#IJSn*3bv%mqmgPW| z!~{yFG&b`6NV|kaQ9GPT1R`73P}4iv#%EZs9=@6CyZsdai5s;JMt34?UGCW1&VE25 zB>#4*n|%n9o3Z)u>d!gV9y$u__P|i$!TpT#w^orwPg$rlwRocS?o89_r(L$7O;#74 zjoEzQI+bItb-?@Md)DKLNd3U8?Msf!%hN?DfcM+>yG{k+6xtN;Jbj0x%X*{prNVCK z0CLgzYo4=14R6Kg><2>p-Y!W*+s~g|oGiUIci~#o-m*V{Y38bz_P4Z67mWbP*o$-CEC{eoC8 z$={`lNH5PgBI%-kin$6B;cL>Gz#R!Grk|G}3Dhz?&|~JS3Ji|tL?y1u?XU@R6BUXv>@R1w3jvcLa(M;-`C? z-n1qWt)5M4`9d?3^5bjLr-s!eX8L$n*T=pqDdBBPy}6ySWS z{smS0>cQG56Nvv>t+yfEDIK)9!xA?EtaB|92^`-Je|m0r&f-NTkyILx#t$klFV7a< ztLqP{yauXnRZFwob_|V|6me0{)uoa1?w0$_E?02MetTv0;qI<4;X0J8MGRXQjf;#B{T;V~^+0JakQeQvE`I?+C1C)atfMYNS5KRr_T98?j z1e;9HD}Hll%-+)a-+NUKAoaM_Q34ssdDZvO1bD66D=%-0B&AFE=eY+S;)RltSw7e` zjWZ~%{6_*@sJ~`G*)HQ+dlsFH0Zo-)=%?9%gI&*+E+NS_AC?BEj z5}n*TKB~$ry2aDck=wC%vChh16d(q_=Z}8R1s_*4!+%)OEp3dU@Q)7)Sw>&_$K0bY zo7Cx9Isdw=AHGwncmb}5!lRZ%OQ&**p`;#$y<%n!<#Vo?uG{N$uYB0074wEoTZce| zMtHemdV4O}gbeb$H%_K;29`U^NN0Y_tgj@&JJ~~cYj4><;)XygAhH++@a#Y1*n%$i zg|X~D{WQg(VLqyHn_|zcDCa4@cRD90M-x&5?4YiDv)L-z0Q?YLgdG^-sFI_WKDLL? z*l!VFi2=_#_vqf}@uA7E4gVdn$XUkXd-J*?z&E<2Y22*9+6z;6Sb)n;Y1~RNd{>_hNwu$Ms zojaHWW=GCmC_aVnLY+k2z6?7Vc10E`9EmZmZN4<0x;ZFb`ZftGi&00}`eK>~=GNxx z9tUDgi|uE*I#L+tx{h4mFS2?ZtI_~BP5x<>X|Ly9chm8^_bD5k&WFUwZ>xNR%6`bg zM4^PyNC?8@I_3uR%*2YX4;l?=?6)xxh>Cx|bs3L1m3a_aX|D-e2os9k#u)GzNR)G- zHVZDHDojAI(G%s*#7@p3AdppEgxxX#hx&uzuh6iR2)QT-^sD`TL&+*nxd$9xX8Ev+p!H0Y7GGNDiguhR<(}v>#RU9wMkyufU=c=LR9n?&0{MyA^u3}K<;;p^d{#2wP6v>zNBuwpq&VE8hgt1r zzuGqt3$=Pnd52>jc1X1})hRXrpf}86Xl7{oX=Q$ za^vcHMNl^P-A0~FF8yt$(=P1SFNekmZl{qTNNiO>9*m8I1 zn``$9JChbQTs;1wt){pRfQN&8I-YM-(*cV(35{$$VhxMbOEyZGan-A`t@0wRSt5a#(cA9Z^kNm`yYhzE zvK3D2_tY7eKGXdGrgxH{sW97`O=>J&NJuWYQw>wyOE!%bi?`gljf| z1v{u0+_h0W6bLQoCvUdbbWfd+c*N|DDH@&i&CppCzhe0sM`WP#`GMgozh#moo5M*wuy70`&J zVX!mo?;pVtbWpEj)Wep+GqTyrk*%3v{0-zEdbrS2<{vL6vIAc3{NM=cy!?_It;hT> z&sRBKV#8yz*^O6dYdJj=zHg(5@GQVZ9{rjy=mFb2z7x)eW2k0-;=~C)JnrbXj{mA_ zg2j;EsP&BJ4(`?zeb~PDq#Wm0Yf^8GlMPZg?y|^{ZhOSF*0^KX;d#9sG)Hhc5*Ws< zgifUI+Oo2;jPK<^j0m^3<1oZ|rq?cU1b_D0&+Ugbur7RkOOg5@=RpODwd2R~0|vE3 zykrW`{U`wclHw0>!Yg`UJ?RNlc-C7?i(hjXjYHk3N{z>Wwv0VIKIzGth078+1YiAi zYm)_@wWSA2RP51jl9KVC@m481CLedee^=J!($8NqXRE5~wa7x1?oR?nm~`TE6aU)L zUq4%d2pW%4^6itPIIJ1V~D{n$7{FWI0nGc4wyEwga7%3mn7j?J4UKDcYMqQz>=>OylvJo5Th{-Py*bK=tXPpph39=#% z$G=VUooZp)?C>S|){E}y?shjbVM(*&i0kzCXy%U!?PWIYtr4UWY|Ns5W{jy+Z|aUL zxUj@z7>3V?#BO#BARrUT&s|bYd)zeo=Nh8i^J%?0kMmHdS-?Hmyqhu-8n6Gx=8It; zCWy|8FK_C773b+;%GelwOwLKpi#c%fhvSXZyDlfF+&;ugU1~1C%cby2v20ZE_t66^ zJj3}vTVxvZ$_^RZ2EKkq9W8mmK7u{Y(^ExvlU9FBviu`xtNU{COUsw6#qYlxU%hyx zHWWQEvd`puy$Cui1+frJD?p7~KuAbYPfzcyqVqqU)e9`2q+R#TlYET+ZJJB8eI`&Q ziMLuyA%Nn?)&893n_?R8Qz=}3K#Wt-D>gD-f|kaLSpghoGc1xb$CU`WpZy${NO&`E zKwjYS^>{{t7tZqSe9_o}n*~(GOD#1mN?6?lKbPM*9<40HO zV4R`<;Gn66g$3EUD05Q6NS2+(>{ogZ$0*g!A*J1WV*?JqJ~IDWqWfH_kmOeTlBR%L z$Jy6|C;)B80P9abB!^_fp@0piFphi^*U-Mrs7`Jjmj-@zv51G=f^9>O1DFLZBHL<> z*Ikn7;#;QH2xk#4?}@0?q7PkP-VhXZbHM)d^RP3q*wRlgB2QgSNwLz4zV>)E3BGbzpxOBf9`p^z-lH2YVRRtSf}T?E zuJQQO#y7(UWBx5)`B#{~k9Rc{CPpy3r0w#== zNx(>|qqD{{NUr3cqlaZA4d*ukm!|kq6NuAcSV~~WvYxylunwGyY^61tp@>sgIiXL* ztMAmm7p#PIlHZSpMtk&ib@;l`iD(>gKZa!-$Abi^!29>_Uzs?%M5wG_iHM!({b4_* z(i)THd0f-aKB-c6U7lJ1`@B>s>YVc$VfJ)2yucB zg4stDVOk8n-bo&gbtD$}>FGz5PE-PS@gt+W11>tCoUDX@i-dzkoQ#c3$Fy-G*=yp6 zO+-H-GNhUKnTYT4;6-4XC&f>X;V(+e3GSTa!NX6#T&=xp8X;!k)Zx4#;=+HB4&=V& zep=`9<98|+1=$Kv2EB4V^e|aSD1q9R+5)@z$zf!pGR0rhDeNXYveuRo_FXV?D#`dC z;a`LrnkMcxc6D(cKYqNpEATyG;xWcsHSe;eOP?;?=w)G8fX0H?XocwfpQ&bXBf1@P z^Z68$@X6H68y3l`ZHVhf_FPHHXJyT=Y_R?#mtwbGuDv@La!A4S{N7`u+6+V2FP~ZK zw6A=W_%O@_&l%f|YC)Kv#6KVmmS%h>alL7{_hL=Vf1Vwl_7+%zNr0eke1H)BOGVG_ zwEke2p(arb3UT9n_CJ^>D^YwGMm@2iF2bUGcr)z(20t?OF;h}$gRm3l3^`~OeOGfh zaNldBQxjSTmW~}YUR>N7NLWRJ$PzqBHd@^agn{Ct_#DRi#^3`dqtVGQS;;Z<%q4NW#85^ zxFhS$#g1=uW=BmA!4K8`qZKetG^54y;{fHCSTT!CmjiWcy>^6)FHOJ`0LA&k?0)n+ z-?DLVpLsaaQ=V$ePuDi^PL!u4SzfI`!2xmv8!;4n%+;0q4}Eb&Y{vBE3N^~Y|qc8 z1UetnaDE^D5_$cIl$VKn? z&yvOjgB@qH$Q(I_o*MHMPHLH(f6Q^6PMe5Aiwu+XkGhiV5DK_DNApF%56%aYt$#O8 zHmopKZdjbZq~-BZe=sW&rvrd{6KrlE$5a>*B(SNm2olw4@nl{eJj=)_OB-dFv|80( z*lShV_NLj z&G_kGgU+f|D1yfN|24Xs$%a+gyyVXEj3;fT{8B?*4lCG1Z`7!7&;NRVYCn{qxUrI; z_;=iqVmB!Ud0(P7Ct9;C2ul=pE^I;1WE{KZ>H}@U{sCwvWB#@W0;UD_b)IjgA}rEx zwf6tH8oBR3^9Q5$6l|(|&$oZZ5dRx?kvM$oK?$L?W{mo2(TOFckaqR>_L6} z)7(H=#E82T8|{`n!zFrurxkiq++-+ot?ZhIvM;+IX6$pz)UA!vM++M34KAhppKTuF z8_ZNcH~zF?6oMiTpUZbmt+scWFB{dD8GsZDnbhN8cw(u%4vK9$Gv^W&P@e zsYT;Nrx52}PmbKCpz%5z{J2&E|NX&i8V$k$NWSV2(0ek*Gt%zH`w1+YAraAV>C?rH zwi$vFBtO5ENEP3igH?qSh5eDhH2!*lN6|qx!vE_}#(bw{VSEI1z4nUfCeGj5)a# z@4sosQ;a2&@Hkzjr}V}+kU%R zSj5qlyp`U6;{{YM7*#Ob+&pf|3m(0Cwv(s{MSqeFE zzXl+~(B4~HTl=_Ri>*oiyd`t{BJu}@K5RU)c~Gs{F~*m>?PE7L@M0fLR(h zI+ChysyMW5o2>Tj)~NGZE+YBoMfN?LbG|X$s-LFh6Vg&2H}X_iejv3J;%K`~Qp|bjtgyuG4ngj|7i~U?Y5EmOdt9$B ze5KkOxnE-1QtNlvfOhLB_wcl~OZ%*U4E3MGKWu|?!xzFLz^t|FX=*phE#L_+U;>rP&{F9Xp z^=_K`o;`Y!=F_Ytuf+|ra1)DlBO=@;U&>oXONEDKi+Gi)#CBCGxRjv{~?vg(J1ewEIlHxN^hH zNRqn;Kdh~+e94o_pWfxSj;3S;rt6P3i$+i1JpOW^W}6NSxjQ?@)0BYcbGL<3)r7ch zxSrNIJZdp``&lTh+|;GE{`K;Ki+}CM2E<;Pj8U;IE*gHFtB#lYcyq}lJ|pe>Kvv3c zhLb*-*>omA;r2;m=<}bst@UXWwjp6cGlJrGw>iyAeRY2+`*z$h^GdW02knqq)p-{R z&^MIyZ5n7giCHF*B}v`e3$ zpijtgW8L3Nb-S9rN`0h%pEj_YIs0aHqej|Q`X>GCbba>%;lJ-y?$)b{bi9-hf}1Rh zd{}fh%-1OB``(frIP>n&bO(G>w4%k%pjP6c1#=#Jz&b5IWu~P%R?U-ZblNc5ZAbMN zeTjmkhtTWN7g?K)KNMO5?sYDwE}h8MHK`q)I??T52zxiyGMy7Bl63Cw z_KI?=n2zjQojUR$`#G(z?>;wZo#r-_w@h0+cd6*5+JUj-8d_!s`Jy934a+Rr@bPx3 z`R{u)QnU9^k(~J%!G7A{OL-EV>s7ZM`2ki9u!vo+GBc-1L`Az!&*xtmec3ic&-cpK z_5SGfIuSvAiQ6-Y@&exRo$p6GnKO*whzY*R5gb>M^*L60Eb3F$=nzk3!Of+w=CIiR zN_`ycY?J!V`81_%=ADT166d4RHfy`*`~R35j9s0d`|D<=wU;@~J%BPjrR^_6>%_}t zt}efK_59D4o70Ylwr;n^1~PAne4Kx- z)@g3%SYYz|_)r^A0!nYf_LAv-nvi)us&i=io;He}KD_i3!@D+tIHrd^U!~wvq{G~& zeo%#Eh2Gjvd1L8N;<91a+&A8C1650x{Lrh*C7o6VUphvo!*Xp4^kagj8V+|Ckeq_| zSDbsRA>laZw&M$|{yp$kV^#%YS6_Y_H7nFL`BIRk@4in|V&}l{%eHzix6o+U<4;p! z3%-|<{w@^JP7a;^Ref>DEP-w;OU%OcowH4vR_m?F-eg#N%MFJush%xnwvAjMH^@+O zZ8=xbT5V=u(K07t-1KWcAGYvxWGvb7(d20TSi2a>N7GGTxo+<{2u^3n_@frNVI$}? z69_%jC}rTRALJZH?xSp)d0Xb0+^~8v;*mbO!~TH%&1jFOG!{j$kQX zN&zCQF`4lGeU=mX;y0Yc<65?{>n{uXEfalaDx1`9ottbp?-FA&-Qy8fSo?G=OX6g+ zX#U%_c8yfE>YL8Ofp#ugu3)G;twL*udg67oxC=~0Tu+QnTN;O+s2;fj6Xty5+4RWZ zfaIruW7ijTDk{2u9gngTNzDe^RM2PIJ(abG>ei2|mdts#x~D|H{4iMxIcBEyq-T)= zp3*PQ!D3*1X-hE=&3!1xkiEHUU3TgWWE7ql zH($WHtN+#%ZuG2)L|r_)d6YlTV_XQXRU(RJQ#&aQ|LEO)8Zwr|wbn(fJdZq1=lh!AdILu|v1z5*wSgKvFqRsnA&`sr<=9IfEgq?p<^t=x#_$ho9t~Z29%EKKeCo#2UGI=%b-*9y;r~nUuzW)#6z6%k@CBQ= zXyE>nb9Bte%*+hQU~0yqE<*cxByeEziT32G)G5~e4xYo)L6Om3&HVChsXn1!aLe{5TXZO3; z+_DK+|ACCd?|1e6k6v|n7+LV6Rh*2D;s)*C4^q{l0=hfCJWiwfTE~Ij<|*Io{|N4> zD>dNx3mi6xX;HI3CnR}}ImJJ++9WLmT|$h1RzWvw#sEVK5!(NVf?)+La5Cgv~z7w?Sl@5TL%P-5Pex*_I&O$OjK%>~m8 z$OU%gNQJRNVl#8h^4gl6jA0oeYMT;M`97~+mC!O#r9~wX>9k`W4~F=ELh>IdZ82rZ z?+qD{_A$bW#mA{n$wOQj$yc}lYEJ&u9qD`$)wnc1U3l*3mL(i!gFOS5DIe7)JiW-j zCGwO4vgM81H_KoaAkt5;46MR5_2jW7;r0wat1-T<}pz- zMzA%79P7mNxF$jGpthux=w*j+S2}Fp$lBEta5;Nq=YMl89vFYfZI#`yAHhkYVe(*9%dAe0 zAkoM`C$tJJ!7!kJl#^37c;B8PY+w!2C%i6uM3F}8@9Jy8c zJue}1V00=Wcb(N1yrchK&8eZy0cQ=^;}y!Z&eea@BB0Oy0r-P#qC@INo&jW7Yi ztHOdSCL@R*8EVA`a3miG`+r{?z#UI-hn8;?F(kX`$^pK8;Z z*Jr=8l0}l!`!edT6D;~155w7<3}IF>X~l*iDA@W#G02=$T1fA%w-(PWHC4RHC)|)6 z-b-cEK3Uu|5uwr};~}B-);ah{T*jrTeJ7j9^`3eBKbZ_z^&!+zFK~~^hgyT>OPM!3 zM>{J0#hVaHA7;nuT-JR6^Nnc9MZNOiP_#)`SfT>a5J8xAA6)u|cP}6QU@Bo*V?=la zZVTcjhwVa<2t|2^P*fsRi+z0|{R8C@?~e^OYfm#$NTl-)rt0qUO=`fE7a_j`S;XO@ zPq>d^hsjV*#nQ^^zHeJ|vrO8SOuTbR4_FyS0VB^whV6R8rd+aM_ei$=Pb(si*l)bu zHx1*lO-?9ZXqIA*=zXa*P~8f0K`x}yv$*0(aL$gCOX~+3KgoEEFaraA4JjkQ=s-rY zDZC1tD0PYQ&85+}?3?jiN|nNqgum-Xab??R!gl8*C$xBzW)mD1d2lhHTh-_Nn@k+T z4dlrQ1+A`ODxlUh)f`zVl>a+DAYo4fb3jKEwpRKX_K?ah$k{#%*fX{YNnX23vtcY8 z9&DOkyA^=ZZYO*va64)321|moG=3rQ9r^7H8>-uPkMUe;mO+We3kixrpJ$s>J#TEdxBbF1gy zB+`7}er~NNf+}g~fRFe)W631pzL^bwgAf!6YJGl=CzM~Y7wl=sRkyic=UvRnIIx$& za_xbFEg^n2ay_AWI+f3MB^Pff^i%*pa?S{$w&gzSy64ASfC|XL7Y9$jUu&<6?ty9H zKBJ5C;gylv(Fgj$<@H~bN8YOWL#G)E(_+%9Zw-{|HlGYUJd1x9&Qfj6F5^nQii(OH zFQ4K94Qx0ma#%?9%&*sM@*U@PHI3wqJ)w2Iece=HD=ARD@jD5v()fay!mY{vgK>&Ml)HJ0*)6Fq4c~$C)ki?1%yotG zt$VMP9mNueILt^GmGBFidKD4Wm;(K7y!>*XN&IHet~o#ljOeTui)Rn$Ox>ttnZ zeOB)F2oCx0VTODc?Mm^TBmg=#?U(wW4?_-TwF=Jxq1C6u&DY0c_?nF7eI7(^X&Cex z9|^7wEdDsgRKXC0_($GdKgr+F>GgS{ZeyMJ-#yr4V6L0A)YiLW)ZreE9JIg7;f80{ zWD!=5ei#IdGrC~~&?^1A7I;>9X#LxXZs6-A44SeNEPdIHde%fe8PmP26w z=xXY{)XVD$HdZz)ChjEI=8t_!sp}@fgBx9AhT3<$K1QIUEk6f$_^ck)elL+5{0jkE zzKrVs?aaCM&<$r>ypgV7;^_FIf+_|$6OC@w#^ECom;#TR-mAKOD>X--K*z7`m znGn90MQy^3i}Fo)yF^6iv~~`P9un#)quP##DP8q&U>i=R^>2sW3orGD4R>mIqm9BS zhsB@Gs|U%B%?qk*Guh$)tujMFiOT4;fWiwxxjzxy5EW4?OZ@2=cL^2Oo`WmbkjL?(;nNlLN9;2w%=mTqHKLkkQPd{bS^oojg?rlsR&b(^9+VZstyNlX zLP+4dMe!lqa^EJH)-oAX$8X9{7flr+tiVroj{l>N11a58oozTr(7u!;^%VVS&fm?d(7q1eS zqJ5jU3jo~HCs{I+Z3@<{)M_^!)@ieAxJH1D-e|AG1;rTBGC54)`&$o;_@fI%gDvR& z6YI}DgapXKyA}6r)t-18T!Pk@9y_+MOs0`8lnDQB^@Wi(Gm|>{r~WA#)b9 zzyIpk#h97w=LcKwe?Fn{S5QnRbL%^e&}(;w#F~%UcK6YRs?(N{ zR?9aKxD_2pqi3Ur9CV92VLVS@5%%MlTr)}SWtVOKk>P?aLB4jO01>J=wcFtzZz{4r zfoVmN?|9`JOP+zwoF`@9F5HF^ESv$Ga=kC+96tb9q(gf)Z2%c{5GPQVJ$wHA=LvNf{DN0$ zuU^eu&Hvr|BtZ0|^U;Yusm|c*ALuA`G18=xHjvz?T2P>!O^wUcl$%A+_sHE zpL)7IAIhpw@iY+;rEzkO zJa&-Y0e&qH*1G#Vu!Zr4jmg=9!*-bvkcFk{U0Bbb$EJrdD-Q=?K|3SjczQDqmX3_7 zng7=$36TkEQBYsjyr@KdP`y;a47aCyIqerFVjo>DOm83V^$!#M2n83NFH3z{6*>z( z?>e~Dz0dKq6=l^=n+=lwax}o zqeQgf{sSGiJ>Fy9viOF=vbw@usYSd8ZoP+P4<2(yt&Y92lfF{qv^8(!^@&A2)kvoP zCN>VCL9oks2g{J%Hm>ip%VcFHE08)&tkHauW2k&q3Feyf7AH4qL+8l>L`2X~w1gbEhToTZV84 zmN)RMv|>|ECwt)+R&kF5mcS174;94{h2R{YmJh#mJ|#)1=BYtkawPD$UwrDo0&@W` zzfKt^V-j)Nx~bemXTOAI@Qg(cE4pxQ~UWbsiRr1~vBg0|| z_f8Nr66>f2OzIvXJPP)oDaNy0cwPh8JWJ0#1e`oJaQ9N?(1pG^d znYZI!Q5ey?Oy#8eOuT$mArpC>^_>JWAC{)lynaitP;+3dsT>LGLUk$r4Jn%}y2o~K z-v=Hl-;jXr58KMU_|am&7Tyi{L`aH{Aix{B)q|mp90E^^*1I@s6f`8W|_uG z2jzE4By2O+cZS>V%t`9LQ$7NgJtto;*s*=zAoL7XQ7e-XIg=SLjDOz_rtw2$r4&r8 z$PXu+NHWV^G{VA`nM^pxu4F+0AGmTrnoMnX)mdqdJsWy_gJ=cL57s zqWmC)`3dyOD0R9zwXm}r{-ufDx~bd?u~PSk86h(wKJ~qe3VR7Bm>nN~V6+w}td0QW zS1YaO95J*yki>4muVhx_OR;W|<)^WSyZ3mimaElxs>t2K%X_cJ0(Uv7eOP>HC$qob zTa^4ZL%M>FHiimUP>`<>ev1DU2#Ov+h%BM~AwN`$=k93rm-SHv4=FO^Cs_Sxe9x2H zU|5hy@{4V!eGss!=0Oww6+_NqycmTH>~lD8xtO6nc_yFJCa|P?l%I>RbG4Hx(7q9YvL3BPX&wsyRFqKAPaR9 zC6Ri<G$^@)8 z;X;9Q@MQ%ii+QFa@XM9mwZCS4Kr^MngC4&CzA0R)tQ;Nqcu$GF2#i{p1C@>gP-twrsrHX;1s76b5S2TEFA{8g zym{6G7P-vwd#yO5&Zb#3R-8~JNnIN9Z5I}COR(4Gz;SO8{d`@E9UMa_ZPq2Hi^Y6K zX4pn1;mbY%-|myp#+6h8Q7aVLZtHiu#o(6@`&F}JgS?yXPAF~vHyOC!#_@;P(ttxm z-Am2LGL8~nq5)K$Tjupsqky7>g}?{}?xakCZXCYIy@Q5NIEs%EA^K{St}9GXF?32> z{1v8y{C`MwrN6p*<;t2(;vq<&+B>a=f1D|y02^B zHdXp!@>#a&FoA!4%w?#cl=8Jvg#Eq7`yPTPpK|_hCzK@X`5sW&c_qg2`~lYf8|GrD0)^a7Bho3-_yj6sBi4uW zRZLAyFru5bPla>Z62`m3P!Z09XtOR;KRcoMR9B>?y)^)e-6k{-m&QW{bAYuGSQ&i66SYy1w`x6qA`k^t(|u`YsdRszyDV@Y3~fMlY8T&!tHx0kdr{;}>z|NGi$pm_&L z?=tVcmdQR_(zMPjx|gEFKqO$ek;E^cc!_1{y3g%(zXigcxfl8!Ifq!G#Pb~5x4$$r zTjxssoyc3^F#MmzhQP!|a0&+~smT>!;OIc*8aMrOJgVLTc1?$dXTW#R44~J1_&qy6G#XUbaH;e!H zyosi^b{8E9>kCxvrY>Iiu1(!`bFfo1h%V94r@(HiX3%vLQZj=i_tFu(`*J|% z=ni!InSH!5yLXr&EnE2=u4YdQ>H%@F zf73Haa=@3o9d+Jb?w^(k@;Aj6(SYzE;!_ph6dbRx)|f$adC30h8YF|rViq?qfzR3( zn&5Hq#qR+ik_?%VAi7oTiYS3l*!XP#!TM#W>HP~ir)mIyYVKhz8$gG^^R}Beo6Z2w zyIp(@rU=wrA=yoBWFoH-53u>EjM@|rz2Xvf#i<2vv>;a{@pWHmti6g2l_~JGN(A0u zosh8C$#AW!<+v@P(X2axJ0!VJc^AkSbxNNesd)p7?je%Gj~^qD2jq<eu2WN)Bd$V#Z0)rINLyXx+OCD0W{{T_y>ppU% z;Eglo0VJFkIc@+nc5VRUYL$BXJXi?)I_3a*W4g}$je*DbT_C2;zVRzM;6xrWQ>nn1 zi9xnqgT9c-GNfp>2=_sZj%qK$fxxh8UUo=yR95Xe+NLLoZzYTGGMJ$mEpxR5@4XAO zuEf<`BLLw4f-b$FhAMF2oPubWF%ET(6GD#&ZT$d%`*#=$N~`|l%P>UK=f$J5aLnfJs`Tl&Sk?AZAnpiRI6ujU?}Qsc#+XH> z)oItx^6h^<0YYUQVk)mf*cl4p=gwrUeNTE+s|#Yqx3v-&JCx#G4ybQe-uWxKhsTji{bDKtJ`of`y)%p$>dN-g-1 zzXy2h=_QM-gj0k)p;->wd~Cn750YpZ%^&Tg>A!=_s|@(@dI=?%Jy$Nx z`J}aWy5XkzawO9;P=aE)tsXPI3%#3fS-hls3SAvd&_S#a&zzY zVj&SRZcqADBi;XSJYwul5FMl`3S@1&z#3+hMKoo73b1;9WB36lbOWqePW9&*!y5Bl zU5WT0-vki2*)XR^~{^hhjh_%D4EzHncsd zDDO{ibpw!&M6rcl&S~Z#dd%X~v25>Hohd~n?jPb-p&l4H1B4S=w%~u##lIWT?>y8I z1SQa}E@w@*tK{->Ll#R%&SPWo!LC4Nu3Y3`0OxDOKj;^U5 z1fBp@x*;jaK43)tI@UVxobSAyz&l8iJr;;Aj={a#{mD%sU$9*eH);(RK@v3AlKNSm z)M`L35nD=Z(Zh%Zn)tu9wj?SH-Pak(Q-4ic^d!w6vj|vHwZe@yXMr4-<1KCzX#@t0 z_e0KvqH2sC6D(=x+KP=z&Q7S>{8C6{D&y_Wo+snYig|z@IkY|ViagK{WKzGe%P!s+ zuCT}V2C|+s3IwcN;&%=9ru-BcJk{Mm{BKBwWC_&c)u#8?0nVDh9U)7_HgdgJ~55n!L$wVDx-z6&#CS@Hz#$t1Uc zwZb<}%Pp`0eBi*LC0mx%zZ78}C9!+Y^m-UDOnCS8ePyN4Vh=qqbShkv7IXdDC%$dh zV3U~*EE|RpM$HwuGQagnfk0TX@1$p{EZLr9TG|581{Rj1@KH_tIWR7is!~Zt&wtHc zI|AUQrizq%hQ}YX$PGO1Vh(_K?KfH@KjX$?*=Ud+sX#VM$T389Uy8VcEE##&+AbS! z!_OCd%wiPMKr?X}cog!5(CS{|N3jA=E4<+rmn_L_O zkQOxk`#+PM|KufL%&`Zih|RuCFD;Ex-~8%wHSF;%-K}siwhQX-e&JW1*TEru>`BD8 zA6O=}-(R)fYKtMT!&DwW(q-XE(XXPX**D&WvK2#{muL1r`&JbLNi}A#IXq+>jQMsb zDCrFn6j<6PE$??8%uVBeD+SwdjSd+{-`64wl(>g166x+ZJyTv|lc%xH7rK?DlKVrX zy5(il_T{$DOH+?%$#Z{L?hhpMqCOKe=JTKjfkfxDkTAIjBylaVkKF*ZT0aKdh6ygb z^GO?Df60vT1T2t(eVY|8{yfCr zh;j~7;evMPwKPCG_&x*f!feXJX<;CSd~ig%Gjm>FM>Ts2he-FumiiZ5AG=v2 zd;6ppbQbzpqMN^y9Pxi8IoBX$NL?u%&h{t?WIJC@Zl*zsyWexHl#U4b@>J8_@pA?psb3LG3E^QkaeyRVPk`hoKV`8_HB7N8Gmz);F7Wc! zTh>7sax<8eI_4-vtBU8V0GD>r;U~&w< z-#}@wR($_1p0&t4A;|OZwvdUK)pAY5l`s+$tM0ZN(3cXpIJSVBGuu{GEry?`5i98mCuvEO{w zH~P%T$8>k;+b(AIbjBvM7P=X9nL;0=@XYS^2=u>ikhB`ed^^tLbFjfaoUf)<+wT3d ziwhAZ22`CtS*t|cLAVp~m2=lGrk-m9K)FEhn)^x~UM%oJY?XN6>mJqJ4?h$zJsIY) z6aO3m_J1l@UdwlpX*Nsv-o2dHzN_1D+VL5odDg+AV5;*jjS~?~YJ4zzSZsgXH9!J@ z<}9=mzYNh9%A)=+j0&MvkAS5Jow-cDGT}e0eGPC7-#cB}ZlQ1ULX~xxfrL<(YlL_V zimXT>@SwWl#_p5fdFr#C%P}AFKxF6<@PfX`PsE=pV3G>lI_VMaQa?RYsO4K}>UuhW z`7FQCw}XZ;SUbp_?vER1*OLs|4X>Rf?{GTo;KR=2N?RRO~x zpcCx7l$&Vi(l|ojRSM z$jKdR0O&ZSc}M2~L~UqSVOpL(*wpyQ78d)vLcb6uN7Xu`$SbY(2h@DhcPrKmK%Kww zyZ`?4XDs}mS>G>HCQzopWJ$FMoQvN8J;h~%`V3e*Z@`g?=+V-f;=2Evy_!foqgaff zvzQh7`7>?XPs#0Rs!Dv)SQ0`kybS;hD27rJ{;;fWv$YFs$MUn=+C8xLn5*&%iOkLp zz~hwGfM^LAsa5$cGR`_RrFj$y#IBLi_i?Ym+LQQ9Arac@kKR8&6PpjczYntYRdByP zus-Fko-(i_!_uEiF3pV|yY#afJ7y%B-$jkt26Ros zMKt>Umn#Bxf-2Yv%Om*?gu5U?h|OTkodP*WO+6vJ3dGhl|G7>!&j;oRrtTYIJI6>w zcV{dcsf#laU(-RcBV;|QJs=tW*sB?P_y!bK_uSnpuXs*54}nNgVI$a?9o5I(31UN6 z<|lz4)!;T(hxCrSA%hptAss3+RT@0ph@b~=Q{`^`2!!(7`+|05po%Kwsb(Utf*fB$ zjDdaga%n4tO8o-@ZZVBuftX8fRPqKJ8yiK(r$A}D3zSrLOyZuYroY}~oG2vR(p_li zxdZCY7r)YLwA=0j9l_gXO9w8?ck}6#Zaw_cfgfmf?oJSC-)N$E=bZcf3u$L_&~ve) zzbOh=?*F?ZL-hkRrh?S)@du&&sgaM=HT)d7o!nkCwS7P!Xbra7>|J@d=h?%u=7WG! zN@rbK-}6j?tCz|}C!If^rR@~oZqVrDR#C44L9~5{{cxGH!lYW}#okr;Rh2i8R~uwt zQ5bzp?*mMmi%c<5A$oA!UWR`D3*bl_cTLP;rOM_z=ODZ0i39L`OW5|`_H~&Lrp>$b z-6=YR7NtKR8*x$fj%<50i=$SV&w{5qU)bKbmbQ@N$o0K03iizKd8rhrzWUCKJyPi@K4 z#l*aGyv^NoW1X&ASP-2muYU^y=u=gJ{~3e)0+8GYSu$=Xs0ai}tV4X| zw$&30fSrvNfR~2w!%rEy9dHjfk4UKMbE_hA9X59b4Q^9r281jzpWrXFJ+mMIdAm3MMTCPEn7J|I_l0Km>6`gJ-)KlM z1-*u1nUZbCS~>DV4|BaZ(3Mlh=w|(|knov6FVHjz>$o!%ZU&(4d!t^o_0@!YuendQ zF`#O8;?d8dTyxLEHz@xyCHTO3F;aw~+cs%+|GNt0=)}VgK(=d8G&m@35cXdyF{W^Z z?yiU`U^hm)QUDCk>|nsj*jyPi^@2l|h!5mysz>c2O~&8Xg?(Q(703n}VK;O}-Rl-_ zgbfZ1zyvIM?r|W9#kjgaU=|-7ztyt|>YR9x97V5WA;uuj`MifI3F<3At2Pnj;e5+a z!KORhoa}zfp`m~jsixu5rdzZ=ZMx#KmX+Q8>qihP$VcMz9csVcS^=}0dzR_m?Z0Ns z&pX!4ynI9Ab*WVPwmTPy$Pa^Ef$J-53|)N%r14M47|Pf>Q#fAE+j-_8zhph&`d5WV zMFQGv5kDi&a?7?90=NyK{&W?zdqZe*&y<`K3v&iQOkOanV8jmaN2W8507a`(6L!;4 z#oXLn4=Hq}%xlL4f?CS^?t$e!Add3)g%Vm9P)X1lRAqZ^&$#?B4*dq=?T|%iY2q*z zctSlxpVo zM{P;&ax62pzK6hgn~ltvQo%}`pL=lL?`DR*2O&A}T5EvoFf4LrYzneSH$6wb_hhyn zSyI0Qq`NPty^^&QAM`K)Xu}HoC~|kX+Z-s3XZwSDH?au(`R%3>l^=paLa%T+Do!YH zGqpFmx6}CEl$ACTBMo!arm(jJcvu54;sS*?L+ej~W?UryVMTWjIg&nw?HrY9T@Pa- z4_cG%wtPsj;4ww;AKd6q+6HFwmV7b``xQYvmm4-@0NPY0613k}bH;_!f*Z@q_$JvK zLZ5>WoTI?O=1d4P!P&fu_-7rLTGA&d1Y*)x^7>|F7H+2){B92wkvI0Q`JF^vR=j#B#T`M4_!8!}R=ahvyCOztk>+Yo&;) zPBW;_BF1(&m(T5f$kP-a#Jq4&(5ES2y2vA@f0wV?u+fKJ`WAL{2N)m83f^3(daoQK zOJ+X?ax+d^LoegL>ob0Py)FPvtM3zw1-a`dxwRqKA-m}m=kTKl9?ylUsSww5jMck+ zu6nDG#~4e%N!cDZhc=Zdur7!zEU8l_qPUWzoxk^z=W`C;#cG@w;``#Ts`x|5^P{?Y zsBhgsCy0Q9Rlm(1kH?o zy0wPy-@_UAivEby*|Wn2I#{1_=$53;IVyMfmw(mnb0#Kc6={43B; znkIQE0WjD;&Zp4_S*PE1jMBRQ|du{W}| zwuRW@w^-}pyl=oX!6Z!_*k(lLniO5EVj_2i5};9f1BOLlnq3*?uGD%PdwlSQ1-qtH z(%tg`3v82?0XQNgM`W4iuC44X33>tpnW0f-ql-_wEC6_!?ca5&eB3!zCXEeL!(nRu~rm(-r|LEIjOplNFHxfhw3Sp@Sb-<-4bs8T5JaV?c+L) zPZHe*aX#2vlUxJOqQ#iOSJ0K#FtnT~olWs4e1L}`P~S_%#=}AkfU)`)_825azY7*p zd+im`)fLqVKJj-U*WL2YyC$5slxZ!@^JZzBX-I4=qTd>DmF0CcL4^AMZy^_G_FEC~ z%m}Xyhdv$K@8|Um7cqo!I=^&0V^es}%MVV_RJ{@}F;M|bZ@Piqx0M9dEU-^3 zdRnV4Z1>~Ylr^Y71rpv5%yj?X(3p4nzekh;_-@Ym@9eu1jEc_uu4B)r<>6S^Q44Q3B_Bn8z zTV4c&ZIFC6J0JSXIs^_ET*_Mf4orjCdfv}cZw81;&B7=8D(7iC4`7Kf#a|sUm#^zU zSF0v4@DT6Gpg(lAc=W~g-WMrku>MaurKxSnHyDnr)qmx4kU2Hfxr4<(4w&Blef+~wBZAY&4@)5vj zZAVGtulqfo>1V9`GdJ;1@9I7vBgF2joxv)>Ap4M`+Q(f$UpS@i1eAkvv`90hqk?>ZN)2lOGv8b92l;qU-;k31+8C z^2XH;RV#@YTgP2^rg0YkuR0-_tj<4^!nshtp3{0mHuDbtG3+3un>WEzdJ9IXQ=wA$ z@^`0`iX`&6B+2~f2NUAo7o1`xL&8AsQ$+=NdT^l$_l=+C&XZyx1yiIR22S0&vi3wrM)K#4w&VBgf z-&vGQi@~2DxaE*~7fhc+?F4|XEnms9`u>oLO>gn8E%nWNAgh=4yQKI}J5vxEj+pvF zb(lf}48E#;dUbF2%mr7mY5aPj%Ztis`~RflX3n}m5d8df92fP?+!g?^muo8lUK{0q z*ablS@()pn_dmb%;X4G9_y&YigDCbDOl+aPgL&hUpBEYi!jlhJBcuNkK>iD6c!38v z|LRpZ6$vy5GC+Rcj8TD#?YFSwPu^1C=h69VXEE0w!_-X=JjC=MQ>BG0TH62q;lHdTU9fE} zIe8Rdn&7+030;YGAOllulq`|IC$a9u$&?TSB3#_QbtdBRMQ}pMeZb-!qq0a3*u1DK z)u=cBMGpvz17G8}KGvKHI2lHIr8<9+z`wEp-w3Ceizc{kgc*-LV*w0!a4LwH+6HE9 z(-^8?PShp+`2QYP|BrM+1W@9M_!Vhg>!U!G^K^IQ&vYA@P<=}2a?a!z;j#;%@=Yjs zN5B-%)hqpfmEM1MWEfPvbjsY#Oa?U2+xG5MY!aOf2HeeW>Fj+Fkpq8%Rq%=6Z%s(pIB2FV z0qym})u5SBeGnW$pseYL9{m?2qM!n4T&x>Cz6xe`Z`!tgf;RHw(X1>NLjLn+m>HX^ zFX-sky;?z1+KTt(>YtXKF-_+o%=53z>5~-IdN9o^Q?-p)0QKF865*M@cig>9nQ8e~y^@JD|M%l>I$(dNaSpAq|~aoFbjb%{WS%OE13J zQ13Qn>NtKfu2gQnm-hl`HP#e_nkXo=o8C{h8>d5rW}RJh3W_^NR$~{38WLsY;JZ}3YLS%hbvJVjU*0j9 zcXPtMbi*6f{@H#$Q?1yPSDcWq;ih@>^-frOiLYmDt7@%a@fZ2%Bh@a^-vpmb5`1DN z&nh>Z^7dPC&jJbc!*!P!_b7V4TdyO;1^T6Rjt|$agUBBn257yozk?cwK!%}Jg#Iiv zNJ+#9v5Dj3<8>L2Ag@v;&ne@V0_BEQzI)*1TNU1ir>#~725CSYezK&ep%5=(+iz7I zh*EHYj2rlJPl`jtg9qsnx*#u&IS^}H9aLiB4kD&Z7*>;(+?>eIfa^Il9*kD(rOE*A z^)U<$@(|&7xL-qQ>UE&~1MwsOssY9Q0@+o#ba%S1d)bxrrq$5hZyO>-rXDV&ni-x! zewD|Qqrtoq{?QdUEP9krJec&F_iHa{pJ2ZXN@@QHKWvIP?sTYVZ+jTI)HtS||3PGS zZ`WroHowz(6{{}hww83Tdt>X4zVL~y`>5@Huc{=XqCzaFTk^5#7cc^)n1UZHhXmMokVsrpyHqiF0Brq_)K0w$OAr?5ECs4zH5HwS|~Cb`YPPIkRF z=p)}GfdTUhQErbv6^V9cQewuJN0{Tts9@B^+@YveD`1ML*6Z#VSagv4@PIo1`ie{ZNr$t%29I}{b$NquJs$$&F75Z6?AQDz2U`DV~_CJCh~hvN~rGA9PE6eWq#L#D#p zGr%oB2-90GlQ%<@5PvK2-D1mt|BNaMKx^rK@=Lcn=`@%Ch$$}PsHs#>(f*Wq4~ZrBYG zvuJ-x!L6Oajj^SiYNJ?mLa%52!@)lD^CoUnjc@kFa948r>rsQcQr@TBM^4|ncMbqIV4;n0Qp9S+!mmABkNZ;JBqtmMn;Y$y|dDrf=GcFISX$0V9 z#jjns4)Bdl zMgwoZbyW@n6YIE36cqm{0v8xHL4;~s#4>_X<4Ee9tTYvuk=ly_(Xj13kR!y~>x*TuW(WY&AJi-@L zPEu!jSJNN9ftwMspve%DE_W7qFTfe_aMDy`2oF`cMfWJ88*gh zJL`Wp-FI#^8}GTgor7$IWnM)5=fvqmVfpCsbdh%%G3){kz4UT?2+G}N@!ABZA>Dv< z|1~0A?5=>tfv6Fs)Q*Jc@uWLF#&%{dLnYk-iMB6yS2;HF*uB{HD_XYFWr-s(xjLS5 ztdaepr~j-NPkYo}GoJ-h2E1G~z3$+_cz${G2-&GL=+F8*F;&qISe10rYA42RPI$&h zSsKJ%zAlIy&|yjpj-I*2P|{h!b9(eJUFB+fWag~sd8s4mGrXj?WRd{B7nTq-D-PijRtS>Ifp7g)!Ye@cP z7T1uNK8xz;9T7*v9d|FdsP!?SN4(YZmycYgtabD-D6V6lbmIn`FfVqZNqngevpulu zZEB9|hz^#J9tqU&k-DKYD$YJ)yx8GXZ?=v?n;%t($Tun42uka#ZtY}hg&apP4@fN? zt-N|zH_LsX?6c7#u8a;=$?KbMHm#7tRD zagd@{^%21QS_G-Z48DTo+-$ik9_qH}R@o)*eBM>-A95>gwjQ2ZI|XT8myVJDvUiDp zzJ<*P_1xE$wt%-Ah|v{)$|d&5C|{LH6wJTlpRON%)G7o&o;8e$jHp*wJr-Sm|3-q+ zmhoVwZtW6Xv3~XC-j>W|TaS>80GzI1XUhzZh@9bGu5x_QZUV0nGl^TN$IB;!nL#$+ zKk6z~q7IN_`jb0P262NNa-f?hc?4XeRY=-?ni=1~=U|h8DR#M? zr8!c9T_&g=kGc<2+)c+TyF#U6$YtWWs_7+(M8wonCM{Tvm1e( zbeF2FoW?2=5wF&^HE+#$o@Yp=QmsAKEzlDeVSH|cP3UD}6ICn?n^kYHqY+*Hn4jyn zoP$+$y)tQfVt_OGnJY`rntiU#%6nNueJ@E^yOv>YXPTIxXw$3c<@vS&-$lt~ayGBomT#ZkofXk7AS(C9DE>Or zaZ;$fRA0tzg@k@D-X-U~=a}tBh6H?lYv__Ik2yqKLv@=lj|up3Xg!wk$|Fm3R?8*R zRq~{bUyK5nAYyaui~F*x6Q00XT!6#aY>}U~xmj{WH2LQ}nBw()R{Kt2Jbb?L;*P~J zLwvpk%M=%OFtniu*`QmGl^Ar^0r_zS&^jxVc@%=VMm{r8a19ksaS}>;n@-s7$+I&v z4XXKr0};fADk;MD3dA^}JFFww=jh_)6a58=`f2GlRl86-Kf;vx?o7Hl# zkPM2BAp;=O=e8}okMC?N%j1WfqdOE&CmYPFP^bP%nQ}^W9NvY(5)YMiKAQT4 z90|nucwzeYVyy1T=^qbb-bEAl9#%?6F`Ugp&@ z#PM$-tOF^TB&hC@pA_zNkEGcqdf`m?;#^c1);|a>|DNZMV|VhU+!@xp{~7CR9y*AyC2wt1)I*yQnY6LyRvbyZc4x5n zx7TzW;)tm-+}@d7i4W8~sB2z1XYl@M@8Vq}-s9AhulHUx`}eYrH(KA-)X-USm$_vy zY}K)G@D%sK!o@2o@cKiICLT-mImfu8E3cNy+`jv`tn@nS=WifTH^{`sCi=RSA4y9O z@!9ElHDlIXOVxuk-kSx#%ho8r#Fxi;(?VhcN8sy8Ji~ZMzfDOTiMOQX+HsWk6J1Z) z!LKoAwTP|LT}|2hUc+m+LG#PkinlF|x)uIl%pF$or0jxhGQ~bE&Xh%*%j%IXVi51N z1bf5Ff@omTFq}d?;P^>m=4WroF{ZFAUB1_sM5hYrI-1VDF-*v9=iOLrtRdi=LJu2l zu!y-03@@$*XLNgROD{EDvl3t19&Bx&&AFNPb^t#b=@%qO7E6D1E#wqMLi^m~nA`FG z5$A!t0kg~L@v>1j${2g4Bj;S3lk^QYy>yySQf_Z)k>t%X5l0s@JR77A4jzAa6q`BL zE3w!>Gd%0AS=`%f6wfJ?a_2C3G6}b0Uw)K7E}LIVz_)*3Yw>3A%|WeI{}ppynNOCe z>9TK4Tvn3Wfpl9q(z;mt(54JqVNL``#MC&qFhcj{uu*NFN2RM)KKr^$r~udAno@s{ zjxf(Ele%ZPOvK10qfLm+O>ijXzLiSrfuGqlD#Ls3oUd=Bd&zYra5Fw8W%gOS3M}aL z25QBAmeBL`ECcpH0@Qn&BJ&wRF>YmstMJq+0L)?TPW7|TFNUfejn+qtQ%nOsM1BpY zC4QkS2y1Ejbruo}ki7jhq+aR}=|0Vzl&LEkLdmjqCl?ITG|}JaddmSa`s0GdcceTu zE$6qKuH5_bd!nnPeye<|NF(A0fj|*Kdl%?+asyA`3Xkq zZ-Yjg6-~}(UIxO8-{fG5*OXJ=lz`Ls-nRd-BcTEbP{sMw!n^$GNTv94q9jjcwD|NV zlftG9lar6zSu&4aj-3|wu8hh(gFrk?t?xXQE|u6PsV`%sW2t*#-2XajI@ zMItK)QD%d9Usq_!W5goGywXdI9;gWuy_nednb}bpeAREnQ!mVeZn!=wZaQv(q5W3& zAknb;`(4G?+mmv_SY$M@a&X;^uEE~AC#zomwT?^?JIc4zl~c4_C>fSSBCC95><%c0 z%?4{Mb2XaLauieR&j+dl+gb!W*XPAb#+k*spNuAj}s(v)1V4vz-yD91z$HX>o?K#9O? z^W@dx0zQ(wzrird))#1TsM1upZG_3dj6Y|yzgJw0a#`^96g9jkX3%jG{UX56EC3-y z`n~_6YlI#7WI}%|0mf5>hE4p-eOjrxUk`GUY1*sDYQs;oYA^(XR zd(w=o(g8yJR7)SWH{DgYsFRcxqf=BqjAeK~Wih1*%j)lV5gaKKFBhEqEGs*3b#{LF zqe_8tWp3XqgIu%qDb#?EQSXaSP7k)CkKHnOeBi6_K4-jUbbrp|Q`X+T4Gj8g`>Eq> zg=XFfTG>|ojm`-^pW6e4fGb^q>{7rEvLc%UC9$y(a4~H)k|0Zl-5UV36}Wzfwc%RK z(HsOa8q|A;@~qD^*6$<7`X}-=lC2J3JPJ>626H63p#i_1-Ah|mV#F)ZVd*>_HDz1Y{EH(g(G#WMTU3gS!qh&k!v=+={5Ddq!Rf*RJ z;? z>ZZ$Tn&-RFy^{~0B@f}XV3cYpKZ6yc#Suv7ZL#`AkO~I;nZ;glvQo)`yO-*=^TZ}? zSea87Qt|u-)6l}yREs9U=GHsm2MG^+xYr&vdgv$*tH$=47B%}|cH?c$^JA~J+72Fl z@RbnjMm`#l!e+Z1cl-MheTFO!>esJ7{FlwZ5J#nZ>EYEb> z`j<^h2gfHnk(z~*LsM@R{#>5A&ge{8_^?$yaa&cv;52_{w=d^3&&!;c?-dYEqzlg{Ph`2#$e^h@$r!4fyV0SG zY3$>ATJ+Y<_gS`Si?rs(DgSqdd-*9d6c-5hJ^&(rjc{cvGu7Mlv*VL z<*6PA&n&&qd2!dydRE|zdltR9uXf-xa#!R$9YMe4v`{d1Hx8*7iA-My0>kY0fs^uz93}PwNQ_nb9L~%wx=8LnSj#_1`CV z8bwj8EZ`(6k1++ioAu~-p~Xt%Z$(#|cA}mS705f1b!D&O9^{_w@mqBm|J8b0RNh15 z`6zgcWzWiT7UqB!@=kl#RwVzS!&LmhnGn9NDZd!eLt_{|Ie^@b^PaXog+JsRv$Jz} zZNaa*)%D1Psi%|%pO3#t-da_ggeokSW;!)S@j_TE)ymS0LjLFghcGJK!h4{Iy!m7| zJa(ZpslskteX9{|*pXQ;8Xt#YQyg`LM|-Mkq)VZ&V%Y3?O=X#H`M5a4JKOf9+fqe}U@bP6MhUGs-0>#v6n(#sAFnvnBDmLgjop{uEzE3}(% zg4kDGe~(nd@erwyX5Bt#M^&t_js%U{D+)2DJEi?QXB#l@!P%;r+$;(ApXAN0NMB_Z z>~v(XM?X)}@jfOUZl&O|LRdTlh0VA21|*tz({sDC|M`=;_k&R#$Z^Rqq=!)TUL18R z$o2^QU1wc!>XWym<2o{B$TDOzA1$vx-%s|8a(N$&7Sl1BJzO<443cW-vM{nB1&8b_ zE0ugkbPln{+n@l#0OAGamj?`j9;aUdFe}Ua=3#sFS9}o_4GYrhdq0MY3^F8#<>9kU zeG{WJXkXP2Mjgmnhd0irqGO$^rFSX6+B-LIuSHrk_~+XlJBO@FPB(XcH7ut~IgEQ|DEDGCS~uh?WxO_`W< z{SQvgYw3pNw6 zdX+Y3&6Dn6_lF}%)}?z@2%-Vq^?j<}?zd4X@~U58Gz#28xpKkh`CXRoxeEOJkWA-+ z`Z52*6R=m6Go)&*=LH93t0}KIDOw=!0^os`JmPkulSU3m==a6Q6`a)6TtMA+hu=4X zj_=F6I0XVN#c1Zu+YLYz!F+uv*FvaB2`l$Fhy*p0XyB1tq`!KiROBxELPzk7{G_9i z+{2V?dOmRY%+u-uPH~xJ96qH_YhBtA;V#Vz-&>O8Ln(B}?$$3*Tp7wt_-1YQW;8xa{%kfyDs!2(q0rljk+W)~g!-D|I5%tef<2Y3 z`@Dvmofh*rp<1}HxF<@Wop!>g6ECf`(d}%(*x~C;BZ+szS%e&PMrUXx8;W+2J*}xg)`%w>YwYeF=hjQx!zUTW(?4|jd4PRh$eHGiJA3*K>bd@pp z^EcX!;wvhw1bim(>UxZG%z^{{6I%^=gNF=M!~D}63-0MZg&U3D4lRFNH@4kTv9?ct zn}Jv6b}xqsG4{!VkmFHi={GCMbla1U6U-l$X~UcWEBgL2QE@~@Jd1?^g6lh@XG{*m zDo#%uFkg4 zePkwH_DAwbWn_0oMaFfpn~iqI;^p?{Q9HIc`>1E)2OQY}Z%An?^Z1>f_7LIo;Gt); zpAee-GN-*$zMkyTb(f%6Z%xqZ(@f3y#Ay7)4fi*fSC?_w0W&YJmYcNuM!BLG-BqQG zR4dHSrRe4>ekU-w7++)c{;+G*u)C9l}{i} zXvxnnC$JAQ*8Qf-EMSX${G|@r$v{fv)`q_rN?r?L0`B#tc&~ngw z?N$$MOw;W=;S=|1%F(LY>BI%~T+r#lyVlwHqmsf#R5<`6_ho)8L8GlH~1~izFihOf*qak0;PR zpT+XHvQ}E9jw`y~>#j}(#yT}Cx?i%H9FUd8P8zLuDvr)Pq9VWhBsleSTNs@`#eWN&OD|2fkag)Kx=}7F4{gPvmG`*l&7Os5j8FO%&U|_96>6URjwjDdGmrg$pQnswr}&eq`5PUgD~cZpP5j)9wkSJ$>c&~7 zWr^8XtTst{$}J@*3|7o9ym3VzM@|3U_dWymsHEaFpzaHa*O)#u7Jq?RvPAPjp5!(isOXZ{fRoF%qgJ3 z5rr`VV?yQD!`C~S%pAz*ZnyAZBUD3OzH8~6H8+x!z}JX*ht{k7Vw z!W4ZR?3;+YSj;QLWF5m~88Yj1YU8?kSjoShJZDE{kHdas8xEd*<{`=^v?nNsfOh=B>!J~Ul~@_*8Qu5 zAc%q}4I-g}2+~rr73od^3F#7$ZghhJN+U>zbc1wnknR+uLApzF6L&7qb6(GR|L6JN z5BGWQhvNs_wdR^@%rVCt`5V5r@tU3CBwiM3p^-^i8LgRfffruyB}ErY>7v6i(MMi+ zYvXxMa>gxF#O-4J;lsx(qTDM$*Q{a`M_B9UTObAw(D6#fovv=|e>tkcZ=Pec^LTF| z9n0kOY7qC*W+#Q6rtnr(0&!f~$Yh)4`a5R6hDX$qZwBP5q~y7dhMr~$tWL1=O~lB7 z@HTT0>*B?wh>A?0{Co-7sP!qHL0)9?;JL@xI0`2cpL3?de%O5UNS%h$#=!mEGzpR` za)2RtAxF(%{Q9*KZoB2)yv+=M^0{xEe%#xSxhA7V){LLJ@C(jXka8pC8Gw^PHM0LH z|6Evdw?>N0k}I4jiT(y{H1arhsz(;pmth#WtBmX@J+Y$Gi1#TZr;i)uzA1DUO}h>= zNE^55F%%KV?Vak=1rY(XNc#G+l%p_ioAg6ajCwQUDZb? zZ_1K#j7eBBdASbr`latxo&!btAG&-9XIWlOz=TI_5XdN#>{20+!mqCpRM5z6v*;x? zrCnwuXk8WpYK0k%Y%Ir1fLgZ+@-QCQdPWE}S_GN6!!8kGMiN7w6(O73zRfJ)QGy00 z0qa^lJo3*x;NH#8P$|c<~Lo>FKxHB zJ-HOD9AcFM-W5g&c}?R(#X|JtmaE~P10gUyH-31qk>UX zZTeZx=QC0`*@!P>tP?(JhW>?zm%NND(qe>ox^w1qF|?x`=2GM6jCspO?FMM32oHGF z5nXmEtDM-%#nwfcn!JKV3`ascM@3&p+Nn`wA;TyMxLWhEZv-Sn+Po{fH;caIn*_+7P7Y9v_4tcu+(}yLkV-$Y6|E9_ctJb(j;1>IuG4rRdMg9ZNP;G% zhsN+^|8C~O4Vz(#uI$|vOD8i9tmuaphf}3fSdOk01gy9^>rrIHY$mo9X81*FEZT2q zzoVhaNrB@%cqynL3l~mgG;9;7cnGp_up_m;(8`}1EWCtiLyUYP|a1vz`RYehw111@fWq8pXQ1eI=B5U8(g zEYn>bDfPRAuh;q!M|b4D^tUpb3^n`7x1!Qc={4-!zVG=k1>6Ndiuo)||4M#Uv$OTE zvHD{@ri)LdQJBA6wBU_KXTwc!n7WmxWdM)Uf1zsHMv6?g8v7Ra;TtI57sUrhiO~-s z83Dls$4+Ho^ur6dWVeSnM%-5;9BRzd#&V;neYfgrPJ+~*DWR*!6JIcnb&es^OXOyh zEpxlZQMA&kp~JZJym*yaHFZ@KX3v$(oz3^Itd%&rCnA~lPq5TGnf+(7GK19=6oW%2}$~-Zrfs}v79+W6z0Ahna@E&9AUK* zE!9)9DZoF%GkwgWjDbGq63p|~?PwZteAv6OE6Un-?fz_7F;f!#*N?0(!kvDYPtG+k z^$Xm?0N`0k&%3W$_N(i-9eWIQ_F8hD@CtJKt9-%wCvTFC4TD@cA&&aXi!7&>yarP@DH`A+MzM>1lc;qRi<0eNBg_8t? zeBzUQs=b`7P^l|W$jQz%2)QDM$fxf&V}{|d>q)E-j3x^1I%Uz@>j`9-NVpG-aNxuY za4DU~%Pv_WT4g~PS$|QF9G_Z+T|&maO4M`Gvrt}PWq(&bV#@#OHh<@F)8}DeovTp_ z$u#ik7zeE-gh%Ke+lQaXuJ5k+i4Ag$44(X89olr5(%bUS*UiBTv!9=ErIRU14Ng}U zCn>1x!BfU##I0R(SaPjm++RdhVdG%tzg&^>HLWK{P=0~gII~SHzdt$fngMF4syaaiKYGW}#DKlO>W#9m9pTez(=Etnh4cFnNKjymy*!EzA?F65C8~pN7^6i`+pwDxAIUK(>^2@8`O9^6@iNEfn7}?&? zVEMA?DC3YxbBP*}X(`ofIQI0$C_+e50kFQjm@tnaE~9S|4=#1kwT(HPs71`jBs{4Z z=E-hx=RbS$w7^|zP&amtEpwc3suLfRtmsz6B;eG&e70yiszfNd#aOK3sl<5jc(!Wy zbs_nRn5Yi!eC9L$T*hIOLrdqV4eU)r!j(CK6Gvs>DdJvE;wCCz_kOBd?{k8UWsB{^ zd|Nl^HPSp(lOtUQ_W5$%O9T0DRSu;R#Fh@J!@>ppAJ+=;M(?+Lh<6#cxI$z;y6!IF z^eJ>rebj|dtBE$xX-jO}WEo>LwtSXka8Sun*Pz#AB$kQIyYnfvU)bBDX{99%L?VnU6D3-5E%0`7-Demipr?ZTJ5kv@Zhg+BWU}xnn7Z=w}f2(unI#v@1wlV?()|RYyF0$$!@CtW+ z=I_C=E8phIJTJ(Asq0q#now92In|00!D4KoC9Ho5IIBN-8VTIO26k!1*fv%jCSl@) z;zpG91)EV<&xG8bsDB7HD z*MD{dxQFJCe(A6_OD}u#RX~gt3hZ5S#BO|ms^2EgZX&;nfp}>zMedmJ;@L*DgIJf& z*Qp`VrB+MCl+P`Eu}@SDM;E5`9fvDTA|>pDX~GyN@yH-1Qq&(fMdZwTKEA*M0{59C_wfo1F7^zY zfxGKo4G+mcnaZ2H;0*R=EEC}{A0wOmT9X1I64Fqi$kJ>5&@LACdl~vW_ft-~UVAdC zmrc_pxGUOIJOvI=TF#M|?olp9R@r)7xzWdQi635V29Y2+qI2)mG+ z3+06^mnWzK`<0$iN{aP7$bJy)^=J+f%}G-6PGv+HgwnP)4~EFS@S1bBDvgKJcX88{ zij!;%k;zGU$WWYq+metdS$8rke>>(`EGDfsVl&1A!JWJu{$NdC_(^4XQLARd2^Jxr zT8l#3-S)DP#ij@#lDb$%5QbMmw`<9jgS^s2wMX?$?I31H9Aa~_zeBdpJ^_{pby9&- z7bQ@UAsg=DkBEr6C9dqb2yq$U52c%Y9msrhsQxI!5cyh&rPuKG!S-_1cuGUX3etCf zI6-9u(IXk+L$t~W$v@U_3EMnC=pn^iCuetbhUq4bk!i8aw|H$H zDdfZ|1D6esG;-HOu2f$~SPyYDZK1+9fZ4UT6rl5ph7@qW@3d=o4E_Sr@3M2O^@5J2 zZlyd~aoGAueV)^japS{Xp~xkTkMxC+D&^Z&6Mck^LcGy|`e-H&$4C9!WqB_bW%d`m z2eMqa_gs3eyC>?qbM!w{zsT+@S{^ARQg7V&pySA4axQ4A`NQViF`wU2;EOldfLMMj zeZqd`1HG~%{`q4c>Zd#(?9@9^gY*&{skNCfCGjP4ObVhC=NJ~9l9OA8T{U$EjKN3c zZsYQ!@`-{$feF@y7c6Hb&AIJUAO~2e>&myeLfNh`6p8V*cEPpX=4#gIPJT81H$0iI zqYQO}(tA_g9gHWKgJ3>#ivAxvI1xJPDdEXg`|CG_*Y`K?q{x5965j#=xSk5sG*>0a z*A_BL5h{f5g-6)mN2kClY1nWP84uhfy7jr5OBw^POE^vULqQG+EW31;q+5b)*0S|p zF@*iu)*jawX;vo6Sh4=t1d+Ve&AmE~7#|892!Ymuj}2DnztSH+dBTIm*%ERI_pJmVyMcjn-+2pD9f}@W53W!291E{g9_I@R;{c}CT z=7@WqmjzFfURMI!c$p;AAma6S9}N`jFz=0#fYfgXK9UTW&s6!35Ri$W%|7i(Y~H$y zZ;2X{g^HIMoUJO|QL%5jB!Tl~?=YMq2DO=6Wb|lqK?{|cn-n3#0Gw}%vx$T`AETEl zS%()y-(9Zr5{@h+|4c?^?08%$w^nBcycpedSgsM=-(#~TJay5uOHgsO1LcPJfKCGP ztw~lyFzO__zY8mTuv#V3Gb1@E8ls(|STHno$|_+sEMe`iSDz|mQat7LC`pi8GzULE zsOV@W6v^-C&lvP5X5&{F!2$R^3=UWtChJSvsY>is1Xl{iaBV zn9SDw5m^OZ6z2D#Sybw)WD9%jP7LzH+%TvUGCs1;ObG&%~Z` zsS-FJ6f6%`jmh$ldd)bQ%1knUn{*xTt{HD`2*$=Z+2bMlWL?iKm#pfNX5j31SEC|$ z%FjFbQV8t_EBBjZ7^bAYot5e&QBlazB#HyQR;ho&yix?t z9LoJ2kqA{Ey9@S>B+R*^0y$XZITmq$5r zskZkGWj>CU9m9OUpj&~EbWHcOiODYEKUpf^1b#u|Kx_bGn#zrf`jvrkam+3MuKL8QycDlp%7 zlRLz7e4>me--Cd|Y3GFlz*?FP)pVow$chxqz3LCU)XrwHJ8JjLE|%B9@_D*HfD%zb zY7;MOUti3ZE3-B#1*xEdAb-#Mf|oNWrKxh6sJcZlJk%KxSW8iou-m$Y&# z+qa5oaj9?lTnmav;SO+GHIhUvMA~j^`KfN|eRg-pIkWtyZA3Fsm`4!0eN6kek9Ie6 z5ph3^OL?+#$C+wdCnf^C20fJuxB;TRGgdbhrU`L9`e z<4s$q)t9?$S%$MrTbc};&?4x*ujrMb8q3{0^*9ca0mX}O1YxUrbfmDqGwRe{%S$yu z7w29Qy>>uj_;PehS7$DQmUTL^c-5NBPZ%UtR7ZbpwCKep%^xrCw0g+}Y=WC+(K(|a zY!!H|#Hx{d|9iMMb&D}xJKtlR)N+1oLAgfYW1k=<)6sDH>D+8<_oCP(Ax>-{4o@5r zr-2hW`VJ^qamVqmyd7B+9Mzk%L)M8?Ftf1jVHgdq;TbOyBDrSEE;v}1S-dGF?w(?y zj-xZsHRCYcqS`f}w|Z3~jFU{XC1Svp%XD~Ueq^xwu;n=QB+ZZEfp7WfO&nEjch<-1 z2#c@0jfi{8b8ioxeVRLqSGOLGjkrc1BNsk+4hjB<$kz_g7VG*G)T?b1nEBcu`!EFboO&mv0>Qy`zd+~C5(I$Of+>DP51mV14CdAI zj&8}eQgB%&&M}Yf16-sQlbpBGA+G?!Q_D;!#;ZZ#jErrL7xaFTa<2Qlld1a{0Hun6 zyCewB99xR54HOvl!bq*v3CZ`0i=EadYVttI8R81b2?C(4$$ACikWc%@i`FX@ZPRwMCJ8|*1i*@w40K)Pl^o7K@enQrJSL)7r#Adq8ij69r z1kI`+gh#>~?J>0L653TalrLW{?WHr>w`$;}%c$Ca@|4{B>1&A!ROas?&=D0d&Nigs zuK4L;%6yIk&*-Kf^;GM3quQB9(H!oIQPR~%TZZy=!>v}kPV|XTxQc5SmH9nM?-QGf zZPZ34+fe6iI0Ff(<9kJ8m)gft%~*bSdnqhpW2ZkTLALg9v~xtyj??igR9jmhn#X|>2- zW@R30MK~in__8ZsnfDv^tEleSQ!Z}_j(Z#Ult{WVv*7OCi{Q#C3j8|v#>x!mS#l*= zPG=BEb28)t*_-+j-aTC!iOxse=Tvyc_g&Ja-Z;OgW5pH7P;3a>OsGd(5G%zhq)wli zWyjV#xI2Izy#(@_dad7LQ+E3hpOD&mFW*nT?W5Z*p{?$diSJ3SbbonaDu%nW>3v=^ zw{*(fqW!Svqm!assXYVQ^);_hN79-J0lu{UZwKFO997Yu`1t(XIp)ivPaY|dQ5p4v+GIT0k0|YG zP!XsDL0WIH;eA+1lJaM57U0%T16D_geAW~I#0>;sjmpDin*c=<-A#my$A#lZ49Ux@ zE0z!-w8LEVoWmd}M5hC=X1Q~p?N=@Vcg(`kY)3R{SPxCl)|RD431{|8J!6>vg`F^O zo~^f~{5_2T5$-mhxJaVS3@<Xq*G);uO0&t(yhaiA z3cCRgI<Hb5+!x{LoduPxGCtls? zSxy<1mA&4iuSundFT0UNUl?Rk#*OS}x8r*?>|K)YclI{kFhDSQH{uknVP)DeC&%Y% z@~rYVOk{7w%!Bj*r3hdZIkZsBD%z&Ha3_5!-E}2c%q`SBD$aREW{E|x^duH!al|s+ zwyhl97}MidN4@j$6_0Aw$+82|tkLn^=GUb9delViZI4GYhW959^fX&5{3!^c^{ILU z`3=}vlBLrut;L09!Uwh;{XhH1k}J5f0iR&B2SaUYMY)!+hI*#1NBqcFTn`@LzeBQyNbM%e*N|B` zN~g&b!V@Hp+VYLIQTBIE4q9)N*PigyY3Sgv@$a|Xc6ZZSo=dH1!#i?gNjLN;JXpyI z0@XaLss0XGQjhYUx3O+6mEEZ0yB_7>2O=CX`00q`%ayW4S$iD}w=yU$)-|QCCVwWT z!ZqUsk@-st^Jr}DFdsC^l$Lv{MK9<`V7NxPwE%?vHULz;{LH2OI4=D5h_}AVdW+@c z!_JZZ1$Jx4E72`9L4vHh)~h3f7RK=?%p`1vxGcS%QpoSSlP$87M5ef2czi$y03mC^ zO%PYGyjUmVdLo00c8CV{r6V~A`8sx!-&HDev#z{dR+A4UM1(Cp2;wO$o86D&imp}P zZLTGEyYO)o72&Ek4{HtFmfGOe>#Oda={#~mX(TB?sFK(n7SB1XMss(Wb*|#A0U`d1 zcZh5Wb}2`K(2U1w*fi|SX$91%uodjxR8&k2qXGeR?KgL&{ZAe&o#<}top|uEM>xF^ z!!t*S(kFNOZ7=Qrs9F=*ZxJ4jB>!4AK9Nfh{2tgmMg~--xm87te2ztAY!oWEUP=?| zo5EtvSnNs9C$$?WQ@k!a-nj49x^tM8@~3X%JvB2_NiCwx)OV6!cGo7op45z!39O#W8Gki;2OzTh z8XAKwGZ<&N|@x!e?su>*WETXaE) z>#NVnao&)e7^XNE52mX{WbOH0FEAbEXjc`m!E3q<#efrH@w>%9h~ZPzUL?Sp1-?F3%Ye&1obeiL}<~t+QJ|JAb8Tm{^FYqRIu`$ z;fS_o4A$lGFrpRe!~~Q%R?gaoAWfYuK#+R*9mQ??QSqf7Q0Vvuq?#+5^d!DJ+(k?x za(@^KJs$v1eGls0lv>L?_zJXV5nCIp+*0coXR4%uitTM&0MPj~2t*VWC8!$>bcB7l zAA2(KZYL(nALe?-@l8351Jt;hK&@I!{t8mLG6oupQB)KhfqKK1WPZfzD4JkfNu_l3 zJxK$u88XP^#g?@mGm2;$SlbPS$_QLb3(c<7bulTB+ElVHk93kg?Fd0d?uxbR3@Ojh zP~lQjnEKgJdbrK!e0Zku5;2#9CVBse#UTxktt(N$%%kczch=~mtK6xTE=O0>3BI?_r%DSG;sHm(i%WHeTWe2~#M*D^a|-`Rb)S{jSi;{9 zmdCr^A$p35L=5tJB=oM-HO+gJlcdCI4UzOlC}i6D*yw^{kMIaVjwEbYE z9S!~xPJ||__n7H+G7iY^Y~Z3@i|?+OX$=hpg`=XM9={U)vNn#;1Vyd40Im#s{LT@z z0e<(L=)6P#Vje%p^8Om#Rm_*)3QagV3)Sg zqVcPYLS6mXzB@{|V{qI%5wd*fD&ek#)vtN=Yj^^10PCIWVGJh4f;aW*IXipZULSg=)|5=J|h21b97O1;$B2b|wPGVOs zpU!D`uUno5iavE`NC8uHh<{a zSZDOZ8W(kP$$?j^>iw(Nxni3uuH1llw7WVB^6;n^4TmY|e!-tp9uCYbsI*8`NOx7E zNom<>Z`J!6#oS;mjqkgR-h7gMGhWQGmCwEx%@|F z)y1x^6k8&~I>xy!78Ut+-B~960sZX8O>rvumC_>BV*taq1wf8J9(z|a62L|V%3+`i z$PoSnGpVyY3DLyY7d#>0Siem20W~XMhG#_#fnO&yK;vdX6j#(zV77JR!;_PfUXL$< z#VuHt6HeO3Zmjj~tEBb$(ew6wNU(s=&=|iDyn+vr`N4&!*rWYR5~6|0_DKlLcmm-Q zJcG1b!AxX$Xqd<{AoxmADNp+zHSM}4$ws$P=FyB@gp!Yh(N=Bv?e{2Hc3Bt!|$I`v+z_n18D(V)Uzd-+zyQF6rn3&JT)ikOzxZM|33Plu=ItJa)#F)mC4>QKxt8@iNy6?ky0!-ZuSSAL}7j*5!^!y9a z0YW^APM(fu0B69E_7-;g+B3NM)oYm7pH_P= z*_9K`On_2|ssOZd4~Oz-^tTOIo`FwTwoC0)FJ5C%mWkjrX9U2`$=1u?m{V7Qu~3?+ zUe002&jS#{;K~>GaHheYzpK~sh!S+J&d~%(L^@Jwpka78G`}8Z(AJ{IP`KYJ>g`V? z@iZFTrA~#kJ`Ou|09hr^v=i(00uaJ@a7FGy8FcC8a%Fj-($RWXrc@XE z$3m|#W91LIapQ`;@7|yTLUZ_BN3k~9lXGw%{4V&2m9z zZaTUFS_RDWXvcWaH0d$46U9kPW1@cI;RU#&G+LJ^8ME`Wrti6JbT%j7&{rM~FNm+n z1a)(KT{Jiq-wJgSmcOlkcyZub_TsSHa7)uuF1ZvE58)<3q4g0Ud&YrY1`jR%D)7gE zAkYp7XVqeO3Sg>%Mrb|dcIMJvP~Hs4m!|@!>?UuA6vTo3DAJY4kSOcv8*pvmdyEXE zwgkCmk03-PEEGCAz%pXC*ddS4ofn{t1cadV<&c*6iGc^B3_-V6E&f6)29#;LCeSJ^ zq(xEEaTJur&348z0kuKgkvt8s1sVvI;=TA1QvsUc1#)s`w_Jq-KcprI4U-zKfJR_? z5bOfz8UXFJASsAzArC+adzoF)dn~cC*<) zjihANM9@9T+CWHhCw1?bUljhc+hWh7YV-R*uv*VQS`5nqn?{`-cx#`<5_cCz!hGi2 zOHfX+v*C%DKU^$|qFx=st1C6b6#HZ`P?*WcUQh;Dg2^VTh05FnD2tEJ_ZS*zrS$6l zLG*W^fjp4mG@o@!*ip2A=3njzO7yhOalHWK)soGH4sLM17-OsL?G4CfwS9uoaxNbt zpC;@3UW72To=~!*|JWuq)-n!~VOc%}xcsAGGN7&tP_wLn6dHF;nhj@~*d6-XkM9+@ z-w9VP41oD4i^~0kv9=&sCv5@_zmC0Y{;-FTV*pKi8b7r4Pdc1!Kaodg%OaOsn|c-H z1jsns?Q1B^_UTWcob*JwD1bT(%{S<$L1Q`h(h2R$nJC0`Z`Atr+`qcR^vIl1mu*=l zJ&GL#wDSnLDd6$u1rwa;rqP25!27l6UjX|+fQEl}#erY(ivN@gzedDu;stx0B}dvw z))uNjBR`7rQUVodn0l)qp9`AXCA3px%j^7oK}uXqQZ+9k^16qn!iatA?0*mUIdgZg~ND_J;rNWgp4&Mmn zZ0e`;hweRuS&*$FpgNFx(_Oz^N)< z7ed5`+I>NtM8Sd(+EP8GgS3ECTz*qMbk-I$&W3 zVL2DHAWE>zSG!n$d<1%jVPg*iL^OCC{&Pyst%G0!J^yn#%3 zag4rm@Sl}&jN6NnltPQ!7ukU{0)te`1M2N2AfBfxJU!e7RpT;&gC;cw=~;gy)u094 z1_a(*W?kukLZJRV6sH9Cfs)5`%0GJO!2#>3bT$7%cm0zpk>CrCOGw*9z&Mz>^BnNV~L9tcq07J_c@)Z0N?lzn9$Q#`%uCJzoVKrY!mo0MPgi%N5f%0 z5=*i}r!9E`KBMfwdR&Noagfxe`&W}NV6FG{IKnJEzvT@;* z5)=7|zNjfYDk`ck`?;I3Fr|=NT>bBfS44i3x}5S@zX z|M&w2ta3rB!rCL)^ZU2TLI0i)hXj))4?o&iU279ky4V)W&!4=a4o(({R#t4*rL;~Y z)NyQN#lFw#V0&rqTjK?kPmsfxw$}BGymAtN@2)N3rn|%5pt!<`B9%HriE}h=Xo*I7 z{95rp$h|-{K?u$~)3_MW^86Z*aSJsc5n!&z56OP+d_8Cd!n5P3FrPq*o`PDG&tFD? z2|>~gZ&ZXD7hn;3O-?+g_5nj^y?$`^OISK41S0{`YZN8Pf9P!xd@K6SvxX`<68YXn zj7nH_gnMWpEM`1aovE`Z5U=*bTd?bXL4lvWN~{Fq8I8XMDASaHXWT@-O#)rmX$+inmmj(gA}PDlytC(Kk*5*Bf{CFrHrJbG0iCXK2@zIbrCD z4lWD^ppjzG|MKh$3^M6*E3j=lQRUl=>r&lp7Hx~CL!SQrC5}6 zr)!qz4U2rp#B*#B$+(>icnVh)Ja`ue*(tp4pXz{!I{f9hJT1>1o#lE$5%2U6y`LKL z;$x50aQ)tO)k74{3KYJAO9+#S)tah#^i5KqT!2zCZ+_5ueZP{k2Hhcf?lE{epaFjL z!vDzzIOmU&YMQhCSr++%eY*d%=tFzy&Ofba8MY#j{n8Eg^ij}gIVMhT1zeR)glA##Us zQ@mzTYfWGJ3h|Dv6gY;$u;)W1HCp{Yj0XVSZ;QL`;qMG{9!*Y_atLbXm|-b5e|DK3(2^iC>g zqCG|z4x3rZv92IMR9{5sXy|>u@*e``l+&yUO4c`Ww$xqRST-d^&Y#|mjsa|EEW68! z0%yk_BW@>1Rd(qyA?4>W1YQ}|_}|kO6W2QZ`XaOl@NXkne|gH^-ZrtFZx8r%QqO%y zAeumulmN4d@(y)x40{0v_*v5nu%B7VUm21A_2l`_=>*(A-yZZyyuytILY~kvofJCz zc3KgvQbmi!KVJMACIYtyrO>^hxWRZqF{r1PibCaJ&E(Z9f0a#t?hL>@5`xcLCSJ93dM=<4Dl-A1^H+=!)|J^)sR&s_oiCmvLI|qq3+DWZq;Q$n-VZ zzjh}7VTnMBJ)y66_|J#^t7tff!h9W!CA8vLU(m*K_jYT7r+ENHLc>)bJb4wYQG5_EX!)c1en}M-mAG zLjGdH>`yBH0H+P7MK&7|^^vS*xS@ zfcEz;>t__Ck(~I_h>#kpWIbx|PR98_`$41ihknoLo`+w9tme1e?&)VwFmXHX5s`q$ zrHfHLz=lM~bN&5H20ABbnXO%yQwt8V*n706o6R2Bx|1tkG|nW0oK!W1S2sXxYHn^8 zE#;QG`B(7*>B1Ek3l_HW6wj}80(kl}pA(WMLO8mrf80e0o2W$xm@t{?5(WAfb2si^_*MzqYj~agdf->7V_X%%? zS90CX&~)EI>ddE(IQoBLe%LMSI+k;_m#So#@-%ff^F4|n9dQ9th!3`j+0S_6uNvl0 zw(k+5{8&RcWXKBLw!i^PMi#p561Z(s8DmP#Ki<|Ey=?-!QXFjFO&3--rawgW|4!m5 zoX4r^n)!BlppIehaVyedlT?B)9+%K$$|g)Aj@cMdyh{{`m&!&V6~gCzpcEg z!&C)RRmfs+pPR<@(el(HKOIdA@Hvf71nFNoEd0Lr7ntJC6G5u|2M=gfGP@x6qZ05r z2--h6)piPi;CM$c=lYLRB02S2?5Df%x-l(Zr^2nxuHlBHKa2(92oLVv)3JW1$Jv<$ zeb%Eu32gI?%T>AZz@QpQKR}WP zsP_HtDsA>ln*H|Fub=%oIY-J-hktG+5&Lz)iH1YawGkpL9b`CTS)(YP&5rUaI12$# z4B3B~F8m2*i92)KGU2-p=RmI_%b$8lJY-6#GI#u(ttdo=i!sChO*EoigwkSLlrqd$ z#*0lNmq&6XcGiyThZ;eXAZRj!khA7L^;RcJAyDtpn|-YCH&f|9C)jiW%sTH=s1fz) z8a815SRRyzyn~=7gt+$>)F$pXy8QN5HwZ9Hcwb}z`rnr4&xQK))lZOvQm%g(@=uQa z|6uSYOSqG2Gzo6lPdlfT-SD*k&i{_palO2&p&iz;!fea{{84zxG7#nAFTpau!T4!e5j0Bi!Kc0f+$^xR-R~{_)^O;zJHy1VA@3J}vxw}mNwhruF8nTF#o3*E z_s;a;zi-_?IRcs=!H+D|1jK^HJ}qei8&ZPx7Wqnf~cg(7O?GUeMisdwL*F%L4uE zmnHQ-&Gj{G@!|-8Xfd+URa`izY3j=$J9A_7?KGKNhA8+|<+`rhKzcKsY#pJ(( z|5pP2OO}7h@*fVI|4NKMmBN3y<-Zc>U$Xp5mVYY;FwCK$IVEqeqVn{SiVF8rcd(7UlQj*^!*}ue38X3aqJQaa`h#-H_m?j# zg_)PyQZM#+nQf16H`PjBsB9CjXotP319S>~RAx6F&AT^u(-XTpL$}V-_*~Mz zAxsl|bEdO^`R%96l36@&PTXsJ`d)md?(zHn8K2A!K7|B2HvyW*{8u9HUZBC8{@Kr1 zpZrv%hRcHQ>J#o`N)e|kz^3C?oZsxvjw*4T`}Q2Fx^qS*IjCQQ3p?#97FH1;ET(<( z;&iyXbpJuO`!2Y>J^s}9@|T)C>Q~qV(#1gC z#|by?C@)8`xvv|R+bA(u@Nx3pzG9*BHuQCU%(;^a@+xOL-drv{>zR@j%yFiI^Bd## zwzAvF%+6n}74Hq~pl_1C>#WR4TNoPKmoUVp9m~IW;KiH??`hEqul)6qJ73A^+0BO^ zUKw1nXxUAxbA8>Nlq0A&9UWzGUZ4|C`^%u|_6?=ds7p3)LoF}uTl;RStbdoZJuwt> zG9ZgvKtacpt0cZXX6DS?ORtsJ9j~r(1n$?p&8}bB3Q=CGx0DYszn6AzF0_!FTTg+x z;H`V^rTscqMXCF3smethx0Lirn@!RB7UE)cXdr}HOEQIpjwVAfb}hn;MwEz zO0*!Eq*Ik4S*Wt?Gp4QnFOM+1Rpy;I%0MT6=h`Fe^;a!-pB~$P_4dqKSPR`xzwNBE zYi41F+r3Aue)=(5Uw8kk{dQgH?$6hQ=Z9}jx>ny`IVScp=>FX2AFsD(zwiE`!%?=?SsbFB~aM(&;RHzkXkqDgk)>=R9OQk+Mk_RH%hPK|lkapLz*JUNm6O6?Wl zmGBqhqn8$|PYfgJOr9zlJPY68atr|}Yf8z{P(l`;vJN&g1s`2LId$7j@$xWJrQn0{ zzT%#szWvctfpzRB6)P{W@(^P8S|t^ubZWG{*!5ztJ1sjuJv6yWr5x+#=C(?>in~MS zRx;%)UJofRUGYAN4CeUGM#CW+B4Ob?BzlX})Z~d3%<-mCiD2fH!s}aVjoGpSaH}S5 zxOQ1Ti?7*{pp*>`CWDU;`oF*ID4~_P5O{9Ovo@M=H+K2g^ske_8`Nt-P}I8^sMOi? zTGH6ABF&1l=-szc;z>YjE@_8mRa#4|AQG5^em z5b5%eb+;qSpV_xUxzEz)D9JsfzhJ&;=$anhtaJN9$I-m3D}qO1&3@%4oakhmIbCV) z1m{j%yvryYa@v(+>@}=_y^&^bf@A9g_wf_i*Y4cs5>*yF=l_k)oV)4a+4CXauC+d7 zi+y+e+zGJ@$x(6F1)|A6nLp7FN8Pzv5Y70I&z;@(?Z&mTm?t;i6lWO~o^yB|d5`tx z1tztr-V=7bGIT=s)O&d5jjGvj5!l><9`$*zYDQ?t=@&HGKRj=1cO0>NAt-oKy;P*2d)}jke{eZ(&*sr;uhw0 z;a=v+xZ!=C>jJ~W8?g$l8Ph87uem*>`}Dj|rq8ACMBk<7QVXH6m9g()!=INwe;@m_ z$e`$+ePW+mp1|YoIE9ax)&wgN_lwDD8Cs@~$5mZ;&3GXn6}9i rijEXR{77FC(> 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 Date: Mon, 21 Oct 2024 12:44:20 +1100 Subject: [PATCH 42/54] Update lockbud to work with bindgen/etc --- .github/workflows/test-suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 7cda3e477d6..6b327483f3b 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -63,8 +63,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Install dependencies - run: apt update && apt install -y cmake - - name: Generate code coverage + run: apt update && apt install -y cmake clang-5.0 + - name: Check for deadlocks run: | cargo lockbud -k deadlock -b -l tokio_util From a69ce786ef10435a5ffb7e9ea978230a1f6f5a53 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 21 Oct 2024 13:09:32 +1100 Subject: [PATCH 43/54] Correct pkg name for Debian --- .github/workflows/test-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 6b327483f3b..5babb50c854 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -63,7 +63,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Install dependencies - run: apt update && apt install -y cmake clang-5.0 + run: apt update && apt install -y cmake libclang-dev - name: Check for deadlocks run: | cargo lockbud -k deadlock -b -l tokio_util From 237de37e1d80a9aa3d58035d99e2a73d66783be8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 28 Oct 2024 11:19:31 +1100 Subject: [PATCH 44/54] Remove vestigial epochs_per_state_diff --- beacon_node/store/src/config.rs | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 03beda514d1..61080bb93bd 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -7,7 +7,7 @@ use std::io::Write; use std::num::NonZeroUsize; use superstruct::superstruct; use types::non_zero_usize::new_non_zero_usize; -use types::{EthSpec, Unsigned}; +use types::EthSpec; use zstd::Encoder; // Only used in tests. Mainnet sets a higher default on the CLI. @@ -24,8 +24,6 @@ pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; /// Database configuration parameters. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StoreConfig { - /// 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. @@ -91,10 +89,6 @@ pub enum StoreConfigError { config: OnDiskStoreConfig, on_disk: OnDiskStoreConfig, }, - InvalidEpochsPerStateDiff { - epochs_per_state_diff: u64, - max_supported: u64, - }, ZeroEpochsPerBlobPrune, InvalidVersionByte(Option), } @@ -102,7 +96,6 @@ pub enum StoreConfigError { impl Default for StoreConfig { fn default() -> Self { Self { - epochs_per_state_diff: DEFAULT_EPOCHS_PER_STATE_DIFF, block_cache_size: DEFAULT_BLOCK_CACHE_SIZE, state_cache_size: DEFAULT_STATE_CACHE_SIZE, historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE, @@ -152,8 +145,7 @@ impl StoreConfig { /// 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::() + self.verify_epochs_per_blob_prune() } /// Check that the compression level is valid. @@ -167,21 +159,6 @@ impl StoreConfig { } } - /// 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> { From 5b43b998869617a0d2cb0d10be27e3b7049dd43b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 28 Oct 2024 12:41:12 +1100 Subject: [PATCH 45/54] Markdown lint --- book/src/advanced_database.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 6ce2c1db2f2..95710560beb 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -87,15 +87,15 @@ lighthouse beacon_node --historic-state-cache-size 4 ## Glossary -* _Freezer DB_: part of the database storing finalized states. States are stored in a sparser +- _Freezer DB_: part of the database storing finalized states. States are stored in a sparser format, and usually less frequently than in the hot DB. -* _Cold DB_: see _Freezer DB_. -* _HDiff_: hierarchical state diff. -* _Hierarchy Exponents_: configuration for hierarchical state diffs, which determines the density +- _Cold DB_: see _Freezer DB_. +- _HDiff_: hierarchical state diff. +- _Hierarchy Exponents_: configuration for hierarchical state diffs, which determines the density of stored diffs and snapshots in the freezer DB. -* _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full +- _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full states are stored every epoch. -* _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Yearly by default. -* _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states +- _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Yearly by default. +- _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states from slots less than the split slot are in the freezer, while all states with slots greater than or equal to the split slot are in the hot DB. From c4d6ef3544b529bbbc8a919916165e6e976eb1fb Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 31 Oct 2024 12:12:16 +1100 Subject: [PATCH 46/54] Address Jimmy's review comments --- beacon_node/http_api/src/lib.rs | 2 +- beacon_node/network/src/sync/backfill_sync/mod.rs | 5 ++--- beacon_node/store/src/config.rs | 2 ++ beacon_node/store/src/errors.rs | 8 -------- book/src/advanced_database.md | 3 ++- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index c4ecdccf997..fe05f55a01a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2717,7 +2717,7 @@ pub fn serve( debug!( log, "HTTP state load"; - "load_time_ms" => t.elapsed().as_millis(), + "total_time_ms" => t.elapsed().as_millis(), "target_slot" => state.slot() ); diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 8031f843dce..5703ed35046 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -158,11 +158,8 @@ impl BackFillSync { log: slog::Logger, ) -> Self { // Determine if backfill is enabled or not. - // Get the anchor info, if this returns None, then backfill is not required for this - // running instance. // If, for some reason a backfill has already been completed (or we've used a trusted // genesis root) then backfill has been completed. - let anchor_info = beacon_chain.store.get_anchor_info(); let (state, current_start) = if anchor_info.block_backfill_complete(beacon_chain.genesis_backfill_slot) { @@ -251,6 +248,8 @@ impl BackFillSync { // Obtain a new start slot, from the beacon chain and handle possible errors. if let Err(e) = self.reset_start_epoch() { + // This infallible match exists to force us to update this code if a future + // refactor of `ResetEpochError` adds a variant. let ResetEpochError::SyncCompleted = e; error!(self.log, "Backfill sync completed whilst in failed status"); self.set_state(BackFillState::Completed); diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 61080bb93bd..4f675305706 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -171,6 +171,8 @@ impl StoreConfig { /// Estimate the size of `len` bytes after compression at the current compression level. pub fn estimate_compressed_size(&self, len: usize) -> usize { + // This is a rough estimate, but for our data it seems that all non-zero compression levels + // provide a similar compression ratio. if self.compression_level == 0 { len } else { diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 3bc44701b64..6bb4edee6b2 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -45,18 +45,11 @@ pub enum Error { expected: Hash256, computed: Hash256, }, - MissingStateRoot(Slot), - MissingState(Hash256), MissingGenesisState, MissingSnapshot(Slot), - NoBaseStateFound(Hash256), BlockReplayError(BlockReplayError), MilhouseError(milhouse::Error), Compression(std::io::Error), - MissingPersistedBeaconChain, - SlotIsBeforeSplit { - slot: Slot, - }, FinalizedStateDecreasingSlot, FinalizedStateUnaligned, StateForCacheHasPendingUpdates { @@ -64,7 +57,6 @@ pub enum Error { slot: Slot, }, AddPayloadLogicError, - SlotClockUnavailableForMigration, InvalidKey, InvalidBytes, InconsistentFork(InconsistentFork), diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 95710560beb..d8d6ea61a18 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -95,7 +95,8 @@ lighthouse beacon_node --historic-state-cache-size 4 of stored diffs and snapshots in the freezer DB. - _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full states are stored every epoch. -- _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Yearly by default. +- _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Approximately yearly by + default (every ~291 days). - _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states from slots less than the split slot are in the freezer, while all states with slots greater than or equal to the split slot are in the hot DB. From f6412f32b9df557dbe73d8aa430fc5b80da1c22a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 31 Oct 2024 14:28:08 +1100 Subject: [PATCH 47/54] Simplify ReplayFrom case --- beacon_node/store/src/hot_cold_store.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 376d521aeeb..fb44ce5c7a8 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1796,13 +1796,9 @@ impl, Cold: ItemStore> HotColdDB Ok(state) } StorageStrategy::ReplayFrom(from) => { - let base_state = if let Some(state) = cached_state { - // Found a prior state in the historic state cache. - state - } else { - // No prior state found, need to load by diffing. - self.load_cold_state_by_slot(from)? - }; + // No prior state found in cache (above), need to load by diffing and then + // replaying. + let base_state = self.load_cold_state_by_slot(from)?; self.load_cold_state_by_slot_using_replay(base_state, slot) } } From 6c327330845a5a6b82d2ceb4a752a63a65a75de8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 31 Oct 2024 14:37:13 +1100 Subject: [PATCH 48/54] Fix and document genesis_state_root --- common/eth2_network_config/src/lib.rs | 36 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index f0db7995364..5d5a50574b1 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -154,23 +154,29 @@ impl Eth2NetworkConfig { } } + /// Get the genesis state root for this network. + /// + /// `Ok(None)` will be returned if the genesis state is not known. No network requests will be + /// made by this function. This function will not error unless the genesis state configuration + /// is corrupted. pub fn genesis_state_root(&self) -> Result, String> { - if let GenesisStateSource::Url { - genesis_state_root, .. - } = self.genesis_state_source - { - Hash256::from_str(genesis_state_root) + match self.genesis_state_source { + GenesisStateSource::Unknown => Ok(None), + GenesisStateSource::Url { + genesis_state_root, .. + } => Hash256::from_str(genesis_state_root) .map(Option::Some) - .map_err(|e| format!("Unable to parse genesis state root: {:?}", e)) - } else { - self.get_genesis_state_from_bytes::() - .and_then(|mut state| { - Ok(Some( - state - .canonical_root() - .map_err(|e| format!("Hashing error: {e:?}"))?, - )) - }) + .map_err(|e| format!("Unable to parse genesis state root: {:?}", e)), + GenesisStateSource::IncludedBytes => { + self.get_genesis_state_from_bytes::() + .and_then(|mut state| { + Ok(Some( + state + .canonical_root() + .map_err(|e| format!("Hashing error: {e:?}"))?, + )) + }) + } } } From f8a80dc1f4d9cde5dd4218d705296a0dd48a3323 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 31 Oct 2024 15:17:40 +1100 Subject: [PATCH 49/54] Typo Co-authored-by: Jimmy Chen --- beacon_node/store/src/hdiff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 49b29de28ea..489c29cfa42 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -39,7 +39,7 @@ pub struct HierarchyConfig { /// - Layer 1: 3000003 - (3000003 mod 2^13) = 2998272 /// - Layer 2: 3000003 - (3000003 mod 2^5) = 3000000 /// - /// Layer 0 is full state snaphost, apply layer 1 diff, then apply layer 2 diff and then replay + /// Layer 0 is full state snapshot, apply layer 1 diff, then apply layer 2 diff and then replay /// blocks 3,000,001 to 3,000,003. pub exponents: Vec, } From b14a9241e358335fe99d63cae493352330338d51 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:19:01 +0700 Subject: [PATCH 50/54] Compute diff of validators list manually (#6556) * Split hdiff computation * Dedicated logic for historical roots and summaries * Benchmark against real states * Mutated source? * Version the hdiff * Add lighthouse DB config for hierarchy exponents * Tidy up hierarchy exponents flag * Apply suggestions from code review Co-authored-by: Michael Sproul * Address PR review * Remove hardcoded paths in benchmarks * Delete unused function in benches * lint --------- Co-authored-by: Michael Sproul --- Cargo.lock | 2 + beacon_node/store/Cargo.toml | 6 + beacon_node/store/benches/hdiff.rs | 116 ++++++ beacon_node/store/src/hdiff.rs | 413 ++++++++++++++++++++-- beacon_node/store/src/hot_cold_store.rs | 4 +- consensus/types/src/historical_summary.rs | 1 + consensus/types/src/validator.rs | 1 + database_manager/src/cli.rs | 12 +- database_manager/src/lib.rs | 1 + 9 files changed, 518 insertions(+), 38 deletions(-) create mode 100644 beacon_node/store/benches/hdiff.rs diff --git a/Cargo.lock b/Cargo.lock index a06229e41f5..5774b3627ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8282,6 +8282,7 @@ version = "0.2.0" dependencies = [ "beacon_chain", "bls", + "criterion", "db-key", "directory", "ethereum_ssz", @@ -8292,6 +8293,7 @@ dependencies = [ "lru", "metrics", "parking_lot 0.12.3", + "rand 0.8.5", "safe_arith", "serde", "slog", diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 61cde61916b..cd7379b11db 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -7,6 +7,8 @@ edition = { workspace = true } [dev-dependencies] tempfile = { workspace = true } beacon_chain = { workspace = true } +criterion = { workspace = true } +rand = { workspace = true } [dependencies] db-key = "0.0.5" @@ -31,3 +33,7 @@ zstd = { workspace = true } bls = { workspace = true } smallvec = { workspace = true } logging = { workspace = true } + +[[bench]] +name = "hdiff" +harness = false diff --git a/beacon_node/store/benches/hdiff.rs b/beacon_node/store/benches/hdiff.rs new file mode 100644 index 00000000000..2577f03f664 --- /dev/null +++ b/beacon_node/store/benches/hdiff.rs @@ -0,0 +1,116 @@ +use bls::PublicKeyBytes; +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::Rng; +use ssz::Decode; +use store::{ + hdiff::{HDiff, HDiffBuffer}, + StoreConfig, +}; +use types::{BeaconState, Epoch, Eth1Data, EthSpec, MainnetEthSpec as E, Validator}; + +pub fn all_benches(c: &mut Criterion) { + let spec = E::default_spec(); + let genesis_time = 0; + let eth1_data = Eth1Data::default(); + let mut rng = rand::thread_rng(); + let validator_mutations = 1000; + let validator_additions = 100; + + for n in [1_000_000, 1_500_000, 2_000_000] { + let mut source_state = BeaconState::::new(genesis_time, eth1_data.clone(), &spec); + + for _ in 0..n { + append_validator(&mut source_state, &mut rng); + } + + let mut target_state = source_state.clone(); + // Change all balances + for i in 0..n { + let balance = target_state.balances_mut().get_mut(i).unwrap(); + *balance += rng.gen_range(1..=1_000_000); + } + // And some validator records + for _ in 0..validator_mutations { + let index = rng.gen_range(1..n); + // TODO: Only change a few things, and not the pubkey + *target_state.validators_mut().get_mut(index).unwrap() = rand_validator(&mut rng); + } + for _ in 0..validator_additions { + append_validator(&mut target_state, &mut rng); + } + + bench_against_states( + c, + source_state, + target_state, + &format!("n={n} v_mut={validator_mutations} v_add={validator_additions}"), + ); + } +} + +fn bench_against_states( + c: &mut Criterion, + source_state: BeaconState, + target_state: BeaconState, + id: &str, +) { + let slot_diff = target_state.slot() - source_state.slot(); + let config = StoreConfig::default(); + let source = HDiffBuffer::from_state(source_state); + let target = HDiffBuffer::from_state(target_state); + let diff = HDiff::compute(&source, &target, &config).unwrap(); + println!( + "state slot diff {slot_diff} - diff size {id} {}", + diff.size() + ); + + c.bench_function(&format!("compute hdiff {id}"), |b| { + b.iter(|| { + HDiff::compute(&source, &target, &config).unwrap(); + }) + }); + c.bench_function(&format!("apply hdiff {id}"), |b| { + b.iter(|| { + let mut source = source.clone(); + diff.apply(&mut source, &config).unwrap(); + }) + }); +} + +fn rand_validator(mut rng: impl Rng) -> Validator { + let mut pubkey = [0u8; 48]; + rng.fill_bytes(&mut pubkey); + let withdrawal_credentials: [u8; 32] = rng.gen(); + + Validator { + pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), + withdrawal_credentials: withdrawal_credentials.into(), + slashed: false, + effective_balance: 32_000_000_000, + activation_eligibility_epoch: Epoch::max_value(), + activation_epoch: Epoch::max_value(), + exit_epoch: Epoch::max_value(), + withdrawable_epoch: Epoch::max_value(), + } +} + +fn append_validator(state: &mut BeaconState, mut rng: impl Rng) { + state + .balances_mut() + .push(32_000_000_000 + rng.gen_range(1..=1_000_000_000)) + .unwrap(); + if let Ok(inactivity_scores) = state.inactivity_scores_mut() { + inactivity_scores.push(0).unwrap(); + } + state + .validators_mut() + .push(rand_validator(&mut rng)) + .unwrap(); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = all_benches +} +criterion_main!(benches); diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 489c29cfa42..3347cb6ebea 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -1,19 +1,26 @@ //! Hierarchical diff implementation. use crate::{metrics, DBColumn, StoreConfig, StoreItem}; +use bls::PublicKeyBytes; use itertools::Itertools; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use std::cmp::Ordering; use std::io::{Read, Write}; use std::ops::RangeInclusive; use std::str::FromStr; -use types::{BeaconState, ChainSpec, EthSpec, List, Slot}; +use std::sync::LazyLock; +use superstruct::superstruct; +use types::historical_summary::HistoricalSummary; +use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, List, Slot, Validator}; use zstd::{Decoder, Encoder}; +static EMPTY_PUBKEY: LazyLock = LazyLock::new(PublicKeyBytes::empty); + #[derive(Debug)] pub enum Error { InvalidHierarchy, - U64DiffDeletionsNotSupported, + DiffDeletionsNotSupported, UnableToComputeDiff, UnableToApplyDiff, BalancesIncompleteChunk, @@ -64,6 +71,12 @@ impl FromStr for HierarchyConfig { } } +impl std::fmt::Display for HierarchyConfig { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.exponents.iter().join(",")) + } +} + #[derive(Debug)] pub struct HierarchyModuli { moduli: Vec, @@ -81,6 +94,10 @@ pub enum StorageStrategy { pub struct HDiffBuffer { state: Vec, balances: Vec, + inactivity_scores: Vec, + validators: Vec, + historical_roots: Vec, + historical_summaries: Vec, } /// Hierarchical state diff. @@ -97,10 +114,32 @@ pub struct HDiffBuffer { /// this strategy the HDiff code is easily mantainable across forks, as new fields are covered /// automatically. xdelta3 algorithm showed diff compute and apply times of ~200 ms on a mainnet /// state from Apr 2023 (570k indexes), and a 92kB diff size. +#[superstruct(variants(V0), variant_attributes(derive(Debug, Encode, Decode)))] #[derive(Debug, Encode, Decode)] +#[ssz(enum_behaviour = "union")] pub struct HDiff { state_diff: BytesDiff, balances_diff: CompressedU64Diff, + /// inactivity_scores are small integers that change slowly epoch to epoch. And are 0 for all + /// participants unless there's non-finality. Computing the diff and compressing the result is + /// much faster than running them through a binary patch algorithm. In the default case where + /// all values are 0 it should also result in a tiny output. + inactivity_scores_diff: CompressedU64Diff, + /// The validators array represents the vast majority of data in a BeaconState. Due to its big + /// size we have seen the performance of xdelta3 degrade. Comparing each entry of the + /// validators array manually significantly speeds up the computation of the diff (+10x faster) + /// and result in the same minimal diff. As the `Validator` record is unlikely to change, + /// maintaining this extra complexity should be okay. + validators_diff: ValidatorsDiff, + /// `historical_roots` is an unbounded forever growing (after Capella it's + /// historical_summaries) list of unique roots. This data is pure entropy so there's no point + /// in compressing it. As it's an append only list, the optimal diff + compression is just the + /// list of new entries. The size of `historical_roots` and `historical_summaries` in + /// non-trivial ~10 MB so throwing it to xdelta3 adds CPU cycles. With a bit of extra complexity + /// we can save those completely. + historical_roots: AppendOnlyDiff, + /// See historical_roots + historical_summaries: AppendOnlyDiff, } #[derive(Debug, Encode, Decode)] @@ -113,30 +152,89 @@ pub struct CompressedU64Diff { bytes: Vec, } +#[derive(Debug, Encode, Decode)] +pub struct ValidatorsDiff { + bytes: Vec, +} + +#[derive(Debug, Encode, Decode)] +pub struct AppendOnlyDiff { + values: Vec, +} + impl HDiffBuffer { pub fn from_state(mut beacon_state: BeaconState) -> Self { let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_FROM_STATE_TIME); // Set state.balances to empty list, and then serialize state as ssz let balances_list = std::mem::take(beacon_state.balances_mut()); + let inactivity_scores = if let Ok(inactivity_scores) = beacon_state.inactivity_scores_mut() + { + std::mem::take(inactivity_scores).to_vec() + } else { + // If this state is pre-altair consider the list empty. If the target state + // is post altair, all its items will show up in the diff as is. + vec![] + }; + let validators = std::mem::take(beacon_state.validators_mut()).to_vec(); + let historical_roots = std::mem::take(beacon_state.historical_roots_mut()).to_vec(); + let historical_summaries = + if let Ok(historical_summaries) = beacon_state.historical_summaries_mut() { + std::mem::take(historical_summaries).to_vec() + } else { + // If this state is pre-capella consider the list empty. The diff will + // include all items in the target state. If both states are + // pre-capella the diff will be empty. + vec![] + }; let state = beacon_state.as_ssz_bytes(); let balances = balances_list.to_vec(); - HDiffBuffer { state, balances } + HDiffBuffer { + state, + balances, + inactivity_scores, + validators, + historical_roots, + historical_summaries, + } } pub fn as_state(&self, spec: &ChainSpec) -> Result, Error> { let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME); let mut state = BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSszState)?; + *state.balances_mut() = List::try_from_iter(self.balances.iter().copied()) .map_err(|_| Error::InvalidBalancesLength)?; + + if let Ok(inactivity_scores) = state.inactivity_scores_mut() { + *inactivity_scores = List::try_from_iter(self.inactivity_scores.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + } + + *state.validators_mut() = List::try_from_iter(self.validators.iter().cloned()) + .map_err(|_| Error::InvalidBalancesLength)?; + + *state.historical_roots_mut() = List::try_from_iter(self.historical_roots.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + + if let Ok(historical_summaries) = state.historical_summaries_mut() { + *historical_summaries = List::try_from_iter(self.historical_summaries.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + } + Ok(state) } /// Byte size of this instance pub fn size(&self) -> usize { - self.state.len() + self.balances.len() * std::mem::size_of::() + self.state.len() + + self.balances.len() * std::mem::size_of::() + + self.inactivity_scores.len() * std::mem::size_of::() + + self.validators.len() * std::mem::size_of::() + + self.historical_roots.len() * std::mem::size_of::() + + self.historical_summaries.len() * std::mem::size_of::() } } @@ -148,27 +246,57 @@ impl HDiff { ) -> Result { let state_diff = BytesDiff::compute(&source.state, &target.state)?; let balances_diff = CompressedU64Diff::compute(&source.balances, &target.balances, config)?; - - Ok(Self { + let inactivity_scores_diff = CompressedU64Diff::compute( + &source.inactivity_scores, + &target.inactivity_scores, + config, + )?; + let validators_diff = + ValidatorsDiff::compute(&source.validators, &target.validators, config)?; + let historical_roots = + AppendOnlyDiff::compute(&source.historical_roots, &target.historical_roots)?; + let historical_summaries = + AppendOnlyDiff::compute(&source.historical_summaries, &target.historical_summaries)?; + + Ok(HDiff::V0(HDiffV0 { state_diff, balances_diff, - }) + inactivity_scores_diff, + validators_diff, + historical_roots, + historical_summaries, + })) } pub fn apply(&self, source: &mut HDiffBuffer, config: &StoreConfig) -> Result<(), Error> { let source_state = std::mem::take(&mut source.state); - self.state_diff.apply(&source_state, &mut source.state)?; + self.state_diff().apply(&source_state, &mut source.state)?; + self.balances_diff().apply(&mut source.balances, config)?; + self.inactivity_scores_diff() + .apply(&mut source.inactivity_scores, config)?; + self.validators_diff() + .apply(&mut source.validators, config)?; + self.historical_roots().apply(&mut source.historical_roots); + self.historical_summaries() + .apply(&mut source.historical_summaries); - self.balances_diff.apply(&mut source.balances, config)?; Ok(()) } - pub fn state_diff_len(&self) -> usize { - self.state_diff.bytes.len() + /// Byte size of this instance + pub fn size(&self) -> usize { + self.sizes().iter().sum() } - pub fn balances_diff_len(&self) -> usize { - self.balances_diff.bytes.len() + pub fn sizes(&self) -> Vec { + vec![ + self.state_diff().size(), + self.balances_diff().size(), + self.inactivity_scores_diff().size(), + self.validators_diff().size(), + self.historical_roots().size(), + self.historical_summaries().size(), + ] } } @@ -205,12 +333,17 @@ impl BytesDiff { *target = xdelta3::decode(&self.bytes, source).ok_or(Error::UnableToApplyDiff)?; Ok(()) } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } } impl CompressedU64Diff { pub fn compute(xs: &[u64], ys: &[u64], config: &StoreConfig) -> Result { if xs.len() > ys.len() { - return Err(Error::U64DiffDeletionsNotSupported); + return Err(Error::DiffDeletionsNotSupported); } let uncompressed_bytes: Vec = ys @@ -223,29 +356,14 @@ impl CompressedU64Diff { }) .collect(); - let compression_level = config.compression_level; - let mut compressed_bytes = - Vec::with_capacity(config.estimate_compressed_size(uncompressed_bytes.len())); - 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, + bytes: compress_bytes(&uncompressed_bytes, config)?, }) } pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { // Decompress balances diff. - let mut balances_diff_bytes = - Vec::with_capacity(config.estimate_decompressed_size(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)?; + let balances_diff_bytes = uncompress_bytes(&self.bytes, config)?; for (i, diff_bytes) in balances_diff_bytes .chunks(u64::BITS as usize / 8) @@ -265,6 +383,202 @@ impl CompressedU64Diff { Ok(()) } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } +} + +fn compress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { + let compression_level = config.compression_level; + let mut out = Vec::with_capacity(config.estimate_compressed_size(input.len())); + let mut encoder = Encoder::new(&mut out, compression_level).map_err(Error::Compression)?; + encoder.write_all(input).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + Ok(out) +} + +fn uncompress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { + let mut out = Vec::with_capacity(config.estimate_decompressed_size(input.len())); + let mut decoder = Decoder::new(input).map_err(Error::Compression)?; + decoder.read_to_end(&mut out).map_err(Error::Compression)?; + Ok(out) +} + +impl ValidatorsDiff { + pub fn compute( + xs: &[Validator], + ys: &[Validator], + config: &StoreConfig, + ) -> Result { + if xs.len() > ys.len() { + return Err(Error::DiffDeletionsNotSupported); + } + + let uncompressed_bytes = ys + .iter() + .enumerate() + .filter_map(|(i, y)| { + let validator_diff = if let Some(x) = xs.get(i) { + if y == x { + return None; + } else { + let pubkey_changed = y.pubkey != x.pubkey; + // Note: If researchers attempt to change the Validator container, go quickly to + // All Core Devs and push hard to add another List in the BeaconState instead. + Validator { + // The pubkey can be changed on index re-use + pubkey: if pubkey_changed { + y.pubkey + } else { + PublicKeyBytes::empty() + }, + // withdrawal_credentials can be set to zero initially but can never be + // changed INTO zero. On index re-use it can be set to zero, but in that + // case the pubkey will also change. + withdrawal_credentials: if pubkey_changed + || y.withdrawal_credentials != x.withdrawal_credentials + { + y.withdrawal_credentials + } else { + Hash256::ZERO + }, + // effective_balance can increase and decrease + effective_balance: y.effective_balance - x.effective_balance, + // slashed can only change from false into true. In an index re-use it can + // switch back to false, but in that case the pubkey will also change. + slashed: y.slashed, + // activation_eligibility_epoch can never be zero under any case. It's + // set to either FAR_FUTURE_EPOCH or get_current_epoch(state) + 1 + activation_eligibility_epoch: if y.activation_eligibility_epoch + != x.activation_eligibility_epoch + { + y.activation_eligibility_epoch + } else { + Epoch::new(0) + }, + // activation_epoch can never be zero under any case. It's + // set to either FAR_FUTURE_EPOCH or epoch + 1 + MAX_SEED_LOOKAHEAD + activation_epoch: if y.activation_epoch != x.activation_epoch { + y.activation_epoch + } else { + Epoch::new(0) + }, + // exit_epoch can never be zero under any case. It's set to either + // FAR_FUTURE_EPOCH or > epoch + 1 + MAX_SEED_LOOKAHEAD + exit_epoch: if y.exit_epoch != x.exit_epoch { + y.exit_epoch + } else { + Epoch::new(0) + }, + // withdrawable_epoch can never be zero under any case. It's set to + // either FAR_FUTURE_EPOCH or > epoch + 1 + MAX_SEED_LOOKAHEAD + withdrawable_epoch: if y.withdrawable_epoch != x.withdrawable_epoch { + y.withdrawable_epoch + } else { + Epoch::new(0) + }, + } + } + } else { + y.clone() + }; + + Some(ValidatorDiffEntry { + index: i as u64, + validator_diff, + }) + }) + .flat_map(|v_diff| v_diff.as_ssz_bytes()) + .collect::>(); + + Ok(Self { + bytes: compress_bytes(&uncompressed_bytes, config)?, + }) + } + + pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { + let validator_diff_bytes = uncompress_bytes(&self.bytes, config)?; + + for diff_bytes in + validator_diff_bytes.chunks(::ssz_fixed_len()) + { + let ValidatorDiffEntry { + index, + validator_diff: diff, + } = ValidatorDiffEntry::from_ssz_bytes(diff_bytes) + .map_err(|_| Error::BalancesIncompleteChunk)?; + + if let Some(x) = xs.get_mut(index as usize) { + // Note: a pubkey change implies index re-use. In that case over-write + // withdrawal_credentials and slashed inconditionally as their default values + // are valid values. + let pubkey_changed = diff.pubkey != *EMPTY_PUBKEY; + if pubkey_changed { + x.pubkey = diff.pubkey; + } + if pubkey_changed || diff.withdrawal_credentials != Hash256::ZERO { + x.withdrawal_credentials = diff.withdrawal_credentials; + } + if diff.effective_balance != 0 { + x.effective_balance = x.effective_balance.wrapping_add(diff.effective_balance); + } + if pubkey_changed || diff.slashed { + x.slashed = diff.slashed; + } + if diff.activation_eligibility_epoch != Epoch::new(0) { + x.activation_eligibility_epoch = diff.activation_eligibility_epoch; + } + if diff.activation_epoch != Epoch::new(0) { + x.activation_epoch = diff.activation_epoch; + } + if diff.exit_epoch != Epoch::new(0) { + x.exit_epoch = diff.exit_epoch; + } + if diff.withdrawable_epoch != Epoch::new(0) { + x.withdrawable_epoch = diff.withdrawable_epoch; + } + } else { + xs.push(diff) + } + } + + Ok(()) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } +} + +#[derive(Debug, Encode, Decode)] +struct ValidatorDiffEntry { + index: u64, + validator_diff: Validator, +} + +impl AppendOnlyDiff { + pub fn compute(xs: &[T], ys: &[T]) -> Result { + match xs.len().cmp(&ys.len()) { + Ordering::Less => Ok(Self { + values: ys.iter().skip(xs.len()).copied().collect(), + }), + // Don't even create an iterator for this common case + Ordering::Equal => Ok(Self { values: vec![] }), + Ordering::Greater => Err(Error::DiffDeletionsNotSupported), + } + } + + pub fn apply(&self, xs: &mut Vec) { + xs.extend(self.values.iter().copied()); + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.values.len() * size_of::() + } } impl Default for HierarchyConfig { @@ -390,6 +704,7 @@ impl StorageStrategy { #[cfg(test)] mod tests { use super::*; + use rand::{thread_rng, Rng}; #[test] fn default_storage_strategy() { @@ -486,4 +801,40 @@ mod tests { // U64 diff wins by more than a factor of 3 assert!(u64_diff.bytes.len() < 3 * bytes_diff.bytes.len()); } + + #[test] + fn compressed_validators_diff() { + assert_eq!(::ssz_fixed_len(), 129); + + let mut rng = thread_rng(); + let config = &StoreConfig::default(); + let xs = (0..10) + .map(|_| rand_validator(&mut rng)) + .collect::>(); + let mut ys = xs.clone(); + ys[5] = rand_validator(&mut rng); + ys.push(rand_validator(&mut rng)); + let diff = ValidatorsDiff::compute(&xs, &ys, config).unwrap(); + + let mut xs_out = xs.clone(); + diff.apply(&mut xs_out, config).unwrap(); + assert_eq!(xs_out, ys); + } + + fn rand_validator(mut rng: impl Rng) -> Validator { + let mut pubkey = [0u8; 48]; + rng.fill_bytes(&mut pubkey); + let withdrawal_credentials: [u8; 32] = rng.gen(); + + Validator { + pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), + withdrawal_credentials: withdrawal_credentials.into(), + slashed: false, + effective_balance: 32_000_000_000, + activation_eligibility_epoch: Epoch::max_value(), + activation_epoch: Epoch::max_value(), + exit_epoch: Epoch::max_value(), + withdrawable_epoch: Epoch::max_value(), + } + } } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index fb44ce5c7a8..4942b148810 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -385,8 +385,8 @@ impl HotColdDB, LevelDB> { info!( db.log, "Updating historic state config"; - "previous_config" => ?hierarchy_config, - "new_config" => ?db.config.hierarchy_config, + "previous_config" => %hierarchy_config, + "new_config" => %db.config.hierarchy_config, ); } } diff --git a/consensus/types/src/historical_summary.rs b/consensus/types/src/historical_summary.rs index 76bb111ea2f..8c82d52b810 100644 --- a/consensus/types/src/historical_summary.rs +++ b/consensus/types/src/historical_summary.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; #[derive( Debug, PartialEq, + Eq, Serialize, Deserialize, Encode, diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 8cf118eea59..275101ddbe1 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; Debug, Clone, PartialEq, + Eq, Serialize, Deserialize, Encode, diff --git a/database_manager/src/cli.rs b/database_manager/src/cli.rs index 5521b97805f..4246a51f899 100644 --- a/database_manager/src/cli.rs +++ b/database_manager/src/cli.rs @@ -3,6 +3,7 @@ use clap_utils::get_color_style; use clap_utils::FLAG_HEADER; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use store::hdiff::HierarchyConfig; use crate::InspectTarget; @@ -21,13 +22,14 @@ use crate::InspectTarget; pub struct DatabaseManager { #[clap( long, - 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)]", + global = true, + value_name = "N0,N1,N2,...", + help = "Specifies the frequency for storing full state snapshots and hierarchical \ + diffs in the freezer DB.", + default_value_t = HierarchyConfig::default(), display_order = 0 )] - pub slots_per_restore_point: Option, + pub hierarchy_exponents: HierarchyConfig, #[clap( long, diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 869605263f1..fc15e98616b 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -39,6 +39,7 @@ fn parse_client_config( .blobs_db_path .clone_from(&database_manager_config.blobs_dir); client_config.store.blob_prune_margin_epochs = database_manager_config.blob_prune_margin_epochs; + client_config.store.hierarchy_config = database_manager_config.hierarchy_exponents.clone(); Ok(client_config) } From dc2b665246d926a259249adc9571026765f28e32 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Sun, 17 Nov 2024 08:03:21 +1100 Subject: [PATCH 51/54] Test hdiff binary format stability (#6585) --- beacon_node/store/Cargo.toml | 2 +- beacon_node/store/src/hdiff.rs | 92 ++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index cd7379b11db..7cee16c3535 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } tempfile = { workspace = true } beacon_chain = { workspace = true } criterion = { workspace = true } -rand = { workspace = true } +rand = { workspace = true, features = ["small_rng"] } [dependencies] db-key = "0.0.5" diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 3347cb6ebea..a29e680eb51 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -114,8 +114,11 @@ pub struct HDiffBuffer { /// this strategy the HDiff code is easily mantainable across forks, as new fields are covered /// automatically. xdelta3 algorithm showed diff compute and apply times of ~200 ms on a mainnet /// state from Apr 2023 (570k indexes), and a 92kB diff size. -#[superstruct(variants(V0), variant_attributes(derive(Debug, Encode, Decode)))] -#[derive(Debug, Encode, Decode)] +#[superstruct( + variants(V0), + variant_attributes(derive(Debug, PartialEq, Encode, Decode)) +)] +#[derive(Debug, PartialEq, Encode, Decode)] #[ssz(enum_behaviour = "union")] pub struct HDiff { state_diff: BytesDiff, @@ -142,22 +145,22 @@ pub struct HDiff { historical_summaries: AppendOnlyDiff, } -#[derive(Debug, Encode, Decode)] +#[derive(Debug, PartialEq, Encode, Decode)] pub struct BytesDiff { bytes: Vec, } -#[derive(Debug, Encode, Decode)] +#[derive(Debug, PartialEq, Encode, Decode)] pub struct CompressedU64Diff { bytes: Vec, } -#[derive(Debug, Encode, Decode)] +#[derive(Debug, PartialEq, Encode, Decode)] pub struct ValidatorsDiff { bytes: Vec, } -#[derive(Debug, Encode, Decode)] +#[derive(Debug, PartialEq, Encode, Decode)] pub struct AppendOnlyDiff { values: Vec, } @@ -320,8 +323,9 @@ impl BytesDiff { } pub fn compute_xdelta(source_bytes: &[u8], target_bytes: &[u8]) -> Result { - let bytes = - xdelta3::encode(target_bytes, source_bytes).ok_or(Error::UnableToComputeDiff)?; + let bytes = xdelta3::encode(target_bytes, source_bytes) + .ok_or(Error::UnableToComputeDiff) + .unwrap(); Ok(Self { bytes }) } @@ -704,7 +708,7 @@ impl StorageStrategy { #[cfg(test)] mod tests { use super::*; - use rand::{thread_rng, Rng}; + use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; #[test] fn default_storage_strategy() { @@ -837,4 +841,74 @@ mod tests { withdrawable_epoch: Epoch::max_value(), } } + + // This test checks that the hdiff algorithm doesn't accidentally change between releases. + // If it does, we need to ensure appropriate backwards compatibility measures are implemented + // before this test is updated. + #[test] + fn hdiff_version_stability() { + let mut rng = SmallRng::seed_from_u64(0xffeeccdd00aa); + + let pre_balances = vec![32_000_000_000, 16_000_000_000, 0]; + let post_balances = vec![31_000_000_000, 17_000_000, 0, 0]; + + let pre_inactivity_scores = vec![1, 1, 1]; + let post_inactivity_scores = vec![0, 0, 0, 1]; + + let pre_validators = (0..3).map(|_| rand_validator(&mut rng)).collect::>(); + let post_validators = pre_validators.clone(); + + let pre_historical_roots = vec![Hash256::repeat_byte(0xff)]; + let post_historical_roots = vec![Hash256::repeat_byte(0xff), Hash256::repeat_byte(0xee)]; + + let pre_historical_summaries = vec![HistoricalSummary::default()]; + let post_historical_summaries = pre_historical_summaries.clone(); + + let pre_buffer = HDiffBuffer { + state: vec![0, 1, 2, 3, 3, 2, 1, 0], + balances: pre_balances, + inactivity_scores: pre_inactivity_scores, + validators: pre_validators, + historical_roots: pre_historical_roots, + historical_summaries: pre_historical_summaries, + }; + let post_buffer = HDiffBuffer { + state: vec![0, 1, 3, 2, 2, 3, 1, 1], + balances: post_balances, + inactivity_scores: post_inactivity_scores, + validators: post_validators, + historical_roots: post_historical_roots, + historical_summaries: post_historical_summaries, + }; + + let config = StoreConfig::default(); + let hdiff = HDiff::compute(&pre_buffer, &post_buffer, &config).unwrap(); + let hdiff_ssz = hdiff.as_ssz_bytes(); + + // First byte should match enum version. + assert_eq!(hdiff_ssz[0], 0); + + // Should roundtrip. + assert_eq!(HDiff::from_ssz_bytes(&hdiff_ssz).unwrap(), hdiff); + + // Should roundtrip as V0 with enum selector stripped. + assert_eq!( + HDiff::V0(HDiffV0::from_ssz_bytes(&hdiff_ssz[1..]).unwrap()), + hdiff + ); + + assert_eq!( + hdiff_ssz, + vec![ + 0u8, 24, 0, 0, 0, 49, 0, 0, 0, 85, 0, 0, 0, 114, 0, 0, 0, 127, 0, 0, 0, 163, 0, 0, + 0, 4, 0, 0, 0, 214, 195, 196, 0, 0, 0, 14, 8, 0, 8, 1, 0, 0, 1, 3, 2, 2, 3, 1, 1, + 9, 4, 0, 0, 0, 40, 181, 47, 253, 0, 72, 189, 0, 0, 136, 255, 255, 255, 255, 196, + 101, 54, 0, 255, 255, 255, 252, 71, 86, 198, 64, 0, 1, 0, 59, 176, 4, 4, 0, 0, 0, + 40, 181, 47, 253, 0, 72, 133, 0, 0, 80, 255, 255, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 10, + 192, 2, 4, 0, 0, 0, 40, 181, 47, 253, 32, 0, 1, 0, 0, 4, 0, 0, 0, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 4, 0, 0, 0 + ] + ); + } } From 6b6f796c4478419f3f642ae1e824ad3216775d8f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 18 Nov 2024 11:14:53 +1100 Subject: [PATCH 52/54] Add deprecation warning for SPRP --- beacon_node/src/config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 906ba46f70c..adcb591aed9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -446,6 +446,10 @@ pub fn get_config( client_config.store.prune_payloads = prune_payloads; } + if clap_utils::parse_optional::(cli_args, "slots-per-restore-point")?.is_some() { + warn!(log, "The slots-per-restore-point flag is deprecated"); + } + if let Some(hierarchy_config) = clap_utils::parse_optional(cli_args, "hierarchy-exponents")? { client_config.store.hierarchy_config = hierarchy_config; } From 9b4b06987caa530e84f35f85322311ef80cadc9e Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 18 Nov 2024 11:39:08 +1100 Subject: [PATCH 53/54] Update xdelta to get rid of duplicate deps --- Cargo.lock | 339 ++++++++++++++--------------------------------------- Cargo.toml | 2 +- 2 files changed, 88 insertions(+), 253 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d063419ec8..a2014728e96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,7 +46,7 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "filesystem", - "rand 0.8.5", + "rand", "regex", "rpassword", "serde", @@ -222,7 +222,7 @@ dependencies = [ "keccak-asm", "proptest", "proptest-derive", - "rand 0.8.5", + "rand", "ruint", "serde", "tiny-keccak", @@ -461,7 +461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand 0.8.5", + "rand", ] [[package]] @@ -471,7 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand", ] [[package]] @@ -646,15 +646,6 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.3.0", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -807,7 +798,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "proto_array", - "rand 0.8.5", + "rand", "rayon", "safe_arith", "sensitive_url", @@ -914,29 +905,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.66.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.77", - "which", -] - [[package]] name = "bindgen" version = "0.69.4" @@ -949,12 +917,15 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", + "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn 2.0.77", + "which", ] [[package]] @@ -1054,7 +1025,7 @@ dependencies = [ "ethereum_ssz", "fixed_bytes", "hex", - "rand 0.8.5", + "rand", "safe_arith", "serde", "tree_hash", @@ -1084,7 +1055,7 @@ dependencies = [ "ff 0.13.0", "group 0.13.0", "pairing", - "rand_core 0.6.4", + "rand_core", "serde", "subtle", ] @@ -1474,15 +1445,6 @@ 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.51" @@ -1732,7 +1694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -1744,7 +1706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -1756,7 +1718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -2246,7 +2208,7 @@ dependencies = [ "more-asserts", "multiaddr", "parking_lot 0.11.2", - "rand 0.8.5", + "rand", "smallvec", "socket2 0.4.10", "tokio", @@ -2353,7 +2315,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", + "rand_core", "serde", "sha2 0.10.8", "subtle", @@ -2411,7 +2373,7 @@ dependencies = [ "ff 0.12.1", "generic-array", "group 0.12.1", - "rand_core 0.6.4", + "rand_core", "sec1 0.3.0", "subtle", "zeroize", @@ -2431,7 +2393,7 @@ dependencies = [ "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", - "rand_core 0.6.4", + "rand_core", "sec1 0.7.3", "subtle", "zeroize", @@ -2459,7 +2421,7 @@ dependencies = [ "hex", "k256 0.13.4", "log", - "rand 0.8.5", + "rand", "serde", "sha3 0.10.8", "zeroize", @@ -2674,7 +2636,7 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand 0.8.5", + "rand", "scrypt", "serde", "serde_json", @@ -2716,7 +2678,7 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand 0.8.5", + "rand", "serde", "serde_json", "serde_repr", @@ -2947,7 +2909,7 @@ dependencies = [ "k256 0.11.6", "once_cell", "open-fastrlp", - "rand 0.8.5", + "rand", "rlp", "rlp-derive", "serde", @@ -3073,7 +3035,7 @@ dependencies = [ "metrics", "parking_lot 0.12.3", "pretty_reqwest_error", - "rand 0.8.5", + "rand", "reqwest", "sensitive_url", "serde", @@ -3152,7 +3114,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -3163,7 +3125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "bitvec 1.0.1", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -3204,7 +3166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.5", + "rand", "rustc-hex", "static_assertions", ] @@ -3216,7 +3178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand", "rustc-hex", "static_assertions", ] @@ -3296,12 +3258,6 @@ 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" @@ -3570,7 +3526,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "quickcheck", - "rand 0.8.5", + "rand", "regex", "serde", "sha2 0.10.8", @@ -3598,7 +3554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -3609,9 +3565,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", - "rand 0.8.5", - "rand_core 0.6.4", - "rand_xorshift 0.3.0", + "rand", + "rand_core", + "rand_xorshift", "subtle", ] @@ -3796,7 +3752,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand 0.8.5", + "rand", "socket2 0.5.7", "thiserror", "tinyvec", @@ -3818,7 +3774,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "resolv-conf", "smallvec", "thiserror", @@ -3977,7 +3933,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "proto_array", - "rand 0.8.5", + "rand", "safe_arith", "sensitive_url", "serde", @@ -4214,7 +4170,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.30", "log", - "rand 0.8.5", + "rand", "tokio", "url", "xmltree", @@ -4288,7 +4244,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg 1.3.0", + "autocfg", "hashbrown 0.12.3", ] @@ -4314,7 +4270,7 @@ dependencies = [ "lockfile", "metrics", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "reqwest", "serde", "serde_json", @@ -4762,7 +4718,7 @@ dependencies = [ "parking_lot 0.12.3", "pin-project", "quick-protobuf", - "rand 0.8.5", + "rand", "rw-stream-sink", "smallvec", "thiserror", @@ -4825,7 +4781,7 @@ dependencies = [ "multihash", "p256", "quick-protobuf", - "rand 0.8.5", + "rand", "sec1 0.7.3", "sha2 0.10.8", "thiserror", @@ -4847,7 +4803,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.5", + "rand", "smallvec", "socket2 0.5.7", "tokio", @@ -4884,7 +4840,7 @@ dependencies = [ "libp2p-identity", "nohash-hasher", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "smallvec", "tracing", "unsigned-varint 0.8.0", @@ -4906,7 +4862,7 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand 0.8.5", + "rand", "sha2 0.10.8", "snow", "static_assertions", @@ -4947,7 +4903,7 @@ dependencies = [ "libp2p-tls", "parking_lot 0.12.3", "quinn", - "rand 0.8.5", + "rand", "ring 0.17.8", "rustls 0.23.13", "socket2 0.5.7", @@ -4972,7 +4928,7 @@ dependencies = [ "lru", "multistream-select", "once_cell", - "rand 0.8.5", + "rand", "smallvec", "tokio", "tracing", @@ -5082,7 +5038,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.5", + "rand", "serde", "sha2 0.9.9", "typenum", @@ -5214,7 +5170,7 @@ dependencies = [ "prometheus-client", "quickcheck", "quickcheck_macros", - "rand 0.8.5", + "rand", "regex", "serde", "sha2 0.9.9", @@ -5292,7 +5248,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg 1.3.0", + "autocfg", "scopeguard", ] @@ -5425,7 +5381,7 @@ name = "mdbx-sys" version = "0.11.6-4" source = "git+https://github.com/sigp/libmdbx-rs?rev=e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a#e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a" dependencies = [ - "bindgen 0.69.4", + "bindgen", "cc", "cmake", "libc", @@ -5449,7 +5405,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "autocfg 1.3.0", + "autocfg", ] [[package]] @@ -5781,7 +5737,7 @@ dependencies = [ "metrics", "operation_pool", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "serde_json", "slog", "slog-async", @@ -5895,7 +5851,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand", "serde", "smallvec", "zeroize", @@ -5922,7 +5878,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "autocfg 1.3.0", + "autocfg", "num-integer", "num-traits", ] @@ -5933,7 +5889,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg 1.3.0", + "autocfg", "libm", ] @@ -6082,7 +6038,7 @@ dependencies = [ "maplit", "metrics", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "rayon", "serde", "state_processing", @@ -6231,7 +6187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -6262,12 +6218,6 @@ dependencies = [ "sha2 0.10.8", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem" version = "3.0.4" @@ -6475,7 +6425,7 @@ dependencies = [ "hmac 0.12.1", "md-5", "memchr", - "rand 0.8.5", + "rand", "sha2 0.10.8", "stringprep", ] @@ -6661,9 +6611,9 @@ dependencies = [ "bitflags 2.6.0", "lazy_static", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_xorshift 0.3.0", + "rand", + "rand_chacha", + "rand_xorshift", "regex-syntax 0.8.4", "rusty-fork", "tempfile", @@ -6754,7 +6704,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand 0.8.5", + "rand", ] [[package]] @@ -6794,7 +6744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", - "rand 0.8.5", + "rand", "ring 0.17.8", "rustc-hash 2.0.0", "rustls 0.23.13", @@ -6859,25 +6809,6 @@ 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" @@ -6885,18 +6816,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -6906,24 +6827,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[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" @@ -6933,75 +6839,13 @@ 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 0.6.4", + "rand_core", ] [[package]] @@ -7036,15 +6880,6 @@ 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 = "redb" version = "2.1.4" @@ -7312,7 +7147,7 @@ dependencies = [ "parity-scale-codec 3.6.12", "primitive-types 0.12.2", "proptest", - "rand 0.8.5", + "rand", "rlp", "ruint-macro", "serde", @@ -7960,7 +7795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest 0.10.7", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -7970,7 +7805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -8032,7 +7867,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg 1.3.0", + "autocfg", ] [[package]] @@ -8054,7 +7889,7 @@ dependencies = [ "maplit", "metrics", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "rayon", "redb", "safe_arith", @@ -8238,7 +8073,7 @@ dependencies = [ "blake2", "chacha20poly1305", "curve25519-dalek", - "rand_core 0.6.4", + "rand_core", "ring 0.17.8", "rustc_version 0.4.1", "sha2 0.10.8", @@ -8338,7 +8173,7 @@ dependencies = [ "itertools 0.10.5", "merkle_proof", "metrics", - "rand 0.8.5", + "rand", "rayon", "safe_arith", "smallvec", @@ -8383,7 +8218,7 @@ dependencies = [ "lru", "metrics", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "safe_arith", "serde", "slog", @@ -8674,7 +8509,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand 0.8.5", + "rand", "serde", "serde_json", "sha2 0.10.8", @@ -8802,7 +8637,7 @@ dependencies = [ "hmac 0.12.1", "once_cell", "pbkdf2 0.11.0", - "rand 0.8.5", + "rand", "rustc-hash 1.1.0", "sha2 0.10.8", "thiserror", @@ -8912,7 +8747,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.8.5", + "rand", "socket2 0.5.7", "tokio", "tokio-util", @@ -9252,8 +9087,8 @@ dependencies = [ "milhouse", "parking_lot 0.12.3", "paste", - "rand 0.8.5", - "rand_xorshift 0.3.0", + "rand", + "rand_xorshift", "rayon", "regex", "rpds", @@ -9474,7 +9309,7 @@ dependencies = [ "filesystem", "hex", "lockfile", - "rand 0.8.5", + "rand", "tempfile", "tree_hash", "types", @@ -9500,7 +9335,7 @@ dependencies = [ "lighthouse_version", "logging", "parking_lot 0.12.3", - "rand 0.8.5", + "rand", "sensitive_url", "serde", "signing_method", @@ -9845,7 +9680,7 @@ dependencies = [ "logging", "network", "r2d2", - "rand 0.8.5", + "rand", "reqwest", "serde", "serde_json", @@ -10302,7 +10137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", + "rand_core", "serde", "zeroize", ] @@ -10327,15 +10162,15 @@ dependencies = [ [[package]] name = "xdelta3" version = "0.1.5" -source = "git+http://github.com/sigp/xdelta3-rs?rev=2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f#2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f" +source = "git+http://github.com/sigp/xdelta3-rs?rev=50d63cdf1878e5cf3538e9aae5eed34a22c64e4a#50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" dependencies = [ - "bindgen 0.66.1", + "bindgen", "cc", "futures-io", "futures-util", "libc", "log", - "rand 0.6.5", + "rand", ] [[package]] @@ -10375,7 +10210,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand 0.8.5", + "rand", "static_assertions", ] @@ -10390,7 +10225,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.3", "pin-project", - "rand 0.8.5", + "rand", "static_assertions", "web-time", ] diff --git a/Cargo.toml b/Cargo.toml index b9f1c06f446..eedb8a05919 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -263,7 +263,7 @@ validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } validator_store= { path = "validator_client/validator_store" } warp_utils = { path = "common/warp_utils" } -xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "2a06390cd5b61b44ca3eaa89632b4ba3410d3d7f" } +xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" } zstd = "0.13" [profile.maxperf] From b0a5bbefb404723065404af9873ab6a95541b7df Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 18 Nov 2024 12:13:32 +1100 Subject: [PATCH 54/54] Document test --- lighthouse/tests/beacon_node.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 80116feb3db..6e730c007f1 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1819,6 +1819,7 @@ fn validator_monitor_metrics_threshold_custom() { } // Tests for Store flags. +// DEPRECATED but should still be accepted. #[test] fn slots_per_restore_point_flag() { CommandLineTest::new()