diff --git a/Cargo.lock b/Cargo.lock index 39f9b66d99..081a69f774 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "aquamarine" @@ -176,6 +176,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -269,7 +275,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.6.2", + "miniz_oxide", "object", "rustc-demangle", ] @@ -304,6 +310,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bincode" version = "1.3.3" @@ -412,9 +424,29 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", +] [[package]] name = "byteorder" @@ -457,7 +489,7 @@ dependencies = [ "humantime", "lmdb-rkv", "log", - "num-rational 0.4.1", + "num-rational", "num-traits", "once_cell", "rand", @@ -486,7 +518,7 @@ dependencies = [ "gh-1470-regression", "gh-1470-regression-call", "log", - "num-rational 0.4.1", + "num-rational", "num-traits", "once_cell", "parity-wasm 0.41.0", @@ -525,11 +557,11 @@ dependencies = [ "log", "num", "num-derive", - "num-rational 0.4.1", + "num-rational", "num-traits", "num_cpus", "once_cell", - "parity-wasm 0.42.2", + "parity-wasm 0.45.0", "proptest", "rand", "rand_chacha", @@ -537,7 +569,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "strum", + "strum 0.24.1", "tempfile", "thiserror", "tracing", @@ -597,6 +629,7 @@ dependencies = [ "ansi_term", "anyhow", "aquamarine", + "array-init", "assert-json-diff", "assert_matches", "async-trait", @@ -610,14 +643,13 @@ dependencies = [ "casper-json-rpc", "casper-types", "datasize", - "derive_more", + "derive_more 0.99.17", "either", "enum-iterator", "erased-serde", "fake_instant", "fs2", "futures", - "futures-io", "hex-buffer-serde 0.3.0", "hex_fmt", "hostname", @@ -625,13 +657,14 @@ dependencies = [ "humantime", "hyper", "itertools 0.10.5", + "juliet", "libc", "linked-hash-map", "lmdb-rkv", "log", "num", "num-derive", - "num-rational 0.4.1", + "num-rational", "num-traits", "num_cpus", "once_cell", @@ -662,13 +695,12 @@ dependencies = [ "static_assertions", "stats_alloc", "structopt", - "strum", + "strum 0.24.1", "sys-info", "tempfile", "thiserror", "tokio", "tokio-openssl", - "tokio-serde", "tokio-stream", "tokio-util 0.6.10", "toml", @@ -704,7 +736,7 @@ dependencies = [ "num", "num-derive", "num-integer", - "num-rational 0.4.1", + "num-rational", "num-traits", "once_cell", "openssl", @@ -719,7 +751,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_test", - "strum", + "strum 0.24.1", "tempfile", "thiserror", "uint", @@ -744,8 +776,8 @@ dependencies = [ "anyhow", "base16", "casper-types", - "clap 3.2.25", - "derive_more", + "clap 3.2.23", + "derive_more 0.99.17", "hex", "serde", "serde_json", @@ -754,13 +786,13 @@ dependencies = [ [[package]] name = "casper-wasm-utils" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9c4208106e8a95a83ab3cb5f4e800114bfc101df9e7cb8c2160c7e298c6397" +checksum = "b49e4ef1382d48c312809fe8f09d0c7beb434a74f5026c5f12efe384df51ca42" dependencies = [ "byteorder", "log", - "parity-wasm 0.42.2", + "parity-wasm 0.45.0", ] [[package]] @@ -804,13 +836,13 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags 1.3.2", - "clap_derive 3.2.25", + "clap_derive 3.2.18", "clap_lex 0.2.4", "indexmap", "once_cell", @@ -847,9 +879,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.25" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.1", "proc-macro-error", @@ -968,9 +1000,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] @@ -1294,9 +1326,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "datasize" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c88ad90721dc8e2ebe1430ac2f59c5bdcd74478baa68da26f30f33b0fe997f11" +checksum = "e65c07d59e45d77a8bda53458c24a828893a99ac6cdd9c84111e09176ab739a2" dependencies = [ "datasize_derive", "fake_instant", @@ -1307,9 +1339,9 @@ dependencies = [ [[package]] name = "datasize_derive" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0415ec81945214410892a00d4b5dd4566f6263205184248e018a3fe384a61e" +checksum = "613e4ee15899913285b7612004bbd490abd605be7b11d35afada5902fb6b91d5" dependencies = [ "proc-macro2 1.0.56", "quote 1.0.26", @@ -1347,6 +1379,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "1.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1335e0609db169713d97c340dd769773c6c63cd953c8fcf1063043fd3d6dd11" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df541e0e2a8069352be228ce4b85a1da6f59bfd325e56f57e4b241babbc3f832" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", + "unicode-xid 0.2.4", +] + [[package]] name = "derp" version = "0.0.14" @@ -1518,18 +1571,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "educe" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae" -dependencies = [ - "enum-ordinalize", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", -] - [[package]] name = "ee-1071-regression" version = "0.1.0" @@ -1781,20 +1822,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "enum-ordinalize" -version = "3.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bb1df8b45ecb7ffa78dca1c17a438fb193eb083db0b1b494d2a61bcb5096a" -dependencies = [ - "num-bigint 0.4.3", - "num-traits", - "proc-macro2 1.0.56", - "quote 1.0.26", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "env_logger" version = "0.9.3" @@ -1925,12 +1952,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.7.1", + "miniz_oxide", ] [[package]] @@ -2833,7 +2860,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.7", "tracing", ] @@ -3233,6 +3260,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "juliet" +version = "0.1.0" +dependencies = [ + "array-init", + "assert_matches", + "bimap", + "bytemuck", + "bytes", + "derive_more 1.0.0-beta.3", + "futures", + "hex_fmt", + "once_cell", + "proptest", + "proptest-attr-macro", + "proptest-derive", + "rand", + "static_assertions", + "strum 0.25.0", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "k256" version = "0.13.1" @@ -3276,9 +3328,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libm" @@ -3294,9 +3346,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "list-authorization-keys" @@ -3445,12 +3497,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memory_units" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d96e3f3c0b6325d8ccd83c33b28acb183edcb6c67938ba104ec546854b0882" - [[package]] name = "memory_units" version = "0.4.0" @@ -3488,15 +3534,6 @@ dependencies = [ "adler", ] -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - [[package]] name = "mint-purse" version = "0.1.0" @@ -3658,22 +3695,11 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" dependencies = [ - "num-bigint 0.4.3", + "num-bigint", "num-complex", "num-integer", "num-iter", - "num-rational 0.4.1", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" -dependencies = [ - "autocfg", - "num-integer", + "num-rational", "num-traits", ] @@ -3729,18 +3755,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -dependencies = [ - "autocfg", - "num-bigint 0.2.6", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.1" @@ -3748,7 +3762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", - "num-bigint 0.4.3", + "num-bigint", "num-integer", "num-traits", "serde", @@ -3794,9 +3808,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -3844,9 +3858,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.3+1.1.1t" +version = "111.25.2+1.1.1t" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" +checksum = "320708a054ad9b3bf314688b5db87cf4d6683d64cfc835e2337924ae62bf4431" dependencies = [ "cc", ] @@ -3909,9 +3923,9 @@ checksum = "ddfc878dac00da22f8f61e7af3157988424567ab01d9920b962ef7dcbd7cd865" [[package]] name = "parity-wasm" -version = "0.42.2" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be5e13c266502aadf83426d87d81a0f5d1ef45b8027f5a471c360abfe4bfae92" +checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" [[package]] name = "parking_lot" @@ -3958,7 +3972,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -4202,7 +4216,7 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "unicode-xid", + "unicode-xid 0.1.0", ] [[package]] @@ -4503,13 +4517,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -4529,9 +4543,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "regression-20210707" @@ -4671,9 +4685,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ "base64 0.21.0", "bytes", @@ -4698,7 +4712,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-util 0.7.8", + "tokio-util 0.7.7", "tower-service", "url", "wasm-bindgen", @@ -4765,9 +4779,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.18" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", @@ -5218,7 +5232,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.2", ] [[package]] @@ -5234,6 +5257,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.56", + "quote 1.0.26", + "rustversion", + "syn 2.0.15", +] + [[package]] name = "subtle" version = "2.4.1" @@ -5248,7 +5284,7 @@ checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", - "unicode-xid", + "unicode-xid 0.1.0", ] [[package]] @@ -5432,11 +5468,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -5480,31 +5517,16 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" -dependencies = [ - "bincode", - "bytes", - "educe", - "futures-core", - "futures-sink", - "pin-project", - "serde", -] - [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.7", ] [[package]] @@ -5527,6 +5549,7 @@ checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", @@ -5535,9 +5558,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -5565,7 +5588,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.7", "tower-layer", "tower-service", "tracing", @@ -5598,13 +5621,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2 1.0.56", "quote 1.0.26", - "syn 2.0.15", + "syn 1.0.109", ] [[package]] @@ -5910,6 +5933,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "untrusted" version = "0.7.1" @@ -6123,7 +6152,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tokio-util 0.7.8", + "tokio-util 0.7.7", "tower-service", "tracing", ] @@ -6202,9 +6231,9 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "wasm-encoder" -version = "0.26.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05d0b6fcd0aeb98adf16e7975331b3c17222aa815148f5b976370ce589d80ef" +checksum = "4eff853c4f09eec94d76af527eddad4e9de13b11d6286a1ef7134bc30135a2b7" dependencies = [ "leb128", ] @@ -6224,26 +6253,35 @@ dependencies = [ [[package]] name = "wasmi" -version = "0.9.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca00c5147c319a8ec91ec1a0edbec31e566ce2c9cc93b3f9bb86a9efd0eb795d" +checksum = "06c326c93fbf86419608361a2c925a31754cf109da1b8b55737070b4d6669422" dependencies = [ - "downcast-rs", - "libc", - "memory_units 0.3.0", - "num-rational 0.2.4", - "num-traits", - "parity-wasm 0.42.2", + "parity-wasm 0.45.0", "wasmi-validation", + "wasmi_core", ] [[package]] name = "wasmi-validation" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165343ecd6c018fc09ebcae280752702c9a2ef3e6f8d02f1cfcbdb53ef6d7937" +checksum = "91ff416ad1ff0c42e5a926ed5d5fab74c0f098749aa0ad8b2a34b982ce0e867b" dependencies = [ - "parity-wasm 0.42.2", + "parity-wasm 0.45.0", +] + +[[package]] +name = "wasmi_core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d20cb3c59b788653d99541c646c561c9dd26506f25c0cebfe810659c54c6d7" +dependencies = [ + "downcast-rs", + "libm", + "memory_units", + "num-rational", + "num-traits", ] [[package]] @@ -6254,9 +6292,9 @@ checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6" [[package]] name = "wast" -version = "57.0.0" +version = "56.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eb0f5ed17ac4421193c7477da05892c2edafd67f9639e3c11a82086416662dc" +checksum = "6b54185c051d7bbe23757d50fe575880a2426a2f06d2e9f6a10fd9a4a42920c0" dependencies = [ "leb128", "memchr", @@ -6266,9 +6304,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.63" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9ab0d87337c3be2bb6fc5cd331c4ba9fd6bcb4ee85048a0dd59ed9ecf92e53" +checksum = "56681922808216ab86d96bb750f70d500b5a7800e41564290fd46bb773581299" dependencies = [ "wast", ] @@ -6291,7 +6329,7 @@ checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" dependencies = [ "cfg-if 0.1.10", "libc", - "memory_units 0.4.0", + "memory_units", "winapi", ] @@ -6338,7 +6376,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -6371,7 +6409,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -6391,9 +6429,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", diff --git a/Cargo.toml b/Cargo.toml index 95eaa0d63d..4e96273e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "execution_engine_testing/tests", "hashing", "json_rpc", + "juliet", "node", "smart_contracts/contract", "smart_contracts/contracts/[!.]*/*", @@ -14,6 +15,8 @@ members = [ "utils/validation", "utils/highway-rewards-analysis", ] +# Ensures we do not pull in all the features of dev dependencies when building. +resolver = "2" default-members = [ "ci/casper_updater", @@ -22,6 +25,7 @@ default-members = [ "execution_engine_testing/tests", "hashing", "json_rpc", + "juliet", "node", "types", "utils/global-state-update-gen", @@ -31,11 +35,6 @@ default-members = [ exclude = ["utils/nctl/remotes/casper-client-rs"] -# Include debug symbols in the release build of `casper-engine-tests` so that `simple-transfer` will yield useful -# perf data. -[profile.release.package.casper-engine-tests] -debug = true - [profile.release] codegen-units = 1 lto = true @@ -43,3 +42,7 @@ lto = true [profile.bench] codegen-units = 1 lto = true + +[profile.release-with-debug] +inherits = "release" +debug = true diff --git a/README.md b/README.md index ffc9bfbcb4..5d5e6cfe0a 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,8 @@ RUST_LOG=info cargo run --release -- validator resources/local/config.toml If the environment variable is unset, it is equivalent to setting `RUST_LOG=error`. +When developing and running unit tests, setting `NODE_TEST_LOG=json` will cause the log messages produced by the tests to be JSON-formatted. + ### Log message format A typical log message will look like: diff --git a/execution_engine/Cargo.toml b/execution_engine/Cargo.toml index d4d6620bcf..96957c45e3 100644 --- a/execution_engine/Cargo.toml +++ b/execution_engine/Cargo.toml @@ -16,7 +16,7 @@ base16 = "0.2.1" bincode = "1.3.1" casper-hashing = { version = "2.0.0", path = "../hashing" } casper-types = { version = "3.0.0", path = "../types", default-features = false, features = ["datasize", "gens", "json-schema"] } -casper-wasm-utils = "1.0.0" +casper-wasm-utils = "2.0.0" datasize = "0.2.4" either = "1.8.1" hex_fmt = "0.3.0" @@ -34,7 +34,7 @@ num-rational = { version = "0.4.0", features = ["serde"] } num-traits = "0.2.10" num_cpus = "1" once_cell = "1.5.2" -parity-wasm = { version = "0.42", default-features = false } +parity-wasm = { version = "0.45.0", default-features = false } proptest = { version = "1.0.0", optional = true } rand = "0.8.3" rand_chacha = "0.3.0" @@ -47,7 +47,7 @@ thiserror = "1.0.18" tracing = "0.1.18" uint = "0.9.0" uuid = { version = "0.8.1", features = ["serde", "v4"] } -wasmi = "0.9.1" +wasmi = "0.13.2" [dev-dependencies] assert_matches = "1.3.0" diff --git a/execution_engine/benches/trie_bench.rs b/execution_engine/benches/trie_bench.rs index ef11e40cdf..6c91a8528e 100644 --- a/execution_engine/benches/trie_bench.rs +++ b/execution_engine/benches/trie_bench.rs @@ -42,19 +42,19 @@ fn deserialize_trie_node(b: &mut Bencher) { } fn serialize_trie_node_pointer(b: &mut Bencher) { - let node = Trie::::Extension { - affix: (0..255).collect(), - pointer: Pointer::NodePointer(Digest::hash([0; 32])), - }; + let node = Trie::::extension( + (0..255).collect(), + Pointer::NodePointer(Digest::hash([0; 32])), + ); b.iter(|| ToBytes::to_bytes(black_box(&node))); } fn deserialize_trie_node_pointer(b: &mut Bencher) { - let node = Trie::::Extension { - affix: (0..255).collect(), - pointer: Pointer::NodePointer(Digest::hash([0; 32])), - }; + let node = Trie::::extension( + (0..255).collect(), + Pointer::NodePointer(Digest::hash([0; 32])), + ); let node_bytes = node.to_bytes().unwrap(); b.iter(|| Trie::::from_bytes(black_box(&node_bytes))); diff --git a/execution_engine/src/core/engine_state/engine_config.rs b/execution_engine/src/core/engine_state/engine_config.rs index eaa38dd549..302d10577c 100644 --- a/execution_engine/src/core/engine_state/engine_config.rs +++ b/execution_engine/src/core/engine_state/engine_config.rs @@ -30,13 +30,8 @@ pub const DEFAULT_MAX_STORED_VALUE_SIZE: u32 = 8 * 1024 * 1024; pub const DEFAULT_MINIMUM_DELEGATION_AMOUNT: u64 = 500 * 1_000_000_000; /// Default value for strict argument checking. pub const DEFAULT_STRICT_ARGUMENT_CHECKING: bool = false; -/// 91 days / 7 days in a week = 13 weeks -/// Length of total vesting schedule in days. -const VESTING_SCHEDULE_LENGTH_DAYS: usize = 91; -const DAY_MILLIS: usize = 24 * 60 * 60 * 1000; /// Default length of total vesting schedule period expressed in days. -pub const DEFAULT_VESTING_SCHEDULE_LENGTH_MILLIS: u64 = - VESTING_SCHEDULE_LENGTH_DAYS as u64 * DAY_MILLIS as u64; +pub const DEFAULT_VESTING_SCHEDULE_LENGTH_MILLIS: u64 = 0; /// Default value for allowing auction bids. pub const DEFAULT_ALLOW_AUCTION_BIDS: bool = true; /// Default value for allowing unrestricted transfers. diff --git a/execution_engine/src/core/engine_state/execution_effect.rs b/execution_engine/src/core/engine_state/execution_effect.rs index b1b17ecf2b..372d7edf3b 100644 --- a/execution_engine/src/core/engine_state/execution_effect.rs +++ b/execution_engine/src/core/engine_state/execution_effect.rs @@ -31,6 +31,7 @@ impl From for ExecutionEffect { | Transform::AddUInt256(_) | Transform::AddUInt512(_) | Transform::AddKeys(_) => ops.insert_add(key, Op::Add), + Transform::Prune => ops.insert_add(key, Op::Prune), }; transforms.insert_add(key, transform); } diff --git a/execution_engine/src/core/engine_state/mod.rs b/execution_engine/src/core/engine_state/mod.rs index 0d8ee88249..4efc774e09 100644 --- a/execution_engine/src/core/engine_state/mod.rs +++ b/execution_engine/src/core/engine_state/mod.rs @@ -497,7 +497,7 @@ where match self .state - .delete_keys(correlation_id, state_root_hash, keys_to_delete) + .prune_keys(correlation_id, state_root_hash, keys_to_delete) { Ok(DeleteResult::Deleted(post_state_hash)) => { Ok(PruneResult::Success { post_state_hash }) @@ -2342,12 +2342,12 @@ where (delay, era_id) }; - for key in withdraw_keys { + for key in &withdraw_keys { // Transform only those withdraw purses that are still to be // processed in the unbonding queue. let withdraw_purses = tracking_copy .borrow_mut() - .read(correlation_id, &key) + .read(correlation_id, key) .map_err(|_| Error::FailedToGetWithdrawKeys)? .ok_or(Error::FailedToGetStoredWithdraws)? .as_withdraw() @@ -2382,6 +2382,11 @@ where } } + // Post-migration clean up + + for withdraw_key in withdraw_keys { + tracking_copy.borrow_mut().prune(withdraw_key); + } Ok(()) } diff --git a/execution_engine/src/core/engine_state/op.rs b/execution_engine/src/core/engine_state/op.rs index 28a187dc66..98ea211dfa 100644 --- a/execution_engine/src/core/engine_state/op.rs +++ b/execution_engine/src/core/engine_state/op.rs @@ -6,7 +6,7 @@ use std::{ }; /// Representation of a single operation during execution. -#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] pub enum Op { /// Read value from a `Key`. Read, @@ -14,7 +14,10 @@ pub enum Op { Write, /// Add a value into a `Key`. Add, + /// Prune a value under a `Key`. + Prune, /// No operation. + #[default] NoOp, } @@ -44,12 +47,6 @@ impl Display for Op { } } -impl Default for Op { - fn default() -> Self { - Op::NoOp - } -} - impl From<&Op> for casper_types::OpKind { fn from(op: &Op) -> Self { match op { @@ -57,6 +54,7 @@ impl From<&Op> for casper_types::OpKind { Op::Write => casper_types::OpKind::Write, Op::Add => casper_types::OpKind::Add, Op::NoOp => casper_types::OpKind::NoOp, + Op::Prune => casper_types::OpKind::Delete, } } } diff --git a/execution_engine/src/core/runtime/args.rs b/execution_engine/src/core/runtime/args.rs index 988890adb9..17af96a8c0 100644 --- a/execution_engine/src/core/runtime/args.rs +++ b/execution_engine/src/core/runtime/args.rs @@ -1,4 +1,4 @@ -use wasmi::{FromRuntimeValue, RuntimeArgs, Trap}; +use wasmi::{FromValue, RuntimeArgs, Trap}; pub(crate) trait Args where @@ -9,7 +9,7 @@ where impl Args for (T1,) where - T1: FromRuntimeValue + Sized, + T1: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -19,8 +19,8 @@ where impl Args for (T1, T2) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -31,9 +31,9 @@ where impl Args for (T1, T2, T3) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -45,10 +45,10 @@ where impl Args for (T1, T2, T3, T4) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -61,11 +61,11 @@ where impl Args for (T1, T2, T3, T4, T5) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -79,12 +79,12 @@ where impl Args for (T1, T2, T3, T4, T5, T6) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -99,13 +99,13 @@ where impl Args for (T1, T2, T3, T4, T5, T6, T7) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, - T7: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, + T7: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -121,14 +121,14 @@ where impl Args for (T1, T2, T3, T4, T5, T6, T7, T8) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, - T7: FromRuntimeValue + Sized, - T8: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, + T7: FromValue + Sized, + T8: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -145,15 +145,15 @@ where impl Args for (T1, T2, T3, T4, T5, T6, T7, T8, T9) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, - T7: FromRuntimeValue + Sized, - T8: FromRuntimeValue + Sized, - T9: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, + T7: FromValue + Sized, + T8: FromValue + Sized, + T9: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -171,16 +171,16 @@ where impl Args for (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, - T7: FromRuntimeValue + Sized, - T8: FromRuntimeValue + Sized, - T9: FromRuntimeValue + Sized, - T10: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, + T7: FromValue + Sized, + T8: FromValue + Sized, + T9: FromValue + Sized, + T10: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; @@ -200,17 +200,17 @@ where impl Args for (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) where - T1: FromRuntimeValue + Sized, - T2: FromRuntimeValue + Sized, - T3: FromRuntimeValue + Sized, - T4: FromRuntimeValue + Sized, - T5: FromRuntimeValue + Sized, - T6: FromRuntimeValue + Sized, - T7: FromRuntimeValue + Sized, - T8: FromRuntimeValue + Sized, - T9: FromRuntimeValue + Sized, - T10: FromRuntimeValue + Sized, - T11: FromRuntimeValue + Sized, + T1: FromValue + Sized, + T2: FromValue + Sized, + T3: FromValue + Sized, + T4: FromValue + Sized, + T5: FromValue + Sized, + T6: FromValue + Sized, + T7: FromValue + Sized, + T8: FromValue + Sized, + T9: FromValue + Sized, + T10: FromValue + Sized, + T11: FromValue + Sized, { fn parse(args: RuntimeArgs) -> Result { let a0: T1 = args.nth_checked(0)?; diff --git a/execution_engine/src/core/runtime/auction_internal.rs b/execution_engine/src/core/runtime/auction_internal.rs index d24f398c89..b835f3a99a 100644 --- a/execution_engine/src/core/runtime/auction_internal.rs +++ b/execution_engine/src/core/runtime/auction_internal.rs @@ -98,12 +98,15 @@ where account_hash: AccountHash, unbonding_purses: Vec, ) -> Result<(), Error> { - self.context - .metered_write_gs_unsafe( - Key::Unbond(account_hash), - StoredValue::Unbonding(unbonding_purses), - ) - .map_err(|exec_error| >::from(exec_error).unwrap_or(Error::Storage)) + let unbond_key = Key::Unbond(account_hash); + if unbonding_purses.is_empty() { + self.context.prune_gs_unsafe(unbond_key); + Ok(()) + } else { + self.context + .metered_write_gs_unsafe(unbond_key, StoredValue::Unbonding(unbonding_purses)) + .map_err(|exec_error| >::from(exec_error).unwrap_or(Error::Storage)) + } } fn record_era_info(&mut self, _era_id: EraId, era_summary: EraInfo) -> Result<(), Error> { diff --git a/execution_engine/src/core/runtime/externals.rs b/execution_engine/src/core/runtime/externals.rs index 03e711c1ae..31ab52396d 100644 --- a/execution_engine/src/core/runtime/externals.rs +++ b/execution_engine/src/core/runtime/externals.rs @@ -320,15 +320,15 @@ where )?; let account_hash: AccountHash = { let bytes = self.bytes_from_mem(key_ptr, key_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let amount: U512 = { let bytes = self.bytes_from_mem(amount_ptr, amount_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let id: Option = { let bytes = self.bytes_from_mem(id_ptr, id_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let ret = match self.transfer_to_account(account_hash, amount, id)? { @@ -382,19 +382,19 @@ where )?; let source_purse = { let bytes = self.bytes_from_mem(source_ptr, source_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let account_hash: AccountHash = { let bytes = self.bytes_from_mem(key_ptr, key_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let amount: U512 = { let bytes = self.bytes_from_mem(amount_ptr, amount_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let id: Option = { let bytes = self.bytes_from_mem(id_ptr, id_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)? + bytesrepr::deserialize_from_slice(bytes).map_err(Error::BytesRepr)? }; let ret = match self.transfer_from_purse_to_account_hash( source_purse, @@ -707,13 +707,13 @@ where self.t_from_mem(entry_point_name_ptr, entry_point_name_size)?; let args_bytes: Vec = { let args_size: u32 = args_size; - self.bytes_from_mem(args_ptr, args_size as usize)? + self.bytes_from_mem(args_ptr, args_size as usize)?.to_vec() }; let ret = self.call_contract_host_buffer( contract_hash, &entry_point_name, - args_bytes, + &args_bytes, result_size_ptr, )?; Ok(Some(RuntimeValue::I32(api_error::i32_from(ret)))) @@ -763,14 +763,14 @@ where self.t_from_mem(entry_point_name_ptr, entry_point_name_size)?; let args_bytes: Vec = { let args_size: u32 = args_size; - self.bytes_from_mem(args_ptr, args_size as usize)? + self.bytes_from_mem(args_ptr, args_size as usize)?.to_vec() }; let ret = self.call_versioned_contract_host_buffer( contract_package_hash, contract_version, entry_point_name, - args_bytes, + &args_bytes, result_size_ptr, )?; Ok(Some(RuntimeValue::I32(api_error::i32_from(ret)))) @@ -894,8 +894,10 @@ where &host_function_costs.blake2b, [in_ptr, in_size, out_ptr, out_size], )?; - let input: Vec = self.bytes_from_mem(in_ptr, in_size as usize)?; - let digest = crypto::blake2b(input); + let digest = + self.checked_memory_slice(in_ptr as usize, in_size as usize, |input| { + crypto::blake2b(input) + })?; let result = if digest.len() != out_size as usize { Err(ApiError::BufferTooSmall) diff --git a/execution_engine/src/core/runtime/mod.rs b/execution_engine/src/core/runtime/mod.rs index d96b2553ac..89883c987c 100644 --- a/execution_engine/src/core/runtime/mod.rs +++ b/execution_engine/src/core/runtime/mod.rs @@ -18,7 +18,7 @@ use std::{ use parity_wasm::elements::Module; use tracing::error; -use wasmi::{MemoryRef, Trap, TrapKind}; +use wasmi::{MemoryRef, Trap, TrapCode}; #[cfg(feature = "test-support")] use wasmi::RuntimeValue; @@ -196,37 +196,76 @@ where self.context.charge_system_contract_call(amount) } + fn checked_memory_slice( + &self, + offset: usize, + size: usize, + func: impl FnOnce(&[u8]) -> Ret, + ) -> Result { + // This is mostly copied from a private function `MemoryInstance::checked_memory_region` + // that calls a user defined function with a validated slice of memory. This allows + // usage patterns that does not involve copying data onto heap first i.e. deserialize + // values without copying data first, etc. + // NOTE: Depending on the VM backend used in future, this may change, as not all VMs may + // support direct memory access. + self.try_get_memory()? + .with_direct_access(|buffer| { + let end = offset.checked_add(size).ok_or_else(|| { + wasmi::Error::Memory(format!( + "trying to access memory block of size {} from offset {}", + size, offset + )) + })?; + + if end > buffer.len() { + return Err(wasmi::Error::Memory(format!( + "trying to access region [{}..{}] in memory [0..{}]", + offset, + end, + buffer.len(), + ))); + } + + Ok(func(&buffer[offset..end])) + }) + .map_err(Into::into) + } + /// Returns bytes from the WASM memory instance. + #[inline] fn bytes_from_mem(&self, ptr: u32, size: usize) -> Result, Error> { - self.try_get_memory()?.get(ptr, size).map_err(Into::into) + self.checked_memory_slice(ptr as usize, size, |data| data.to_vec()) } /// Returns a deserialized type from the WASM memory instance. + #[inline] fn t_from_mem(&self, ptr: u32, size: u32) -> Result { - let bytes = self.bytes_from_mem(ptr, size as usize)?; - bytesrepr::deserialize(bytes).map_err(Into::into) + let result = self.checked_memory_slice(ptr as usize, size as usize, |data| { + bytesrepr::deserialize_from_slice(data) + })?; + Ok(result?) } /// Reads key (defined as `key_ptr` and `key_size` tuple) from Wasm memory. + #[inline] fn key_from_mem(&mut self, key_ptr: u32, key_size: u32) -> Result { - let bytes = self.bytes_from_mem(key_ptr, key_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Into::into) + self.t_from_mem(key_ptr, key_size) } /// Reads `CLValue` (defined as `cl_value_ptr` and `cl_value_size` tuple) from Wasm memory. + #[inline] fn cl_value_from_mem( &mut self, cl_value_ptr: u32, cl_value_size: u32, ) -> Result { - let bytes = self.bytes_from_mem(cl_value_ptr, cl_value_size as usize)?; - bytesrepr::deserialize(bytes).map_err(Into::into) + self.t_from_mem(cl_value_ptr, cl_value_size) } /// Returns a deserialized string from the WASM memory instance. + #[inline] fn string_from_mem(&self, ptr: u32, size: u32) -> Result { - let bytes = self.bytes_from_mem(ptr, size as usize)?; - bytesrepr::deserialize(bytes).map_err(|e| Error::BytesRepr(e).into()) + self.t_from_mem(ptr, size).map_err(Trap::from) } fn get_module_from_entry_points( @@ -241,8 +280,7 @@ where #[allow(clippy::wrong_self_convention)] fn is_valid_uref(&self, uref_ptr: u32, uref_size: u32) -> Result { - let bytes = self.bytes_from_mem(uref_ptr, uref_size as usize)?; - let uref: URef = bytesrepr::deserialize(bytes).map_err(Error::BytesRepr)?; + let uref: URef = self.t_from_mem(uref_ptr, uref_size)?; Ok(self.context.validate_uref(&uref).is_ok()) } @@ -450,18 +488,15 @@ where /// type is `Trap`, indicating that this function will always kill the current Wasm instance. fn ret(&mut self, value_ptr: u32, value_size: usize) -> Trap { self.host_buffer = None; - let memory = match self.try_get_memory() { - Ok(memory) => memory, - Err(error) => return Trap::from(error), - }; - let mem_get = memory - .get(value_ptr, value_size) - .map_err(|e| Error::Interpreter(e.into())); + + let mem_get = + self.checked_memory_slice(value_ptr as usize, value_size, |data| data.to_vec()); + match mem_get { Ok(buf) => { // Set the result field in the runtime and return the proper element of the `Error` // enum indicating that the reason for exiting the module was a call to ret. - self.host_buffer = bytesrepr::deserialize(buf).ok(); + self.host_buffer = bytesrepr::deserialize_from_slice(buf).ok(); let urefs = match &self.host_buffer { Some(buf) => utils::extract_urefs(buf), @@ -1441,14 +1476,14 @@ where &mut self, contract_hash: ContractHash, entry_point_name: &str, - args_bytes: Vec, + args_bytes: &[u8], result_size_ptr: u32, ) -> Result, Error> { // Exit early if the host buffer is already occupied if let Err(err) = self.check_host_buffer() { return Ok(Err(err)); } - let args: RuntimeArgs = bytesrepr::deserialize(args_bytes)?; + let args: RuntimeArgs = bytesrepr::deserialize_from_slice(args_bytes)?; let result = self.call_contract(contract_hash, entry_point_name, args)?; self.manage_call_contract_host_buffer(result_size_ptr, result) } @@ -1458,14 +1493,14 @@ where contract_package_hash: ContractPackageHash, contract_version: Option, entry_point_name: String, - args_bytes: Vec, + args_bytes: &[u8], result_size_ptr: u32, ) -> Result, Error> { // Exit early if the host buffer is already occupied if let Err(err) = self.check_host_buffer() { return Ok(Err(err)); } - let args: RuntimeArgs = bytesrepr::deserialize(args_bytes)?; + let args: RuntimeArgs = bytesrepr::deserialize_from_slice(args_bytes)?; let result = self.call_versioned_contract( contract_package_hash, contract_version, @@ -1984,7 +2019,7 @@ where let source_serialized = self.bytes_from_mem(account_hash_ptr, account_hash_size)?; // Account hash deserialized let source: AccountHash = - bytesrepr::deserialize(source_serialized).map_err(Error::BytesRepr)?; + bytesrepr::deserialize_from_slice(source_serialized).map_err(Error::BytesRepr)?; source }; let weight = Weight::new(weight_value); @@ -2015,7 +2050,7 @@ where let source_serialized = self.bytes_from_mem(account_hash_ptr, account_hash_size)?; // Account hash deserialized let source: AccountHash = - bytesrepr::deserialize(source_serialized).map_err(Error::BytesRepr)?; + bytesrepr::deserialize_from_slice(source_serialized).map_err(Error::BytesRepr)?; source }; @@ -2041,7 +2076,7 @@ where let source_serialized = self.bytes_from_mem(account_hash_ptr, account_hash_size)?; // Account hash deserialized let source: AccountHash = - bytesrepr::deserialize(source_serialized).map_err(Error::BytesRepr)?; + bytesrepr::deserialize_from_slice(source_serialized).map_err(Error::BytesRepr)?; source }; let weight = Weight::new(weight_value); @@ -2080,7 +2115,7 @@ where Err(error) => Err(error.into()), } } - Err(_) => Err(Trap::new(TrapKind::Unreachable)), + Err(_) => Err(Trap::Code(TrapCode::Unreachable)), } } @@ -2421,7 +2456,7 @@ where let purse: URef = { let bytes = self.bytes_from_mem(purse_ptr, purse_size)?; - match bytesrepr::deserialize(bytes) { + match bytesrepr::deserialize_from_slice(bytes) { Ok(purse) => purse, Err(error) => return Ok(Err(error.into())), } @@ -2832,13 +2867,13 @@ where } let uref: URef = self.t_from_mem(uref_ptr, uref_size)?; - let dictionary_item_key_bytes = self.bytes_from_mem( - dictionary_item_key_bytes_ptr, + let dictionary_item_key = self.checked_memory_slice( + dictionary_item_key_bytes_ptr as usize, dictionary_item_key_bytes_size as usize, + |utf8_bytes| std::str::from_utf8(utf8_bytes).map(ToOwned::to_owned), )?; - let dictionary_item_key = if let Ok(item_key) = String::from_utf8(dictionary_item_key_bytes) - { + let dictionary_item_key = if let Ok(item_key) = dictionary_item_key { item_key } else { return Ok(Err(ApiError::InvalidDictionaryItemKey)); @@ -2912,12 +2947,16 @@ where value_size: u32, ) -> Result, Trap> { let uref: URef = self.t_from_mem(uref_ptr, uref_size)?; - let dictionary_item_key_bytes = self.bytes_from_mem(key_ptr, key_size as usize)?; - if dictionary_item_key_bytes.len() > DICTIONARY_ITEM_KEY_MAX_LENGTH { - return Ok(Err(ApiError::DictionaryItemKeyExceedsLength)); - } - let dictionary_item_key = if let Ok(item_key) = String::from_utf8(dictionary_item_key_bytes) - { + let dictionary_item_key_bytes = { + if (key_size as usize) > DICTIONARY_ITEM_KEY_MAX_LENGTH { + return Ok(Err(ApiError::DictionaryItemKeyExceedsLength)); + } + self.checked_memory_slice(key_ptr as usize, key_size as usize, |data| { + std::str::from_utf8(data).map(ToOwned::to_owned) + })? + }; + + let dictionary_item_key = if let Ok(item_key) = dictionary_item_key_bytes { item_key } else { return Ok(Err(ApiError::InvalidDictionaryItemKey)); diff --git a/execution_engine/src/core/runtime_context/mod.rs b/execution_engine/src/core/runtime_context/mod.rs index 716af1b305..5b874a68be 100644 --- a/execution_engine/src/core/runtime_context/mod.rs +++ b/execution_engine/src/core/runtime_context/mod.rs @@ -924,6 +924,17 @@ where Ok(()) } + /// Prune a key from the global state. + /// + /// Use with caution - there is no validation done as the key is assumed to be validated + /// already. + pub(crate) fn prune_gs_unsafe(&mut self, key: K) + where + K: Into, + { + self.tracking_copy.borrow_mut().prune(key.into()); + } + /// Writes data to a global state and charges for bytes stored. /// /// This method performs full validation of the key to be written. diff --git a/execution_engine/src/core/tracking_copy/mod.rs b/execution_engine/src/core/tracking_copy/mod.rs index 3608398da3..3cd3e8ae3c 100644 --- a/execution_engine/src/core/tracking_copy/mod.rs +++ b/execution_engine/src/core/tracking_copy/mod.rs @@ -353,6 +353,12 @@ impl> TrackingCopy { self.journal.push((normalized_key, Transform::Write(value))); } + /// Prunes a `key`. + pub(crate) fn prune(&mut self, key: Key) { + let normalized_key = key.normalize(); + self.journal.push((normalized_key, Transform::Prune)); + } + /// Ok(None) represents missing key to which we want to "add" some value. /// Ok(Some(unit)) represents successful operation. /// Err(error) is reserved for unexpected errors when accessing global @@ -417,11 +423,15 @@ impl> TrackingCopy { }; match transform.clone().apply(current_value) { - Ok(new_value) => { + Ok(Some(new_value)) => { self.cache.insert_write(normalized_key, new_value); self.journal.push((normalized_key, transform)); Ok(AddResult::Success) } + Ok(None) => { + self.journal.push((normalized_key, transform)); + Ok(AddResult::Success) + } Err(transform::Error::TypeMismatch(type_mismatch)) => { Ok(AddResult::TypeMismatch(type_mismatch)) } diff --git a/execution_engine/src/shared/host_function_costs.rs b/execution_engine/src/shared/host_function_costs.rs index 83ed7b7c8d..724fff837b 100644 --- a/execution_engine/src/shared/host_function_costs.rs +++ b/execution_engine/src/shared/host_function_costs.rs @@ -203,12 +203,10 @@ pub struct HostFunctionCosts { /// Cost of calling the `read_value` host function. pub read_value: HostFunction<[Cost; 3]>, /// Cost of calling the `dictionary_get` host function. - #[serde(alias = "read_value_local")] pub dictionary_get: HostFunction<[Cost; 3]>, /// Cost of calling the `write` host function. pub write: HostFunction<[Cost; 4]>, /// Cost of calling the `dictionary_put` host function. - #[serde(alias = "write_local")] pub dictionary_put: HostFunction<[Cost; 4]>, /// Cost of calling the `add` host function. pub add: HostFunction<[Cost; 4]>, diff --git a/execution_engine/src/shared/opcode_costs.rs b/execution_engine/src/shared/opcode_costs.rs index 5d9ec9ec44..e0cef5803f 100644 --- a/execution_engine/src/shared/opcode_costs.rs +++ b/execution_engine/src/shared/opcode_costs.rs @@ -1,9 +1,12 @@ //! Support for Wasm opcode costs. use std::{convert::TryInto, num::NonZeroU32}; -use casper_wasm_utils::rules::{MemoryGrowCost, Rules}; +use casper_wasm_utils::{ + parity_wasm::elements::Instruction, + rules::{MemoryGrowCost, Rules}, +}; use datasize::DataSize; -use parity_wasm::elements::Instruction; + use rand::{distributions::Standard, prelude::*, Rng}; use serde::{Deserialize, Serialize}; diff --git a/execution_engine/src/shared/transform.rs b/execution_engine/src/shared/transform.rs index 3a724a1818..e7ff9c8181 100644 --- a/execution_engine/src/shared/transform.rs +++ b/execution_engine/src/shared/transform.rs @@ -59,11 +59,12 @@ impl From for Error { /// Note that all arithmetic variants of [`Transform`] are commutative which means that a given /// collection of them can be executed in any order to produce the same end result. #[allow(clippy::large_enum_variant)] -#[derive(PartialEq, Eq, Debug, Clone, DataSize)] +#[derive(PartialEq, Eq, Debug, Clone, DataSize, Default)] pub enum Transform { /// An identity transformation that does not modify a value in the global state. /// /// Created as part of a read from the global state. + #[default] Identity, /// Writes a new value in the global state. Write(StoredValue), @@ -86,6 +87,8 @@ pub enum Transform { /// /// This transform assumes that the existing stored value is either an Account or a Contract. AddKeys(NamedKeys), + /// Prunes a key. + Prune, /// Represents the case where applying a transform would cause an error. #[data_size(skip)] Failure(Error), @@ -167,24 +170,26 @@ where impl Transform { /// Applies the transformation on a specified stored value instance. /// - /// This method produces a new [`StoredValue`] instance based on the [`Transform`] variant. - pub fn apply(self, stored_value: StoredValue) -> Result { + /// This method produces a new [`StoredValue`] instance based on the [`Transform`] variant. If a + /// given transform is a [`Transform::Prune`] then `None` is returned as the [`StoredValue`] is + /// consumed but no new value is produced. + pub fn apply(self, stored_value: StoredValue) -> Result, Error> { match self { - Transform::Identity => Ok(stored_value), - Transform::Write(new_value) => Ok(new_value), - Transform::AddInt32(to_add) => wrapping_addition(stored_value, to_add), - Transform::AddUInt64(to_add) => wrapping_addition(stored_value, to_add), - Transform::AddUInt128(to_add) => wrapping_addition(stored_value, to_add), - Transform::AddUInt256(to_add) => wrapping_addition(stored_value, to_add), - Transform::AddUInt512(to_add) => wrapping_addition(stored_value, to_add), + Transform::Identity => Ok(Some(stored_value)), + Transform::Write(new_value) => Ok(Some(new_value)), + Transform::AddInt32(to_add) => Ok(Some(wrapping_addition(stored_value, to_add)?)), + Transform::AddUInt64(to_add) => Ok(Some(wrapping_addition(stored_value, to_add)?)), + Transform::AddUInt128(to_add) => Ok(Some(wrapping_addition(stored_value, to_add)?)), + Transform::AddUInt256(to_add) => Ok(Some(wrapping_addition(stored_value, to_add)?)), + Transform::AddUInt512(to_add) => Ok(Some(wrapping_addition(stored_value, to_add)?)), Transform::AddKeys(mut keys) => match stored_value { StoredValue::Contract(mut contract) => { contract.named_keys_append(&mut keys); - Ok(StoredValue::Contract(contract)) + Ok(Some(StoredValue::Contract(contract))) } StoredValue::Account(mut account) => { account.named_keys_append(&mut keys); - Ok(StoredValue::Account(account)) + Ok(Some(StoredValue::Account(account))) } StoredValue::CLValue(cl_value) => { let expected = "Contract or Account".to_string(); @@ -232,6 +237,11 @@ impl Transform { Err(StoredValueTypeMismatch::new(expected, found).into()) } }, + Transform::Prune => { + // Prune does not produce new values, it just consumes a stored value that it + // receives. + Ok(None) + } Transform::Failure(error) => Err(error), } } @@ -275,11 +285,14 @@ impl Add for Transform { (a @ Transform::Failure(_), _) => a, (_, b @ Transform::Failure(_)) => b, (_, b @ Transform::Write(_)) => b, + (_, Transform::Prune) => Transform::Prune, + (Transform::Prune, b) => b, (Transform::Write(v), b) => { // second transform changes value being written match b.apply(v) { + Ok(Some(new_value)) => Transform::Write(new_value), + Ok(None) => Transform::Prune, Err(error) => Transform::Failure(error), - Ok(new_value) => Transform::Write(new_value), } } (Transform::AddInt32(i), b) => match b { @@ -333,12 +346,6 @@ impl Display for Transform { } } -impl Default for Transform { - fn default() -> Self { - Transform::Identity - } -} - impl From<&Transform> for casper_types::Transform { fn from(transform: &Transform) -> Self { match transform { @@ -389,6 +396,7 @@ impl From<&Transform> for casper_types::Transform { .collect(), ), Transform::Failure(error) => casper_types::Transform::Failure(error.to_string()), + Transform::Prune => casper_types::Transform::Prune, } } } @@ -419,6 +427,7 @@ pub mod gens { buf.copy_from_slice(&u); Transform::AddUInt512(buf.into()) }), + Just(Transform::Prune) ] } } @@ -434,7 +443,7 @@ mod tests { }; use super::*; - use std::collections::BTreeMap; + use std::{collections::BTreeMap, convert::TryInto}; const ZERO_ARRAY: [u8; 32] = [0; 32]; const ZERO_PUBLIC_KEY: AccountHash = AccountHash::new(ZERO_ARRAY); @@ -479,6 +488,16 @@ mod tests { const ONE_U512: U512 = U512([1, 0, 0, 0, 0, 0, 0, 0]); const MAX_U512: U512 = U512([MAX_U64; 8]); + fn add_transforms(value: u32) -> Vec { + vec![ + Transform::AddInt32(value.try_into().expect("positive value")), + Transform::AddUInt64(value.into()), + Transform::AddUInt128(value.into()), + Transform::AddUInt256(value.into()), + Transform::AddUInt512(value.into()), + ] + } + #[test] fn i32_overflow() { let max = std::i32::MAX; @@ -493,8 +512,18 @@ mod tests { let transform_overflow = Transform::AddInt32(max) + Transform::AddInt32(1); let transform_underflow = Transform::AddInt32(min) + Transform::AddInt32(-1); - assert_eq!(apply_overflow.expect("Unexpected overflow"), min_value); - assert_eq!(apply_underflow.expect("Unexpected underflow"), max_value); + assert_eq!( + apply_overflow + .expect("Unexpected overflow") + .expect("New value"), + min_value + ); + assert_eq!( + apply_underflow + .expect("Unexpected underflow") + .expect("New value"), + max_value + ); assert_eq!(transform_overflow, min.into()); assert_eq!(transform_underflow, max.into()); @@ -527,9 +556,9 @@ mod tests { let transform_overflow_uint = max_transform + one_transform; let transform_underflow = min_transform + Transform::AddInt32(-1); - assert_eq!(apply_overflow, Ok(zero_value.clone())); - assert_eq!(apply_overflow_uint, Ok(zero_value)); - assert_eq!(apply_underflow, Ok(max_value)); + assert_eq!(apply_overflow, Ok(Some(zero_value.clone()))); + assert_eq!(apply_overflow_uint, Ok(Some(zero_value))); + assert_eq!(apply_underflow, Ok(Some(max_value))); assert_eq!(transform_overflow, zero.into()); assert_eq!(transform_overflow_uint, zero.into()); @@ -868,4 +897,57 @@ mod tests { assert_eq!(ZERO_U512, add(MAX_U512, ONE_U512)); assert_eq!(MAX_U512 - 1, add(MAX_U512, MAX_U512)); } + + #[test] + fn delete_should_produce_correct_transform() { + { + // prune + write == write + let lhs = Transform::Prune; + let rhs = Transform::Write(StoredValue::CLValue(CLValue::unit())); + + let new_transform = lhs + rhs.clone(); + assert_eq!(new_transform, rhs); + } + + { + // prune + identity == prune (prune modifies the global state, identity does not + // modify, so we need to preserve prune) + let new_transform = Transform::Prune + Transform::Identity; + assert_eq!(new_transform, Transform::Prune); + } + + { + // prune + failure == failure + let failure = Transform::Failure(Error::Serialization(bytesrepr::Error::Formatting)); + let new_transform = Transform::Prune + failure.clone(); + assert_eq!(new_transform, failure); + } + + { + // write + prune == prune + let lhs = Transform::Write(StoredValue::CLValue(CLValue::unit())); + let rhs = Transform::Prune; + + let new_transform = lhs + rhs.clone(); + assert_eq!(new_transform, rhs); + } + + { + // add + prune == prune + for lhs in add_transforms(123) { + let rhs = Transform::Prune; + let new_transform = lhs + rhs.clone(); + assert_eq!(new_transform, rhs); + } + } + + { + // prune + add == add + for rhs in add_transforms(123) { + let lhs = Transform::Prune; + let new_transform = lhs + rhs.clone(); + assert_eq!(new_transform, rhs); + } + } + } } diff --git a/execution_engine/src/shared/wasm_prep.rs b/execution_engine/src/shared/wasm_prep.rs index 75063c1abb..b64a90da49 100644 --- a/execution_engine/src/shared/wasm_prep.rs +++ b/execution_engine/src/shared/wasm_prep.rs @@ -1,13 +1,19 @@ //! Preprocessing of Wasm modules. -use casper_wasm_utils::{self, stack_height}; +use casper_wasm_utils::{self, parity_wasm::elements::Module, stack_height}; use parity_wasm::elements::{ - self, External, Instruction, Internal, MemorySection, Module, Section, TableType, Type, + self, External, Instruction, Internal, MemorySection, Section, TableType, Type, }; use thiserror::Error; use super::wasm_config::WasmConfig; use crate::core::execution; +const ATOMIC_OPCODE_PREFIX: u8 = 0xfe; +const BULK_OPCODE_PREFIX: u8 = 0xfc; +const SIGN_EXT_OPCODE_START: u8 = 0xc0; +const SIGN_EXT_OPCODE_END: u8 = 0xc4; +const SIMD_OPCODE_PREFIX: u8 = 0xfd; + const DEFAULT_GAS_MODULE_NAME: &str = "env"; /// Name of the internal gas function injected by [`casper_wasm_utils::inject_gas_counter`]. const INTERNAL_GAS_FUNCTION_NAME: &str = "gas"; @@ -405,7 +411,37 @@ pub fn preprocess( /// Returns a parity Module from the given bytes without making modifications or checking limits. pub fn deserialize(module_bytes: &[u8]) -> Result { - parity_wasm::deserialize_buffer::(module_bytes).map_err(Into::into) + parity_wasm::deserialize_buffer::(module_bytes).map_err(|deserialize_error| { + match deserialize_error { + parity_wasm::SerializationError::UnknownOpcode(BULK_OPCODE_PREFIX) => { + PreprocessingError::Deserialize( + "Bulk memory operations are not supported".to_string(), + ) + } + parity_wasm::SerializationError::UnknownOpcode(SIMD_OPCODE_PREFIX) => { + PreprocessingError::Deserialize("SIMD operations are not supported".to_string()) + } + parity_wasm::SerializationError::UnknownOpcode(ATOMIC_OPCODE_PREFIX) => { + PreprocessingError::Deserialize("Atomic operations are not supported".to_string()) + } + parity_wasm::SerializationError::UnknownOpcode( + SIGN_EXT_OPCODE_START..=SIGN_EXT_OPCODE_END, + ) => PreprocessingError::Deserialize( + "Sign extension operations are not supported".to_string(), + ), + parity_wasm::SerializationError::Other(msg) if msg == "Enable the multi_value feature to deserialize more than one function result" => { + // Due to the way parity-wasm crate works, it's always deserializes opcodes + // from multi_value proposal but if the feature is not enabled, then it will + // error with very specific message (as compared to other extensions). + // + // That's OK since we'd prefer to not inspect deserialized bytecode. We + // can simply replace the error message with a more user friendly one. + PreprocessingError::Deserialize("Multi value extension is not supported".to_string()) + } + _ => deserialize_error.into(), + + } + }) } /// Creates new wasm module from entry points. @@ -443,7 +479,10 @@ mod tests { builder, elements::{CodeSection, Instructions}, }; - use walrus::{FunctionBuilder, ModuleConfig, ValType}; + use walrus::{ + ir::{Instr, UnaryOp, Unop}, + FunctionBuilder, ModuleConfig, ValType, + }; use super::*; @@ -645,8 +684,316 @@ mod tests { .expect_err("should fail with an error"); assert!( matches!(&error, PreprocessingError::Deserialize(msg) - // TODO: GH-3762 will improve the error message for unsupported wasm proposals. - if msg == "Enable the multi_value feature to deserialize more than one function result"), + if msg == "Multi value extension is not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_atomics_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_atomics = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_atomics.func_body().atomic_fence(); + + let func_with_atomics = func_with_atomics.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_atomics); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Atomic operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_bulk_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_bulk = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_bulk.func_body().memory_copy(memory_id, memory_id); + + let func_with_bulk = func_with_bulk.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_bulk); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Bulk memory operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_simd_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_simd = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_simd.func_body().v128_bitselect(); + + let func_with_simd = func_with_simd.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_simd); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "SIMD operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_sign_ext_i32_e8s_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_sign_ext = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_sign_ext.func_body().i32_const(0); + + { + let mut body = func_with_sign_ext.func_body(); + let instructions = body.instrs_mut(); + let (instr, _) = instructions.get_mut(0).unwrap(); + *instr = Instr::Unop(Unop { + op: UnaryOp::I32Extend8S, + }); + } + + let func_with_sign_ext = func_with_sign_ext.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_sign_ext); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Sign extension operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_sign_ext_i32_e16s_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_sign_ext = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_sign_ext.func_body().i32_const(0); + + { + let mut body = func_with_sign_ext.func_body(); + let instructions = body.instrs_mut(); + let (instr, _) = instructions.get_mut(0).unwrap(); + *instr = Instr::Unop(Unop { + op: UnaryOp::I32Extend16S, + }); + } + + let func_with_sign_ext = func_with_sign_ext.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_sign_ext); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Sign extension operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_sign_ext_i64_e8s_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_sign_ext = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_sign_ext.func_body().i32_const(0); + + { + let mut body = func_with_sign_ext.func_body(); + let instructions = body.instrs_mut(); + let (instr, _) = instructions.get_mut(0).unwrap(); + *instr = Instr::Unop(Unop { + op: UnaryOp::I64Extend8S, + }); + } + + let func_with_sign_ext = func_with_sign_ext.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_sign_ext); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Sign extension operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_sign_ext_i64_e16s_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_sign_ext = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_sign_ext.func_body().i32_const(0); + + { + let mut body = func_with_sign_ext.func_body(); + let instructions = body.instrs_mut(); + let (instr, _) = instructions.get_mut(0).unwrap(); + *instr = Instr::Unop(Unop { + op: UnaryOp::I64Extend16S, + }); + } + + let func_with_sign_ext = func_with_sign_ext.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_sign_ext); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Sign extension operations are not supported"), + "{:?}", + error, + ); + } + + #[test] + fn should_not_accept_sign_ext_i64_e32s_proposal_wasm() { + let module_bytes = { + let mut module = walrus::Module::with_config(ModuleConfig::new()); + + let _memory_id = module.memories.add_local(false, 11, None); + + let mut func_with_sign_ext = FunctionBuilder::new(&mut module.types, &[], &[]); + + func_with_sign_ext.func_body().i32_const(0); + + { + let mut body = func_with_sign_ext.func_body(); + let instructions = body.instrs_mut(); + let (instr, _) = instructions.get_mut(0).unwrap(); + *instr = Instr::Unop(Unop { + op: UnaryOp::I64Extend32S, + }); + } + + let func_with_sign_ext = func_with_sign_ext.finish(vec![], &mut module.funcs); + + let mut call_func = FunctionBuilder::new(&mut module.types, &[], &[]); + + call_func.func_body().call(func_with_sign_ext); + + let call = call_func.finish(Vec::new(), &mut module.funcs); + + module.exports.add(DEFAULT_ENTRY_POINT_NAME, call); + + module.emit_wasm() + }; + let error = preprocess(WasmConfig::default(), &module_bytes) + .expect_err("should fail with an error"); + assert!( + matches!(&error, PreprocessingError::Deserialize(msg) + if msg == "Sign extension operations are not supported"), "{:?}", error, ); diff --git a/execution_engine/src/storage/global_state/in_memory.rs b/execution_engine/src/storage/global_state/in_memory.rs index a132e74457..1f31f95e17 100644 --- a/execution_engine/src/storage/global_state/in_memory.rs +++ b/execution_engine/src/storage/global_state/in_memory.rs @@ -284,7 +284,7 @@ impl StateProvider for InMemoryGlobalState { Ok(missing_descendants) } - fn delete_keys( + fn prune_keys( &self, correlation_id: CorrelationId, mut root: Digest, diff --git a/execution_engine/src/storage/global_state/lmdb.rs b/execution_engine/src/storage/global_state/lmdb.rs index dab903d229..a27a85e7bd 100644 --- a/execution_engine/src/storage/global_state/lmdb.rs +++ b/execution_engine/src/storage/global_state/lmdb.rs @@ -92,7 +92,7 @@ impl LmdbGlobalState { &self, correlation_id: CorrelationId, prestate_hash: Digest, - stored_values: HashMap, + stored_values: HashMap>, ) -> Result { let scratch_trie = self.get_scratch_store(); let new_state_root = put_stored_values::<_, _, error::Error>( @@ -293,8 +293,8 @@ impl StateProvider for LmdbGlobalState { Ok(missing_hashes) } - /// Delete keys. - fn delete_keys( + /// Prune keys. + fn prune_keys( &self, correlation_id: CorrelationId, mut state_root_hash: Digest, @@ -329,6 +329,8 @@ impl StateProvider for LmdbGlobalState { #[cfg(test)] mod tests { + use std::{collections::BTreeSet, iter::FromIterator}; + use lmdb::DatabaseFlags; use tempfile::tempdir; @@ -360,24 +362,32 @@ mod tests { ] } + const KEY_ACCOUNT_1: Key = Key::Account(AccountHash::new([1u8; 32])); + const KEY_ACCOUNT_2: Key = Key::Account(AccountHash::new([2u8; 32])); + const KEY_ACCOUNT_3: Key = Key::Account(AccountHash::new([3u8; 32])); + fn create_test_pairs_updated() -> [TestPair; 3] { [ TestPair { - key: Key::Account(AccountHash::new([1u8; 32])), + key: KEY_ACCOUNT_1, value: StoredValue::CLValue(CLValue::from_t("one".to_string()).unwrap()), }, TestPair { - key: Key::Account(AccountHash::new([2u8; 32])), + key: KEY_ACCOUNT_2, value: StoredValue::CLValue(CLValue::from_t("two".to_string()).unwrap()), }, TestPair { - key: Key::Account(AccountHash::new([3u8; 32])), + key: KEY_ACCOUNT_3, value: StoredValue::CLValue(CLValue::from_t(3_i32).unwrap()), }, ] } - fn create_test_state(pairs_creator: fn() -> [TestPair; 2]) -> (LmdbGlobalState, Digest) { + fn create_test_state(pairs_creator: F) -> (LmdbGlobalState, Digest) + where + T: AsRef<[TestPair]>, + F: FnOnce() -> T, + { let correlation_id = CorrelationId::new(); let temp_dir = tempdir().unwrap(); let environment = Arc::new( @@ -397,7 +407,7 @@ mod tests { { let mut txn = ret.environment.create_read_write_txn().unwrap(); - for TestPair { key, value } in &(pairs_creator)() { + for TestPair { key, value } in pairs_creator().as_ref() { match write::<_, _, _, LmdbTrieStore, error::Error>( correlation_id, &mut txn, @@ -466,6 +476,67 @@ mod tests { } } + #[test] + fn commit_updates_state_with_delete() { + let correlation_id = CorrelationId::new(); + let test_pairs_updated = create_test_pairs_updated(); + + let (state, root_hash) = create_test_state(create_test_pairs_updated); + + let effects: AdditiveMap = { + let mut tmp = AdditiveMap::new(); + + let head = test_pairs_updated[..test_pairs_updated.len() - 1].to_vec(); + let tail = test_pairs_updated[test_pairs_updated.len() - 1..].to_vec(); + assert_eq!(head.len() + tail.len(), test_pairs_updated.len()); + + for TestPair { key, value } in &head { + tmp.insert(*key, Transform::Write(value.to_owned())); + } + for TestPair { key, .. } in &tail { + tmp.insert(*key, Transform::Prune); + } + + tmp + }; + + let updated_hash = state.commit(correlation_id, root_hash, effects).unwrap(); + + assert_ne!( + root_hash, updated_hash, + "Post state root hash is expected to be different than pre state root hash" + ); + + let updated_checkout = state.checkout(updated_hash).unwrap().unwrap(); + + let all_keys = updated_checkout + .keys_with_prefix(correlation_id, &[]) + .unwrap(); + assert_eq!( + BTreeSet::from_iter(all_keys), + BTreeSet::from_iter(vec![KEY_ACCOUNT_1, KEY_ACCOUNT_2,]) + ); + + let account_1 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_1) + .unwrap(); + assert_eq!(account_1, Some(test_pairs_updated[0].clone().value)); + + let account_2 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_2) + .unwrap(); + assert_eq!(account_2, Some(test_pairs_updated[1].clone().value)); + + let account_3 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_3) + .unwrap(); + assert_eq!( + account_3, None, + "Account {:?} should be deleted", + KEY_ACCOUNT_3 + ); + } + #[test] fn commit_updates_state_and_original_state_stays_intact() { let correlation_id = CorrelationId::new(); diff --git a/execution_engine/src/storage/global_state/mod.rs b/execution_engine/src/storage/global_state/mod.rs index 46c501c763..39897691a4 100644 --- a/execution_engine/src/storage/global_state/mod.rs +++ b/execution_engine/src/storage/global_state/mod.rs @@ -32,7 +32,7 @@ use crate::{ }, }; -use super::trie_store::operations::DeleteResult; +use super::trie_store::operations::{delete, DeleteResult}; /// A trait expressing the reading of state. This trait is used to abstract the underlying store. pub trait StateReader { @@ -123,8 +123,8 @@ pub trait StateProvider { trie_raw: &[u8], ) -> Result, Self::Error>; - /// Delete key from the global state. - fn delete_keys( + /// Prune keys from the global state. + fn prune_keys( &self, correlation_id: CorrelationId, root: Digest, @@ -138,7 +138,7 @@ pub fn put_stored_values<'a, R, S, E>( store: &S, correlation_id: CorrelationId, prestate_hash: Digest, - stored_values: HashMap, + stored_values: HashMap>, ) -> Result where R: TransactionSource<'a, Handle = S::Handle>, @@ -152,17 +152,43 @@ where if maybe_root.is_none() { return Err(CommitError::RootNotFound(prestate_hash).into()); }; - for (key, value) in stored_values.iter() { - let write_result = - write::<_, _, _, _, E>(correlation_id, &mut txn, store, &state_root, key, value)?; - match write_result { - WriteResult::Written(root_hash) => { - state_root = root_hash; + for (key, maybe_value) in stored_values.iter() { + match maybe_value { + Some(value) => { + let write_result = write::<_, _, _, _, E>( + correlation_id, + &mut txn, + store, + &state_root, + key, + value, + )?; + match write_result { + WriteResult::Written(root_hash) => { + state_root = root_hash; + } + WriteResult::AlreadyExists => (), + WriteResult::RootNotFound => { + error!(?state_root, ?key, ?value, "Error writing new value"); + return Err(CommitError::WriteRootNotFound(state_root).into()); + } + } } - WriteResult::AlreadyExists => (), - WriteResult::RootNotFound => { - error!(?state_root, ?key, ?value, "Error writing new value"); - return Err(CommitError::WriteRootNotFound(state_root).into()); + None => { + let delete_result = + delete::<_, _, _, _, E>(correlation_id, &mut txn, store, &state_root, key)?; + match delete_result { + DeleteResult::Deleted(root_hash) => { + state_root = root_hash; + } + DeleteResult::DoesNotExist => { + return Err(CommitError::KeyNotFound(*key).into()); + } + DeleteResult::RootNotFound => { + error!(?state_root, ?key, "Error deleting value"); + return Err(CommitError::WriteRootNotFound(state_root).into()); + } + } } } } @@ -198,7 +224,7 @@ where let read_result = read::<_, _, _, _, E>(correlation_id, &txn, store, &state_root, &key)?; let value = match (read_result, transform) { - (ReadResult::NotFound, Transform::Write(new_value)) => new_value, + (ReadResult::NotFound, Transform::Write(new_value)) => Some(new_value), (ReadResult::NotFound, transform) => { error!( ?state_root, @@ -231,17 +257,40 @@ where } }; - let write_result = - write::<_, _, _, _, E>(correlation_id, &mut txn, store, &state_root, &key, &value)?; - - match write_result { - WriteResult::Written(root_hash) => { - state_root = root_hash; + match value { + Some(value) => { + let write_result = write::<_, _, _, _, E>( + correlation_id, + &mut txn, + store, + &state_root, + &key, + &value, + )?; + + match write_result { + WriteResult::Written(root_hash) => { + state_root = root_hash; + } + WriteResult::AlreadyExists => (), + WriteResult::RootNotFound => { + error!(?state_root, ?key, ?value, "Error writing new value"); + return Err(CommitError::WriteRootNotFound(state_root).into()); + } + } } - WriteResult::AlreadyExists => (), - WriteResult::RootNotFound => { - error!(?state_root, ?key, ?value, "Error writing new value"); - return Err(CommitError::WriteRootNotFound(state_root).into()); + None => { + match delete::<_, _, _, _, E>(correlation_id, &mut txn, store, &state_root, &key)? { + DeleteResult::Deleted(root_hash) => { + state_root = root_hash; + } + DeleteResult::DoesNotExist => { + return Err(CommitError::KeyNotFound(key).into()); + } + DeleteResult::RootNotFound => { + return Err(CommitError::RootNotFound(state_root).into()); + } + } } } } diff --git a/execution_engine/src/storage/global_state/scratch.rs b/execution_engine/src/storage/global_state/scratch.rs index 8b1a1442ad..757bce073e 100644 --- a/execution_engine/src/storage/global_state/scratch.rs +++ b/execution_engine/src/storage/global_state/scratch.rs @@ -31,7 +31,7 @@ use crate::{ type SharedCache = Arc>; struct Cache { - cached_values: HashMap, + cached_values: HashMap)>, } impl Cache { @@ -41,21 +41,24 @@ impl Cache { } } - fn insert_write(&mut self, key: Key, value: StoredValue) { + fn insert_write(&mut self, key: Key, value: Option) { self.cached_values.insert(key, (true, value)); } fn insert_read(&mut self, key: Key, value: StoredValue) { - self.cached_values.entry(key).or_insert((false, value)); + self.cached_values + .entry(key) + .or_insert((false, Some(value))); } fn get(&self, key: &Key) -> Option<&StoredValue> { - self.cached_values.get(key).map(|(_dirty, value)| value) + let maybe_value = self.cached_values.get(key).map(|(_dirty, value)| value)?; + maybe_value.as_ref() } /// Consumes self and returns only written values as values that were only read must be filtered /// out to prevent unnecessary writes. - fn into_dirty_writes(self) -> HashMap { + fn into_dirty_writes(self) -> HashMap> { self.cached_values .into_iter() .filter_map(|(key, (dirty, value))| if dirty { Some((key, value)) } else { None }) @@ -104,7 +107,7 @@ impl ScratchGlobalState { } /// Consume self and return inner cache. - pub fn into_inner(self) -> HashMap { + pub fn into_inner(self) -> HashMap> { let cache = mem::replace(&mut *self.cache.write().unwrap(), Cache::new()); cache.into_dirty_writes() } @@ -204,7 +207,7 @@ impl CommitProvider for ScratchGlobalState { for (key, transform) in effects.into_iter() { let cached_value = self.cache.read().unwrap().get(&key).cloned(); let value = match (cached_value, transform) { - (None, Transform::Write(new_value)) => new_value, + (None, Transform::Write(new_value)) => Some(new_value), (None, transform) => { // It might be the case that for `Add*` operations we don't have the previous // value in cache yet. @@ -328,7 +331,7 @@ impl StateProvider for ScratchGlobalState { Ok(missing_descendants) } - fn delete_keys( + fn prune_keys( &self, correlation_id: CorrelationId, mut state_root_hash: Digest, @@ -376,14 +379,18 @@ mod tests { value: StoredValue, } + const KEY_ACCOUNT_1: Key = Key::Account(AccountHash::new([1u8; 32])); + const KEY_ACCOUNT_2: Key = Key::Account(AccountHash::new([2u8; 32])); + const KEY_ACCOUNT_3: Key = Key::Account(AccountHash::new([3u8; 32])); + fn create_test_pairs() -> [TestPair; 2] { [ TestPair { - key: Key::Account(AccountHash::new([1_u8; 32])), + key: KEY_ACCOUNT_1, value: StoredValue::CLValue(CLValue::from_t(1_i32).unwrap()), }, TestPair { - key: Key::Account(AccountHash::new([2_u8; 32])), + key: KEY_ACCOUNT_2, value: StoredValue::CLValue(CLValue::from_t(2_i32).unwrap()), }, ] @@ -392,15 +399,15 @@ mod tests { fn create_test_pairs_updated() -> [TestPair; 3] { [ TestPair { - key: Key::Account(AccountHash::new([1u8; 32])), + key: KEY_ACCOUNT_1, value: StoredValue::CLValue(CLValue::from_t("one".to_string()).unwrap()), }, TestPair { - key: Key::Account(AccountHash::new([2u8; 32])), + key: KEY_ACCOUNT_2, value: StoredValue::CLValue(CLValue::from_t("two".to_string()).unwrap()), }, TestPair { - key: Key::Account(AccountHash::new([3u8; 32])), + key: KEY_ACCOUNT_3, value: StoredValue::CLValue(CLValue::from_t(3_i32).unwrap()), }, ] @@ -428,7 +435,11 @@ mod tests { root_hash: Digest, } - fn create_test_state() -> TestState { + fn create_test_state(pairs_creator: F) -> TestState + where + T: AsRef<[TestPair]>, + F: FnOnce() -> T, + { let correlation_id = CorrelationId::new(); let temp_dir = tempdir().unwrap(); let environment = Arc::new( @@ -448,7 +459,7 @@ mod tests { { let mut txn = state.environment.create_read_write_txn().unwrap(); - for TestPair { key, value } in &create_test_pairs() { + for TestPair { key, value } in pairs_creator().as_ref() { match write::<_, _, _, LmdbTrieStore, error::Error>( correlation_id, &mut txn, @@ -482,7 +493,7 @@ mod tests { let correlation_id = CorrelationId::new(); let test_pairs_updated = create_test_pairs_updated(); - let TestState { state, root_hash } = create_test_state(); + let TestState { state, root_hash } = create_test_state(create_test_pairs); let scratch = state.create_scratch(); @@ -515,13 +526,10 @@ mod tests { for key in all_keys { assert!(stored_values.get(&key).is_some()); - assert_eq!( - stored_values.get(&key), - updated_checkout - .read(correlation_id, &key) - .unwrap() - .as_ref() - ); + let lhs = stored_values.get(&key); + let stored_value = updated_checkout.read(correlation_id, &key).unwrap(); + let rhs = Some(&stored_value); + assert_eq!(lhs, rhs,); } for TestPair { key, value } in test_pairs_updated.iter().cloned() { @@ -532,17 +540,94 @@ mod tests { } } + #[test] + fn commit_updates_state_with_delete() { + let correlation_id = CorrelationId::new(); + let test_pairs_updated = create_test_pairs_updated(); + + let TestState { state, root_hash } = create_test_state(create_test_pairs_updated); + + let scratch = state.create_scratch(); + + let effects: AdditiveMap = { + let mut tmp = AdditiveMap::new(); + + let head = test_pairs_updated[..test_pairs_updated.len() - 1].to_vec(); + let tail = test_pairs_updated[test_pairs_updated.len() - 1..].to_vec(); + assert_eq!(head.len() + tail.len(), test_pairs_updated.len()); + + for TestPair { key, value } in &head { + tmp.insert(*key, Transform::Write(value.to_owned())); + } + for TestPair { key, .. } in &tail { + tmp.insert(*key, Transform::Prune); + } + + tmp + }; + + let scratch_root_hash = scratch + .commit(correlation_id, root_hash, effects.clone()) + .unwrap(); + + assert_eq!( + scratch_root_hash, root_hash, + "ScratchGlobalState should not modify the state root, as it does no hashing" + ); + + let lmdb_hash = state.commit(correlation_id, root_hash, effects).unwrap(); + let updated_checkout = state.checkout(lmdb_hash).unwrap().unwrap(); + + let all_keys = updated_checkout + .keys_with_prefix(correlation_id, &[]) + .unwrap(); + + let stored_values = scratch.into_inner(); + assert_eq!( + all_keys.len(), + stored_values.len() - 1, + "Should delete one key from the global state" + ); + + for key in all_keys { + assert!(stored_values.get(&key).is_some()); + let lhs = stored_values.get(&key).cloned(); + let rhs = updated_checkout.read(correlation_id, &key).unwrap(); + + assert_eq!(lhs, Some(rhs)); + } + + let account_1 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_1) + .unwrap(); + assert_eq!(account_1, Some(test_pairs_updated[0].clone().value)); + + let account_2 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_2) + .unwrap(); + assert_eq!(account_2, Some(test_pairs_updated[1].clone().value)); + + let account_3 = updated_checkout + .read(correlation_id, &KEY_ACCOUNT_3) + .unwrap(); + assert_eq!( + account_3, None, + "Account {:?} should be deleted", + KEY_ACCOUNT_3 + ); + } + #[test] fn commit_updates_state_with_add() { let correlation_id = CorrelationId::new(); let test_pairs_updated = create_test_pairs_updated(); // create two lmdb instances, with a scratch instance on the first - let TestState { state, root_hash } = create_test_state(); + let TestState { state, root_hash } = create_test_state(create_test_pairs); let TestState { state: state2, root_hash: state_2_root_hash, - } = create_test_state(); + } = create_test_state(create_test_pairs); let scratch = state.create_scratch(); @@ -599,7 +684,7 @@ mod tests { let TestState { state, root_hash, .. - } = create_test_state(); + } = create_test_state(create_test_pairs); let scratch = state.create_scratch(); diff --git a/execution_engine/src/storage/store/mod.rs b/execution_engine/src/storage/store/mod.rs index 19ea5f8953..2db3851ba0 100644 --- a/execution_engine/src/storage/store/mod.rs +++ b/execution_engine/src/storage/store/mod.rs @@ -21,6 +21,24 @@ pub trait Store { /// `handle` returns the underlying store. fn handle(&self) -> Self::Handle; + /// Deserialize a value. + #[inline] + fn deserialize_value(&self, bytes: &[u8]) -> Result + where + V: FromBytes, + { + bytesrepr::deserialize_from_slice(bytes) + } + + /// Serialize a value. + #[inline] + fn serialize_value(&self, value: &V) -> Result, bytesrepr::Error> + where + V: ToBytes, + { + value.to_bytes() + } + /// Returns an optional value (may exist or not) as read through a transaction, or an error /// of the associated `Self::Error` variety. fn get(&self, txn: &T, key: &K) -> Result, Self::Error> @@ -33,7 +51,7 @@ pub trait Store { let raw = self.get_raw(txn, key)?; match raw { Some(bytes) => { - let value = bytesrepr::deserialize_from_slice(bytes)?; + let value = self.deserialize_value(&bytes)?; Ok(Some(value)) } None => Ok(None), @@ -61,7 +79,8 @@ pub trait Store { V: ToBytes, Self::Error: From, { - self.put_raw(txn, key, Cow::from(value.to_bytes()?)) + let serialized_value = self.serialize_value(value)?; + self.put_raw(txn, key, Cow::from(serialized_value)) } /// Puts a raw `value` into the store at `key` within a transaction, potentially returning an diff --git a/execution_engine/src/storage/trie/gens.rs b/execution_engine/src/storage/trie/gens.rs index 53485c3b25..955324ea22 100644 --- a/execution_engine/src/storage/trie/gens.rs +++ b/execution_engine/src/storage/trie/gens.rs @@ -32,10 +32,8 @@ pub fn trie_leaf_arb() -> impl Strategy> { } pub fn trie_extension_arb() -> impl Strategy> { - (vec(any::(), 0..32), trie_pointer_arb()).prop_map(|(affix, pointer)| Trie::Extension { - affix: affix.into(), - pointer, - }) + (vec(any::(), 0..32), trie_pointer_arb()) + .prop_map(|(affix, pointer)| Trie::extension(affix, pointer)) } pub fn trie_node_arb() -> impl Strategy> { diff --git a/execution_engine/src/storage/trie/mod.rs b/execution_engine/src/storage/trie/mod.rs index 7cc67aba5a..a091d51844 100644 --- a/execution_engine/src/storage/trie/mod.rs +++ b/execution_engine/src/storage/trie/mod.rs @@ -1,7 +1,7 @@ //! Core types for a Merkle Trie use std::{ - convert::TryInto, + convert::{TryFrom, TryInto}, fmt::{self, Debug, Display, Formatter}, iter::Flatten, mem::MaybeUninit, @@ -9,7 +9,6 @@ use std::{ }; use datasize::DataSize; -use either::Either; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive, ToPrimitive}; use serde::{ @@ -511,40 +510,112 @@ impl Trie { } } -pub(crate) type LazyTrieLeaf = Either>; +/// Bytes representation of a `Trie` that is a `Trie::Leaf` variant. +/// The bytes for this trie leaf also include the `Trie::Tag`. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct TrieLeafBytes(Bytes); -pub(crate) fn lazy_trie_tag(bytes: &[u8]) -> Option { - bytes.first().copied().and_then(TrieTag::from_u8) +impl TrieLeafBytes { + pub(crate) fn bytes(&self) -> &Bytes { + &self.0 + } + + pub(crate) fn try_deserialize_leaf_key( + &self, + ) -> Result<(K, &[u8]), bytesrepr::Error> { + let (tag_byte, rem) = u8::from_bytes(&self.0)?; + let tag = TrieTag::from_u8(tag_byte).ok_or(bytesrepr::Error::Formatting)?; + assert_eq!( + tag, + TrieTag::Leaf, + "Unexpected layout for trie leaf bytes. Expected `TrieTag::Leaf` but got {:?}", + tag + ); + K::from_bytes(rem) + } } -pub(crate) fn lazy_trie_deserialize( - bytes: Bytes, -) -> Result, bytesrepr::Error> -where - K: FromBytes, - V: FromBytes, -{ - let trie_tag = lazy_trie_tag(&bytes); +impl From<&[u8]> for TrieLeafBytes { + fn from(value: &[u8]) -> Self { + Self(value.into()) + } +} - if trie_tag == Some(TrieTag::Leaf) { - Ok(Either::Left(bytes)) - } else { - let deserialized: Trie = bytesrepr::deserialize(bytes.into())?; - Ok(Either::Right(deserialized)) +impl From> for TrieLeafBytes { + fn from(value: Vec) -> Self { + Self(value.into()) } } -pub(crate) fn lazy_trie_iter_children( - trie_bytes: &LazyTrieLeaf, -) -> DescendantsIterator { - match trie_bytes { - Either::Left(_) => { - // Leaf bytes does not have any children - DescendantsIterator::ZeroOrOne(None) +/// Like `Trie` but does not deserialize the leaf when constructed. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum LazilyDeserializedTrie { + /// Serialized trie leaf bytes + Leaf(TrieLeafBytes), + /// Trie node. + Node { pointer_block: Box }, + /// Trie extension node. + Extension { affix: Bytes, pointer: Pointer }, +} + +impl LazilyDeserializedTrie { + pub(crate) fn iter_children(&self) -> DescendantsIterator { + match self { + LazilyDeserializedTrie::Leaf(_) => { + // Leaf bytes does not have any children + DescendantsIterator::ZeroOrOne(None) + } + LazilyDeserializedTrie::Node { pointer_block } => DescendantsIterator::PointerBlock { + iter: pointer_block.0.iter().flatten(), + }, + LazilyDeserializedTrie::Extension { pointer, .. } => { + DescendantsIterator::ZeroOrOne(Some(pointer.into_hash())) + } } - Either::Right(trie) => { - // Trie::Node or Trie::Extension has children - trie.iter_children() + } +} + +impl FromBytes for LazilyDeserializedTrie { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag_byte, rem) = u8::from_bytes(bytes)?; + let tag = TrieTag::from_u8(tag_byte).ok_or(bytesrepr::Error::Formatting)?; + match tag { + TrieTag::Leaf => Ok((LazilyDeserializedTrie::Leaf(bytes.into()), &[])), + TrieTag::Node => { + let (pointer_block, rem) = PointerBlock::from_bytes(rem)?; + Ok(( + LazilyDeserializedTrie::Node { + pointer_block: Box::new(pointer_block), + }, + rem, + )) + } + TrieTag::Extension => { + let (affix, rem) = FromBytes::from_bytes(rem)?; + let (pointer, rem) = Pointer::from_bytes(rem)?; + Ok((LazilyDeserializedTrie::Extension { affix, pointer }, rem)) + } + } + } +} + +impl TryFrom> for LazilyDeserializedTrie +where + K: ToBytes, + V: ToBytes, +{ + type Error = bytesrepr::Error; + + fn try_from(value: Trie) -> Result { + match value { + Trie::Leaf { .. } => { + let serialized_bytes = ToBytes::to_bytes(&value)?; + Ok(LazilyDeserializedTrie::Leaf(serialized_bytes.into())) + } + Trie::Node { pointer_block } => Ok(LazilyDeserializedTrie::Node { pointer_block }), + Trie::Extension { affix, pointer } => { + Ok(LazilyDeserializedTrie::Extension { affix, pointer }) + } } } } @@ -596,6 +667,8 @@ where } fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + // NOTE: When changing this make sure all partial deserializers that are referencing + // `LazyTrieLeaf` are also updated. writer.push(u8::from(self.tag())); match self { Trie::Leaf { key, value } => { @@ -640,6 +713,24 @@ impl FromBytes for Trie { } } +impl TryFrom for Trie { + type Error = bytesrepr::Error; + + fn try_from(value: LazilyDeserializedTrie) -> Result { + match value { + LazilyDeserializedTrie::Leaf(leaf_bytes) => { + let (key, value_bytes) = leaf_bytes.try_deserialize_leaf_key()?; + let value = bytesrepr::deserialize_from_slice(value_bytes)?; + Ok(Self::Leaf { key, value }) + } + LazilyDeserializedTrie::Node { pointer_block } => Ok(Self::Node { pointer_block }), + LazilyDeserializedTrie::Extension { affix, pointer } => { + Ok(Self::Extension { affix, pointer }) + } + } + } +} + pub(crate) mod operations { use casper_types::bytesrepr::{self, ToBytes}; diff --git a/execution_engine/src/storage/trie/tests.rs b/execution_engine/src/storage/trie/tests.rs index b0f87a43f0..b21169d5cb 100644 --- a/execution_engine/src/storage/trie/tests.rs +++ b/execution_engine/src/storage/trie/tests.rs @@ -92,12 +92,65 @@ mod pointer_block { } mod proptests { + use std::convert::TryInto; + use proptest::prelude::*; use casper_hashing::Digest; - use casper_types::{bytesrepr, gens::key_arb, Key, StoredValue}; + use casper_types::{ + bytesrepr::{self, deserialize_from_slice, FromBytes, ToBytes}, + gens::key_arb, + Key, StoredValue, + }; + + use crate::storage::trie::{gens::*, LazilyDeserializedTrie, PointerBlock, Trie}; + + fn test_trie_roundtrip_to_lazy_trie(trie: &Trie) + where + K: ToBytes + FromBytes + PartialEq + std::fmt::Debug + Clone, + V: ToBytes + FromBytes + PartialEq + std::fmt::Debug + Clone, + { + let serialized = ToBytes::to_bytes(trie).expect("Unable to serialize data"); + + let expected_lazy_trie_leaf: LazilyDeserializedTrie = (*trie) + .clone() + .try_into() + .expect("Cannot convert Trie to LazilyDeserializedTrie"); + + let deserialized_from_slice: LazilyDeserializedTrie = + deserialize_from_slice(&serialized).expect("Unable to deserialize data"); + assert_eq!(expected_lazy_trie_leaf, deserialized_from_slice); + assert_eq!( + *trie, + deserialized_from_slice + .clone() + .try_into() + .expect("Expected to be able to convert LazilyDeserializedTrie to Trie") + ); + if let LazilyDeserializedTrie::Leaf(leaf_bytes) = deserialized_from_slice { + let (key, _) = leaf_bytes + .try_deserialize_leaf_key::() + .expect("Should have been able to deserialize key"); + assert_eq!(key, *trie.key().unwrap()); + }; - use crate::storage::trie::{gens::*, PointerBlock, Trie}; + let deserialized: LazilyDeserializedTrie = + bytesrepr::deserialize(serialized).expect("Unable to deserialize data"); + assert_eq!(expected_lazy_trie_leaf, deserialized); + assert_eq!( + *trie, + deserialized + .clone() + .try_into() + .expect("Expected to be able to convert LazilyDeserializedTrie to Trie") + ); + if let LazilyDeserializedTrie::Leaf(leaf_bytes) = deserialized { + let (key, _) = leaf_bytes + .try_deserialize_leaf_key::() + .expect("Should have been able to deserialize key"); + assert_eq!(key, *trie.key().unwrap()); + }; + } proptest! { #[test] @@ -120,6 +173,21 @@ mod proptests { bytesrepr::test_serialization_roundtrip(&trie_leaf); } + #[test] + fn bytesrepr_roundtrip_trie_leaf_to_lazy_trie(trie_leaf in trie_leaf_arb()) { + test_trie_roundtrip_to_lazy_trie(&trie_leaf) + } + + #[test] + fn bytesrepr_roundtrip_trie_extension_to_lazy_trie(trie_extension in trie_extension_arb()) { + test_trie_roundtrip_to_lazy_trie(&trie_extension) + } + + #[test] + fn bytesrepr_roundtrip_trie_node_to_lazy_trie(trie_node in trie_node_arb()) { + test_trie_roundtrip_to_lazy_trie(&trie_node); + } + #[test] fn bytesrepr_roundtrip_trie_extension(trie_extension in trie_extension_arb()) { bytesrepr::test_serialization_roundtrip(&trie_extension); diff --git a/execution_engine/src/storage/trie_store/lmdb.rs b/execution_engine/src/storage/trie_store/lmdb.rs index 01131e3659..973539497c 100644 --- a/execution_engine/src/storage/trie_store/lmdb.rs +++ b/execution_engine/src/storage/trie_store/lmdb.rs @@ -122,7 +122,7 @@ use crate::storage::{ global_state::CommitError, store::Store, transaction_source::{lmdb::LmdbEnvironment, Readable, TransactionSource, Writable}, - trie::{self, LazyTrieLeaf, Trie}, + trie::{LazilyDeserializedTrie, Trie}, trie_store::{self, TrieStore}, }; @@ -219,9 +219,8 @@ impl ScratchTrieStore { continue; }; - let lazy_trie: LazyTrieLeaf = - trie::lazy_trie_deserialize(trie_bytes.clone())?; - tries_to_write.extend(trie::lazy_trie_iter_children(&lazy_trie)); + let lazy_trie: LazilyDeserializedTrie = bytesrepr::deserialize_from_slice(trie_bytes)?; + tries_to_write.extend(lazy_trie.iter_children()); Store::>::put_raw( &*self.store, diff --git a/execution_engine/src/storage/trie_store/operations/mod.rs b/execution_engine/src/storage/trie_store/operations/mod.rs index 6201aeeb4e..030d72435f 100644 --- a/execution_engine/src/storage/trie_store/operations/mod.rs +++ b/execution_engine/src/storage/trie_store/operations/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod store_wrappers; #[cfg(test)] mod tests; @@ -5,7 +6,6 @@ mod tests; use std::collections::HashSet; use std::{borrow::Cow, cmp, collections::VecDeque, convert::TryInto, mem}; -use either::Either; use num_traits::FromPrimitive; use tracing::{error, warn}; @@ -15,16 +15,19 @@ use casper_types::bytesrepr::{self, Bytes, FromBytes, ToBytes}; use crate::{ shared::newtypes::CorrelationId, storage::{ + store::Store, transaction_source::{Readable, Writable}, trie::{ - self, merkle_proof::{TrieMerkleProof, TrieMerkleProofStep}, - Parents, Pointer, PointerBlock, Trie, TrieTag, RADIX, USIZE_EXCEEDS_U8, + LazilyDeserializedTrie, Parents, Pointer, PointerBlock, Trie, TrieTag, RADIX, + USIZE_EXCEEDS_U8, }, trie_store::TrieStore, }, }; +use self::store_wrappers::NonDeserializingStore; + #[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq)] pub enum ReadResult { @@ -58,6 +61,8 @@ where { let path: Vec = key.to_bytes()?; + let store = store_wrappers::OnceDeserializingStore::new(store); + let mut depth: usize = 0; let mut current: Trie = match store.get(txn, root)? { Some(root) => root, @@ -289,60 +294,24 @@ where }) } -struct TrieScan { - tip: Trie, - parents: Parents, -} - -impl TrieScan { - fn new(tip: Trie, parents: Parents) -> Self { - TrieScan { tip, parents } - } -} - -/// Returns a [`TrieScan`] from the given key at a given root in a given store. -/// A scan consists of the deepest trie variant found at that key, a.k.a. the -/// "tip", along the with the parents of that variant. Parents are ordered by -/// their depth from the root (shallow to deep). -fn scan( - txn: &T, - store: &S, - key_bytes: &[u8], - root: &Trie, -) -> Result, E> -where - K: ToBytes + FromBytes + Clone, - V: ToBytes + FromBytes + Clone, - T: Readable, - S: TrieStore, - S::Error: From, - E: From + From, -{ - let root_bytes = root.to_bytes()?; - let TrieScanRaw { tip, parents } = - scan_raw::(txn, store, key_bytes, root_bytes.into())?; - let tip = match tip { - Either::Left(trie_leaf_bytes) => bytesrepr::deserialize(trie_leaf_bytes.to_vec())?, - Either::Right(tip) => tip, - }; - Ok(TrieScan::new(tip, parents)) -} - struct TrieScanRaw { - tip: Either>, + tip: LazilyDeserializedTrie, parents: Parents, } impl TrieScanRaw { - fn new(tip: Either>, parents: Parents) -> Self { + fn new(tip: LazilyDeserializedTrie, parents: Parents) -> Self { TrieScanRaw { tip, parents } } } -/// Just like scan, however we don't parse the tip. +/// Returns a [`TrieScanRaw`] from the given key at a given root in a given store. +/// A scan consists of the deepest trie variant found at that key, a.k.a. the +/// "tip", along the with the parents of that variant. Parents are ordered by +/// their depth from the root (shallow to deep). The tip is not parsed. fn scan_raw( txn: &T, - store: &S, + store: &NonDeserializingStore, key_bytes: &[u8], root_bytes: Bytes, ) -> Result, E> @@ -356,24 +325,17 @@ where { let path = key_bytes; - let mut current_trie; let mut current = root_bytes; let mut depth: usize = 0; let mut acc: Parents = Vec::new(); loop { - let maybe_trie_leaf = trie::lazy_trie_deserialize(current)?; - current_trie = match maybe_trie_leaf { - leaf_bytes @ Either::Left(_) => return Ok(TrieScanRaw::new(leaf_bytes, acc)), - Either::Right(trie_object) => trie_object, - }; - match current_trie { - _leaf @ Trie::Leaf { .. } => { - // since we are checking if this is a leaf and skipping, we do not expect to ever - // hit this. - unreachable!() + let maybe_trie_leaf = bytesrepr::deserialize_from_slice(¤t)?; + match maybe_trie_leaf { + leaf_bytes @ LazilyDeserializedTrie::Leaf(_) => { + return Ok(TrieScanRaw::new(leaf_bytes, acc)) } - Trie::Node { pointer_block } => { + LazilyDeserializedTrie::Node { pointer_block } => { let index = { assert!(depth < path.len(), "depth must be < {}", path.len()); path[depth] @@ -387,7 +349,7 @@ where Some(pointer) => pointer, None => { return Ok(TrieScanRaw::new( - Either::Right(Trie::Node { pointer_block }), + LazilyDeserializedTrie::Node { pointer_block }, acc, )); } @@ -407,11 +369,11 @@ where } } } - Trie::Extension { affix, pointer } => { + LazilyDeserializedTrie::Extension { affix, pointer } => { let sub_path = &path[depth..depth + affix.len()]; if sub_path != affix.as_slice() { return Ok(TrieScanRaw::new( - Either::Right(Trie::Extension { affix, pointer }), + LazilyDeserializedTrie::Extension { affix, pointer }, acc, )); } @@ -423,7 +385,7 @@ where }; current = next; depth += affix.len(); - acc.push((index, Trie::Extension { affix, pointer })) + acc.push((index, Trie::extension(affix.into(), pointer))) } None => { panic!( @@ -461,6 +423,7 @@ where S::Error: From, E: From + From, { + let store = store_wrappers::NonDeserializingStore::new(store); let root_trie_bytes = match store.get_raw(txn, root)? { None => return Ok(DeleteResult::RootNotFound), Some(root_trie) => root_trie, @@ -468,23 +431,16 @@ where let key_bytes = key_to_delete.to_bytes()?; let TrieScanRaw { tip, mut parents } = - scan_raw::<_, _, _, _, E>(txn, store, &key_bytes, root_trie_bytes)?; + scan_raw::<_, _, _, _, E>(txn, &store, &key_bytes, root_trie_bytes)?; // Check that tip is a leaf match tip { - Either::Left(bytes) + LazilyDeserializedTrie::Leaf(leaf_bytes) if { // Partially deserialize a key of a leaf node to ensure that we can only continue if // the key matches what we're looking for. - let ((tag_u8, key), _rem): ((u8, K), _) = FromBytes::from_bytes(&bytes)?; - let trie_tag = TrieTag::from_u8(tag_u8); // _rem contains bytes of serialized V, but we don't need to inspect it. - assert_eq!( - trie_tag, - Some(TrieTag::Leaf), - "Tip should contain leaf bytes, but has tag {:?}", - trie_tag - ); + let (key, _rem) = leaf_bytes.try_deserialize_leaf_key::()?; key == *key_to_delete } => {} _ => return Ok(DeleteResult::DoesNotExist), @@ -586,10 +542,8 @@ where // this extension might need to be combined with a grandparent // extension. Trie::Node { .. } => { - let new_extension: Trie = Trie::Extension { - affix: vec![sibling_idx].into(), - pointer: sibling_pointer, - }; + let new_extension: Trie = + Trie::extension(vec![sibling_idx], sibling_pointer); let trie_key = new_extension.trie_hash()?; new_elements.push((trie_key, new_extension)) } @@ -603,10 +557,7 @@ where } => { let mut new_affix = vec![sibling_idx]; new_affix.extend(Vec::::from(extension_affix)); - let new_extension: Trie = Trie::Extension { - affix: new_affix.into(), - pointer, - }; + let new_extension: Trie = Trie::extension(new_affix, pointer); let trie_key = new_extension.trie_hash()?; new_elements.push((trie_key, new_extension)) } @@ -642,10 +593,8 @@ where new_affix.extend_from_slice(child_affix.as_slice()); *child_affix = new_affix.into(); *trie_key = { - let new_extension: Trie = Trie::Extension { - affix: child_affix.to_owned(), - pointer: pointer.to_owned(), - }; + let new_extension: Trie = + Trie::extension(child_affix.to_owned().into(), pointer.to_owned()); new_extension.trie_hash()? } } @@ -922,57 +871,61 @@ where S::Error: From, E: From + From, { - match store.get(txn, root)? { + let store = store_wrappers::NonDeserializingStore::new(store); + match store.get_raw(txn, root)? { None => Ok(WriteResult::RootNotFound), - Some(current_root) => { + Some(current_root_bytes) => { let new_leaf = Trie::Leaf { key: key.to_owned(), value: value.to_owned(), }; let path: Vec = key.to_bytes()?; - let TrieScan { tip, parents } = - scan::(txn, store, &path, ¤t_root)?; + let TrieScanRaw { tip, parents } = + scan_raw::(txn, &store, &path, current_root_bytes)?; let new_elements: Vec<(Digest, Trie)> = match tip { - // If the "tip" is the same as the new leaf, then the leaf - // is already in the Trie. - Trie::Leaf { .. } if new_leaf == tip => Vec::new(), - // If the "tip" is an existing leaf with the same key as the - // new leaf, but the existing leaf and new leaf have different - // values, then we are in the situation where we are "updating" - // an existing leaf. - Trie::Leaf { - key: ref leaf_key, - value: ref leaf_value, - } if key == leaf_key && value != leaf_value => rehash(new_leaf, parents)?, - // If the "tip" is an existing leaf with a different key than - // the new leaf, then we are in a situation where the new leaf - // shares some common prefix with the existing leaf. - Trie::Leaf { - key: ref existing_leaf_key, - .. - } if key != existing_leaf_key => { - let existing_leaf_path = existing_leaf_key.to_bytes()?; - let (new_node, parents) = reparent_leaf(&path, &existing_leaf_path, parents)?; - let parents = add_node_to_parents(&path, new_node, parents); - rehash(new_leaf, parents)? + LazilyDeserializedTrie::Leaf(leaf_bytes) => { + let (existing_leaf_key, existing_value_bytes) = + leaf_bytes.try_deserialize_leaf_key()?; + + if key != &existing_leaf_key { + // If the "tip" is an existing leaf with a different key than + // the new leaf, then we are in a situation where the new leaf + // shares some common prefix with the existing leaf. + let existing_leaf_path = existing_leaf_key.to_bytes()?; + let (new_node, parents) = + reparent_leaf(&path, &existing_leaf_path, parents)?; + let parents = add_node_to_parents(&path, new_node, parents); + rehash(new_leaf, parents)? + } else { + let new_value_bytes = value.to_bytes()?; + if new_value_bytes != existing_value_bytes { + // If the "tip" is an existing leaf with the same key as the + // new leaf, but the existing leaf and new leaf have different + // values, then we are in the situation where we are "updating" + // an existing leaf. + rehash(new_leaf, parents)? + } else { + // Both key and values are the same. + // If the "tip" is the same as the new leaf, then the leaf + // is already in the Trie. + Vec::new() + } + } } - // This case is unreachable, but the compiler can't figure - // that out. - Trie::Leaf { .. } => unreachable!(), // If the "tip" is an existing node, then we can add a pointer // to the new leaf to the node's pointer block. - node @ Trie::Node { .. } => { - let parents = add_node_to_parents(&path, node, parents); + node @ LazilyDeserializedTrie::Node { .. } => { + let parents = add_node_to_parents(&path, node.try_into()?, parents); rehash(new_leaf, parents)? } // If the "tip" is an extension node, then we must modify or // replace it, adding a node where necessary. - extension @ Trie::Extension { .. } => { + extension @ LazilyDeserializedTrie::Extension { .. } => { let SplitResult { new_node, parents, maybe_hashed_child_extension, - } = split_extension(&path, extension, parents)?; + } = split_extension(&path, extension.try_into()?, parents)?; let parents = add_node_to_parents(&path, new_node, parents); if let Some(hashed_extension) = maybe_hashed_child_extension { let mut ret = vec![hashed_extension]; @@ -1026,16 +979,16 @@ enum KeysIteratorState> { Failed, } -struct VisitedTrieNode { - trie: Trie, +struct VisitedTrieNode { + trie: LazilyDeserializedTrie, maybe_index: Option, path: Vec, } pub struct KeysIterator<'a, 'b, K, V, T, S: TrieStore> { initial_descend: VecDeque, - visited: Vec>, - store: &'a S, + visited: Vec, + store: NonDeserializingStore<'a, K, V, S>, txn: &'b T, state: KeysIteratorState, } @@ -1067,25 +1020,37 @@ where mut path, }) = self.visited.pop() { - let mut maybe_next_trie: Option> = None; + let mut maybe_next_trie: Option = None; match trie { - Trie::Leaf { key, .. } => { - let key_bytes = match key.to_bytes() { - Ok(bytes) => bytes, - Err(e) => { - self.state = KeysIteratorState::Failed; - return Some(Err(e.into())); - } - }; - debug_assert!(key_bytes.starts_with(&path)); + LazilyDeserializedTrie::Leaf(leaf_bytes) => { + let leaf_bytes = leaf_bytes.bytes(); + if leaf_bytes.is_empty() { + self.state = KeysIteratorState::Failed; + return Some(Err(bytesrepr::Error::Formatting.into())); + } + + let key_bytes = &leaf_bytes[1..]; // Skip `Trie::Leaf` tag + debug_assert!( + key_bytes.starts_with(&path), + "Expected key bytes to start with the current path" + ); + // only return the leaf if it matches the initial descend path path.extend(&self.initial_descend); if key_bytes.starts_with(&path) { + // Only deserializes K when we're absolutely sure the path matches. + let (key, _stored_value): (K, _) = match K::from_bytes(key_bytes) { + Ok(key) => key, + Err(error) => { + self.state = KeysIteratorState::Failed; + return Some(Err(error.into())); + } + }; return Some(Ok(key)); } } - Trie::Node { ref pointer_block } => { + LazilyDeserializedTrie::Node { ref pointer_block } => { // if we are still initially descending (and initial_descend is not empty), take // the first index we should descend to, otherwise take maybe_index from the // visited stack @@ -1097,14 +1062,28 @@ where .unwrap_or_default(); while index < RADIX { if let Some(ref pointer) = pointer_block[index] { - maybe_next_trie = match self.store.get(self.txn, pointer.hash()) { - Ok(trie) => trie, - Err(e) => { - self.state = KeysIteratorState::Failed; - return Some(Err(e)); + maybe_next_trie = { + match self.store.get_raw(self.txn, pointer.hash()) { + Ok(Some(trie_bytes)) => { + match bytesrepr::deserialize_from_slice(&trie_bytes) { + Ok(lazy_trie) => Some(lazy_trie), + Err(error) => { + self.state = KeysIteratorState::Failed; + return Some(Err(error.into())); + } + } + } + Ok(None) => None, + Err(error) => { + self.state = KeysIteratorState::Failed; + return Some(Err(error)); + } } }; - debug_assert!(maybe_next_trie.is_some()); + debug_assert!( + maybe_next_trie.is_some(), + "Trie at the pointer is expected to exist" + ); if self.initial_descend.pop_front().is_none() { self.visited.push(VisitedTrieNode { trie, @@ -1124,7 +1103,7 @@ where index += 1; } } - Trie::Extension { affix, pointer } => { + LazilyDeserializedTrie::Extension { affix, pointer } => { let descend_len = cmp::min(self.initial_descend.len(), affix.len()); let check_prefix = self .initial_descend @@ -1135,14 +1114,27 @@ where // if we are not, the check_prefix will be empty, so we will enter the if // anyway if affix.starts_with(&check_prefix) { - maybe_next_trie = match self.store.get(self.txn, pointer.hash()) { - Ok(trie) => trie, + maybe_next_trie = match self.store.get_raw(self.txn, pointer.hash()) { + Ok(Some(trie_bytes)) => { + match bytesrepr::deserialize_from_slice(&trie_bytes) { + Ok(lazy_trie) => Some(lazy_trie), + Err(error) => { + self.state = KeysIteratorState::Failed; + return Some(Err(error.into())); + } + } + } + Ok(None) => None, Err(e) => { self.state = KeysIteratorState::Failed; return Some(Err(e)); } }; - debug_assert!({ matches!(&maybe_next_trie, Some(Trie::Node { .. })) }); + debug_assert!( + matches!(&maybe_next_trie, Some(LazilyDeserializedTrie::Node { .. }),), + "Expected a LazilyDeserializedTrie::Node but received {:?}", + maybe_next_trie + ); path.extend(affix); } } @@ -1177,17 +1169,24 @@ where S: TrieStore, S::Error: From, { - let (visited, init_state): (Vec>, _) = match store.get(txn, root) { + let store = store_wrappers::NonDeserializingStore::new(store); + let (visited, init_state): (Vec, _) = match store.get_raw(txn, root) { Ok(None) => (vec![], KeysIteratorState::Ok), Err(e) => (vec![], KeysIteratorState::ReturnError(e)), - Ok(Some(current_root)) => ( - vec![VisitedTrieNode { - trie: current_root, - maybe_index: None, - path: vec![], - }], - KeysIteratorState::Ok, - ), + Ok(Some(current_root_bytes)) => match bytesrepr::deserialize_from_slice(current_root_bytes) + { + Ok(lazy_trie) => { + let visited = vec![VisitedTrieNode { + trie: lazy_trie, + maybe_index: None, + path: vec![], + }]; + let init_state = KeysIteratorState::Ok; + + (visited, init_state) + } + Err(error) => (vec![], KeysIteratorState::ReturnError(error.into())), + }, }; KeysIterator { diff --git a/execution_engine/src/storage/trie_store/operations/store_wrappers.rs b/execution_engine/src/storage/trie_store/operations/store_wrappers.rs new file mode 100644 index 0000000000..2cb03b774e --- /dev/null +++ b/execution_engine/src/storage/trie_store/operations/store_wrappers.rs @@ -0,0 +1,240 @@ +use std::marker::PhantomData; +#[cfg(debug_assertions)] +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; + +use casper_hashing::Digest; +use casper_types::bytesrepr::{self, FromBytes, ToBytes}; + +use crate::storage::{ + store::Store, + transaction_source::{Readable, Writable}, + trie::Trie, + trie_store::TrieStore, +}; + +/// A [`TrieStore`] wrapper that panics in debug mode whenever an attempt to deserialize [`V`] is +/// made, otherwise it behaves as a [`TrieStore`]. +/// +/// To ensure this wrapper has zero overhead, a debug assertion is used. +pub(crate) struct NonDeserializingStore<'a, K, V, S>(&'a S, PhantomData<*const (K, V)>) +where + S: TrieStore; + +impl<'a, K, V, S> NonDeserializingStore<'a, K, V, S> +where + S: TrieStore, +{ + pub(crate) fn new(store: &'a S) -> Self { + Self(store, PhantomData) + } +} + +impl<'a, K, V, S> Store> for NonDeserializingStore<'a, K, V, S> +where + S: TrieStore, +{ + type Error = S::Error; + + type Handle = S::Handle; + + #[inline] + fn handle(&self) -> Self::Handle { + self.0.handle() + } + + #[inline] + fn deserialize_value(&self, bytes: &[u8]) -> Result, bytesrepr::Error> + where + Trie: FromBytes, + { + #[cfg(debug_assertions)] + { + let trie: Trie = self.0.deserialize_value(bytes)?; + if let Trie::Leaf { .. } = trie { + panic!("Tried to deserialize a value but expected no deserialization to happen.") + } + Ok(trie) + } + #[cfg(not(debug_assertions))] + { + self.0.deserialize_value(bytes) + } + } + + #[inline] + fn serialize_value(&self, value: &Trie) -> Result, bytesrepr::Error> + where + Trie: ToBytes, + { + self.0.serialize_value(value) + } + + #[inline] + fn get(&self, txn: &T, key: &Digest) -> Result>, Self::Error> + where + T: Readable, + Digest: AsRef<[u8]>, + Trie: FromBytes, + Self::Error: From, + { + self.0.get(txn, key) + } + + #[inline] + fn get_raw(&self, txn: &T, key: &Digest) -> Result, Self::Error> + where + T: Readable, + Digest: AsRef<[u8]>, + Self::Error: From, + { + self.0.get_raw(txn, key) + } + + #[inline] + fn put(&self, txn: &mut T, key: &Digest, value: &Trie) -> Result<(), Self::Error> + where + T: Writable, + Digest: AsRef<[u8]>, + Trie: ToBytes, + Self::Error: From, + { + self.0.put(txn, key, value) + } + + #[inline] + fn put_raw( + &self, + txn: &mut T, + key: &Digest, + value_bytes: std::borrow::Cow<'_, [u8]>, + ) -> Result<(), Self::Error> + where + T: Writable, + Digest: AsRef<[u8]>, + Self::Error: From, + { + self.0.put_raw(txn, key, value_bytes) + } +} + +pub(crate) struct OnceDeserializingStore<'a, K: ToBytes, V: ToBytes, S: TrieStore> { + store: &'a S, + #[cfg(debug_assertions)] + deserialize_tracking: Arc>>, + _marker: PhantomData<*const (K, V)>, +} + +impl<'a, K, V, S> OnceDeserializingStore<'a, K, V, S> +where + K: ToBytes, + V: ToBytes, + S: TrieStore, +{ + pub(crate) fn new(store: &'a S) -> Self { + Self { + store, + #[cfg(debug_assertions)] + deserialize_tracking: Arc::new(Mutex::new(HashSet::new())), + _marker: PhantomData, + } + } +} + +impl<'a, K, V, S> Store> for OnceDeserializingStore<'a, K, V, S> +where + K: ToBytes, + V: ToBytes, + S: TrieStore, +{ + type Error = S::Error; + + type Handle = S::Handle; + + #[inline] + fn handle(&self) -> Self::Handle { + self.store.handle() + } + + #[inline] + fn deserialize_value(&self, bytes: &[u8]) -> Result, bytesrepr::Error> + where + Trie: FromBytes, + { + #[cfg(debug_assertions)] + { + let trie: Trie = self.store.deserialize_value(bytes)?; + if let Trie::Leaf { .. } = trie { + let trie_hash = trie.trie_hash()?; + let mut tracking = self.deserialize_tracking.lock().expect("Poisoned lock"); + if tracking.get(&trie_hash).is_some() { + panic!("Tried to deserialize a value more than once."); + } else { + tracking.insert(trie_hash); + } + } + Ok(trie) + } + #[cfg(not(debug_assertions))] + { + self.store.deserialize_value(bytes) + } + } + + #[inline] + fn serialize_value(&self, value: &Trie) -> Result, bytesrepr::Error> + where + Trie: ToBytes, + { + self.store.serialize_value(value) + } + + #[inline] + fn get(&self, txn: &T, key: &Digest) -> Result>, Self::Error> + where + T: Readable, + Digest: AsRef<[u8]>, + Trie: FromBytes, + Self::Error: From, + { + self.store.get(txn, key) + } + + #[inline] + fn get_raw(&self, txn: &T, key: &Digest) -> Result, Self::Error> + where + T: Readable, + Digest: AsRef<[u8]>, + Self::Error: From, + { + self.store.get_raw(txn, key) + } + + #[inline] + fn put(&self, txn: &mut T, key: &Digest, value: &Trie) -> Result<(), Self::Error> + where + T: Writable, + Digest: AsRef<[u8]>, + Trie: ToBytes, + Self::Error: From, + { + self.store.put(txn, key, value) + } + + #[inline] + fn put_raw( + &self, + txn: &mut T, + key: &Digest, + value_bytes: std::borrow::Cow<'_, [u8]>, + ) -> Result<(), Self::Error> + where + T: Writable, + Digest: AsRef<[u8]>, + Self::Error: From, + { + self.store.put_raw(txn, key, value_bytes) + } +} diff --git a/execution_engine/src/storage/trie_store/operations/tests/bytesrepr_utils.rs b/execution_engine/src/storage/trie_store/operations/tests/bytesrepr_utils.rs new file mode 100644 index 0000000000..7c44d0f9af --- /dev/null +++ b/execution_engine/src/storage/trie_store/operations/tests/bytesrepr_utils.rs @@ -0,0 +1,43 @@ +use casper_types::bytesrepr::{self, FromBytes, ToBytes}; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub(crate) struct PanickingFromBytes(T); + +impl PanickingFromBytes { + pub(crate) fn new(inner: T) -> PanickingFromBytes { + PanickingFromBytes(inner) + } +} + +impl FromBytes for PanickingFromBytes +where + T: FromBytes, +{ + fn from_bytes(_: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + unreachable!("This type is expected to never deserialize."); + } +} + +impl ToBytes for PanickingFromBytes +where + T: ToBytes, +{ + fn into_bytes(self) -> Result, bytesrepr::Error> + where + Self: Sized, + { + self.0.into_bytes() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } + + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } +} diff --git a/execution_engine/src/storage/trie_store/operations/tests/delete.rs b/execution_engine/src/storage/trie_store/operations/tests/delete.rs index 6ab12a7549..cf661445fb 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/delete.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/delete.rs @@ -1,30 +1,41 @@ use super::*; -use crate::storage::{transaction_source::Writable, trie_store::operations::DeleteResult}; +use crate::storage::trie_store::operations::DeleteResult; -fn checked_delete( +fn checked_delete<'a, K, V, R, WR, S, WS, E>( correlation_id: CorrelationId, - txn: &mut T, + environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, root: &Digest, key_to_delete: &K, ) -> Result where K: ToBytes + FromBytes + Clone + std::fmt::Debug + Eq, V: ToBytes + FromBytes + Clone + std::fmt::Debug, - T: Readable + Writable, + R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, - S::Error: From, - E: From + From, + WS: TrieStore>, + S::Error: From, + WS::Error: From, + E: From + From + From + From + From, { - let _counter = TestValue::before_operation(TestOperation::Delete); - let delete_result = - operations::delete::(correlation_id, txn, store, root, key_to_delete); - let counter = TestValue::after_operation(TestOperation::Delete); - assert_eq!(counter, 0, "Delete should never deserialize a value"); + let mut txn = write_environment.create_read_write_txn()?; + let delete_result = operations::delete::, _, WS, E>( + correlation_id, + &mut txn, + write_store, + root, + key_to_delete, + ); + txn.commit()?; let delete_result = delete_result?; + let rtxn = environment.create_read_write_txn()?; if let DeleteResult::Deleted(new_root) = delete_result { - operations::check_integrity::(correlation_id, txn, store, vec![new_root])?; + operations::check_integrity::(correlation_id, &rtxn, store, vec![new_root])?; } + rtxn.commit()?; Ok(delete_result) } @@ -32,10 +43,13 @@ mod partial_tries { use super::*; use crate::storage::trie_store::operations::DeleteResult; - fn delete_from_partial_trie_had_expected_results<'a, K, V, R, S, E>( + #[allow(clippy::too_many_arguments)] + fn delete_from_partial_trie_had_expected_results<'a, K, V, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, root: &Digest, key_to_delete: &K, expected_root_after_delete: &Digest, @@ -45,17 +59,27 @@ mod partial_tries { K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { - let mut txn = environment.create_read_write_txn()?; + let rtxn = environment.create_read_txn()?; // The assert below only works with partial tries - assert_eq!(store.get(&txn, expected_root_after_delete)?, None); - let root_after_delete = match checked_delete::( + assert_eq!(store.get(&rtxn, expected_root_after_delete)?, None); + rtxn.commit()?; + let root_after_delete = match checked_delete::( correlation_id, - &mut txn, + environment, + write_environment, store, + write_store, root, key_to_delete, )? { @@ -64,9 +88,11 @@ mod partial_tries { DeleteResult::RootNotFound => panic!("root should be found"), }; assert_eq!(root_after_delete, *expected_root_after_delete); + let rtxn = environment.create_read_txn()?; for HashedTrie { hash, trie } in expected_tries_after_delete { - assert_eq!(store.get(&txn, hash)?, Some(trie.clone())); + assert_eq!(store.get(&rtxn, hash)?, Some(trie.clone())); } + rtxn.commit()?; Ok(()) } @@ -79,9 +105,19 @@ mod partial_tries { let key_to_delete = &TEST_LEAVES[i]; let context = LmdbTestContext::new(&initial_tries).unwrap(); - delete_from_partial_trie_had_expected_results::( + delete_from_partial_trie_had_expected_results::< + TestKey, + TestValue, + _, + _, + _, + _, + error::Error, + >( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_root_hash, key_to_delete.key().unwrap(), @@ -101,9 +137,19 @@ mod partial_tries { let key_to_delete = &TEST_LEAVES[i]; let context = InMemoryTestContext::new(&initial_tries).unwrap(); - delete_from_partial_trie_had_expected_results::( + delete_from_partial_trie_had_expected_results::< + TestKey, + TestValue, + _, + _, + _, + _, + error::Error, + >( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_root_hash, key_to_delete.key().unwrap(), @@ -114,10 +160,21 @@ mod partial_tries { } } - fn delete_non_existent_key_from_partial_trie_should_return_does_not_exist<'a, K, V, R, S, E>( + fn delete_non_existent_key_from_partial_trie_should_return_does_not_exist< + 'a, + K, + V, + R, + WR, + S, + WS, + E, + >( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, root: &Digest, key_to_delete: &K, ) -> Result<(), E> @@ -125,13 +182,26 @@ mod partial_tries { K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { - let mut txn = environment.create_read_write_txn()?; - match checked_delete::(correlation_id, &mut txn, store, root, key_to_delete)? - { + match checked_delete::( + correlation_id, + environment, + write_environment, + store, + write_store, + root, + key_to_delete, + )? { DeleteResult::Deleted(_) => panic!("should not delete"), DeleteResult::DoesNotExist => Ok(()), DeleteResult::RootNotFound => panic!("root should be found"), @@ -151,10 +221,14 @@ mod partial_tries { TestValue, _, _, + _, + _, error::Error, >( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_root_hash, key_to_delete.key().unwrap(), @@ -176,10 +250,14 @@ mod partial_tries { TestValue, _, _, + _, + _, error::Error, >( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_root_hash, key_to_delete.key().unwrap(), @@ -190,6 +268,7 @@ mod partial_tries { } mod full_tries { + use super::*; use std::ops::RangeInclusive; use proptest::{collection, prelude::*}; @@ -209,7 +288,7 @@ mod full_tries { operations::{ delete, tests::{ - InMemoryTestContext, LmdbTestContext, TestKey, TestOperation, TestValue, + InMemoryTestContext, LmdbTestContext, TestKey, TestValue, TEST_TRIE_GENERATORS, }, write, DeleteResult, WriteResult, @@ -231,21 +310,23 @@ mod full_tries { K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, R: TransactionSource<'a, Handle = S::Handle>, - S: TrieStore, + S: TrieStore>, S::Error: From, E: From + From + From, { - let mut txn = environment.create_read_write_txn()?; + let mut txn: R::ReadWriteTransaction = environment.create_read_write_txn()?; + let mut roots = Vec::new(); // Insert the key-value pairs, keeping track of the roots as we go for (key, value) in pairs { - if let WriteResult::Written(new_root) = write::( + let new_value = PanickingFromBytes::new(value.clone()); + if let WriteResult::Written(new_root) = write::, _, _, E>( correlation_id, &mut txn, store, roots.last().unwrap_or(root), key, - value, + &new_value, )? { roots.push(new_root); } else { @@ -255,11 +336,13 @@ mod full_tries { // Delete the key-value pairs, checking the resulting roots as we go let mut current_root = roots.pop().unwrap_or_else(|| root.to_owned()); for (key, _value) in pairs.iter().rev() { - let _counter = TestValue::before_operation(TestOperation::Delete); - let delete_result = - delete::(correlation_id, &mut txn, store, ¤t_root, key); - let counter = TestValue::after_operation(TestOperation::Delete); - assert_eq!(counter, 0, "Delete should never deserialize a value"); + let delete_result = delete::, _, _, E>( + correlation_id, + &mut txn, + store, + ¤t_root, + key, + ); if let DeleteResult::Deleted(new_root) = delete_result? { current_root = roots.pop().unwrap_or_else(|| root.to_owned()); assert_eq!(new_root, current_root); @@ -332,28 +415,36 @@ mod full_tries { K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, R: TransactionSource<'a, Handle = S::Handle>, - S: TrieStore, + S: TrieStore>, S::Error: From, E: From + From + From, { - let mut txn = environment.create_read_write_txn()?; + let mut txn: R::ReadWriteTransaction = environment.create_read_write_txn()?; let mut expected_root = *root; // Insert the key-value pairs, keeping track of the roots as we go for (key, value) in pairs_to_insert.iter() { - if let WriteResult::Written(new_root) = - write::(correlation_id, &mut txn, store, &expected_root, key, value)? - { + let new_value = PanickingFromBytes::new(value.clone()); + if let WriteResult::Written(new_root) = write::, _, _, E>( + correlation_id, + &mut txn, + store, + &expected_root, + key, + &new_value, + )? { expected_root = new_root; } else { panic!("Could not write pair") } } for key in keys_to_delete.iter() { - let _counter = TestValue::before_operation(TestOperation::Delete); - let delete_result = - delete::(correlation_id, &mut txn, store, &expected_root, key); - let counter = TestValue::after_operation(TestOperation::Delete); - assert_eq!(counter, 0, "Delete should never deserialize a value"); + let delete_result = delete::, _, _, E>( + correlation_id, + &mut txn, + store, + &expected_root, + key, + ); match delete_result? { DeleteResult::Deleted(new_root) => { expected_root = new_root; @@ -372,9 +463,15 @@ mod full_tries { let mut actual_root = *root; for (key, value) in pairs_to_insert_less_deleted.iter() { - if let WriteResult::Written(new_root) = - write::(correlation_id, &mut txn, store, &actual_root, key, value)? - { + let new_value = PanickingFromBytes::new(value.clone()); + if let WriteResult::Written(new_root) = write::, _, _, E>( + correlation_id, + &mut txn, + store, + &actual_root, + key, + &new_value, + )? { actual_root = new_root; } else { panic!("Could not write pair") diff --git a/execution_engine/src/storage/trie_store/operations/tests/ee_699.rs b/execution_engine/src/storage/trie_store/operations/tests/ee_699.rs index 6d8927ac91..c6c89aed96 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/ee_699.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/ee_699.rs @@ -302,10 +302,14 @@ mod empty_tries { _, _, _, + _, + _, in_memory::Error, >( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, &TEST_LEAVES, diff --git a/execution_engine/src/storage/trie_store/operations/tests/keys.rs b/execution_engine/src/storage/trie_store/operations/tests/keys.rs index 32aa55dee7..3ebd8d112f 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/keys.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/keys.rs @@ -1,4 +1,5 @@ mod partial_tries { + use crate::{ shared::newtypes::CorrelationId, storage::{ @@ -7,8 +8,8 @@ mod partial_tries { trie_store::operations::{ self, tests::{ - InMemoryTestContext, LmdbTestContext, TestKey, TestValue, TEST_LEAVES, - TEST_TRIE_GENERATORS, + bytesrepr_utils::PanickingFromBytes, InMemoryTestContext, LmdbTestContext, + TestKey, TestValue, TEST_LEAVES, TEST_TRIE_GENERATORS, }, }, }, @@ -34,7 +35,7 @@ mod partial_tries { }; let actual = { let txn = context.environment.create_read_txn().unwrap(); - let mut tmp = operations::keys::( + let mut tmp = operations::keys::, _, _>( correlation_id, &txn, &context.store, @@ -70,7 +71,7 @@ mod partial_tries { }; let actual = { let txn = context.environment.create_read_txn().unwrap(); - let mut tmp = operations::keys::( + let mut tmp = operations::keys::, _, _>( correlation_id, &txn, &context.store, @@ -88,6 +89,7 @@ mod partial_tries { } mod full_tries { + use casper_hashing::Digest; use crate::{ @@ -98,8 +100,8 @@ mod full_tries { trie_store::operations::{ self, tests::{ - InMemoryTestContext, TestKey, TestValue, EMPTY_HASHED_TEST_TRIES, TEST_LEAVES, - TEST_TRIE_GENERATORS, + bytesrepr_utils::PanickingFromBytes, InMemoryTestContext, TestKey, TestValue, + EMPTY_HASHED_TEST_TRIES, TEST_LEAVES, TEST_TRIE_GENERATORS, }, }, }, @@ -131,7 +133,7 @@ mod full_tries { }; let actual = { let txn = context.environment.create_read_txn().unwrap(); - let mut tmp = operations::keys::( + let mut tmp = operations::keys::, _, _>( correlation_id, &txn, &context.store, @@ -162,8 +164,8 @@ mod keys_iterator { trie_store::operations::{ self, tests::{ - hash_test_tries, HashedTestTrie, HashedTrie, InMemoryTestContext, TestKey, - TestValue, TEST_LEAVES, + bytesrepr_utils::PanickingFromBytes, hash_test_tries, HashedTestTrie, + HashedTrie, InMemoryTestContext, TestKey, TestValue, TEST_LEAVES, }, }, }, @@ -221,7 +223,7 @@ mod keys_iterator { let correlation_id = CorrelationId::new(); let context = return_on_err!(InMemoryTestContext::new(&tries)); let txn = return_on_err!(context.environment.create_read_txn()); - let _tmp = operations::keys::( + let _tmp = operations::keys::, _, _>( correlation_id, &txn, &context.store, @@ -231,21 +233,21 @@ mod keys_iterator { } #[test] - #[should_panic] + #[should_panic = "Expected a LazilyDeserializedTrie::Node but received"] fn should_panic_on_leaf_after_extension() { let (root_hash, tries) = return_on_err!(create_invalid_extension_trie()); test_trie(root_hash, tries); } #[test] - #[should_panic] + #[should_panic = "Expected key bytes to start with the current path"] fn should_panic_when_key_not_matching_path() { let (root_hash, tries) = return_on_err!(create_invalid_path_trie()); test_trie(root_hash, tries); } #[test] - #[should_panic] + #[should_panic = "Trie at the pointer is expected to exist"] fn should_panic_on_pointer_to_nonexisting_hash() { let (root_hash, tries) = return_on_err!(create_invalid_hash_trie()); test_trie(root_hash, tries); @@ -253,6 +255,7 @@ mod keys_iterator { } mod keys_with_prefix_iterator { + use crate::{ shared::newtypes::CorrelationId, storage::{ @@ -260,7 +263,10 @@ mod keys_with_prefix_iterator { trie::Trie, trie_store::operations::{ self, - tests::{create_6_leaf_trie, InMemoryTestContext, TestKey, TestValue, TEST_LEAVES}, + tests::{ + bytesrepr_utils::PanickingFromBytes, create_6_leaf_trie, InMemoryTestContext, + TestKey, TestValue, TEST_LEAVES, + }, }, }, }; @@ -285,15 +291,16 @@ mod keys_with_prefix_iterator { .create_read_txn() .expect("should create a read txn"); let expected = expected_keys(prefix); - let mut actual = operations::keys_with_prefix::( - correlation_id, - &txn, - &context.store, - &root_hash, - prefix, - ) - .filter_map(Result::ok) - .collect::>(); + let mut actual = + operations::keys_with_prefix::, _, _>( + correlation_id, + &txn, + &context.store, + &root_hash, + prefix, + ) + .filter_map(Result::ok) + .collect::>(); actual.sort(); assert_eq!(expected, actual); } diff --git a/execution_engine/src/storage/trie_store/operations/tests/mod.rs b/execution_engine/src/storage/trie_store/operations/tests/mod.rs index f4d6591331..21a3fd46b1 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/mod.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod bytesrepr_utils; mod delete; mod ee_699; mod keys; @@ -7,12 +8,7 @@ mod scan; mod synchronize; mod write; -use std::{ - cell::RefCell, - collections::{BTreeMap, HashMap}, - convert, - ops::Not, -}; +use std::{collections::HashMap, convert, ops::Not}; use lmdb::DatabaseFlags; use tempfile::{tempdir, TempDir}; @@ -40,6 +36,8 @@ use crate::{ }, }; +use self::bytesrepr_utils::PanickingFromBytes; + const TEST_KEY_LENGTH: usize = 7; /// A short key type for tests. @@ -67,57 +65,10 @@ impl FromBytes for TestKey { const TEST_VAL_LENGTH: usize = 6; -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub(crate) enum TestOperation { - Delete, // Deleting an existing value should not deserialize V -} - -type Counter = BTreeMap; - -thread_local! { - static FROMBYTES_INSIDE_OPERATION: RefCell = RefCell::new(Default::default()); - static FROMBYTES_COUNTER: RefCell = RefCell::new(Default::default()); -} - /// A short value type for tests. #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct TestValue([u8; TEST_VAL_LENGTH]); -impl TestValue { - pub(crate) fn before_operation(op: TestOperation) -> usize { - FROMBYTES_INSIDE_OPERATION.with(|flag| { - *flag.borrow_mut().entry(op).or_default() += 1; - }); - - FROMBYTES_COUNTER.with(|counter| { - let mut counter = counter.borrow_mut(); - let old = counter.get(&op).copied().unwrap_or_default(); - *counter.entry(op).or_default() = 0; - old - }) - } - - pub(crate) fn after_operation(op: TestOperation) -> usize { - FROMBYTES_INSIDE_OPERATION.with(|flag| { - *flag.borrow_mut().get_mut(&op).unwrap() -= 1; - }); - - FROMBYTES_COUNTER.with(|counter| counter.borrow().get(&op).copied().unwrap()) - } - - pub(crate) fn increment() { - let flag = FROMBYTES_INSIDE_OPERATION.with(|flag| flag.borrow().clone()); - let op = TestOperation::Delete; - if let Some(value) = flag.get(&op) { - if *value > 0 { - FROMBYTES_COUNTER.with(|counter| { - *counter.borrow_mut().entry(op).or_default() += 1; - }); - } - } - } -} - impl ToBytes for TestValue { fn to_bytes(&self) -> Result, bytesrepr::Error> { Ok(self.0.to_vec()) @@ -134,8 +85,6 @@ impl FromBytes for TestValue { let mut ret = [0u8; TEST_VAL_LENGTH]; ret.copy_from_slice(key); - TestValue::increment(); - Ok((TestValue(ret), rem)) } } @@ -651,7 +600,9 @@ where if let Trie::Leaf { key, value } = leaf { let maybe_value: ReadResult = read::<_, _, _, _, E>(correlation_id, txn, store, root, key)?; - ret.push(ReadResult::Found(*value) == maybe_value) + if let ReadResult::Found(value_found) = maybe_value { + ret.push(*value == value_found); + } } else { panic!("leaves should only contain leaves") } @@ -698,7 +649,7 @@ where Ok(ret) } -fn check_keys( +fn check_keys( correlation_id: CorrelationId, txn: &T, store: &S, @@ -711,7 +662,6 @@ where T: Readable, S: TrieStore, S::Error: From, - E: From + From, { let expected = { let mut tmp = leaves @@ -774,7 +724,7 @@ where .all(bool::not) ); - assert!(check_keys::<_, _, _, _, E>( + assert!(check_keys::<_, _, _, _>( correlation_id, &txn, store, @@ -797,7 +747,7 @@ where K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq, R: TransactionSource<'a, Handle = S::Handle>, - S: TrieStore, + S: TrieStore>, S::Error: From, E: From + From + From, { @@ -806,12 +756,19 @@ where return Ok(results); } let mut root_hash = root_hash.to_owned(); - let mut txn = environment.create_read_write_txn()?; + let mut txn: R::ReadWriteTransaction = environment.create_read_write_txn()?; for leaf in leaves.iter() { if let Trie::Leaf { key, value } = leaf { - let write_result = - write::<_, _, _, _, E>(correlation_id, &mut txn, store, &root_hash, key, value)?; + let new_value = PanickingFromBytes::new(value.clone()); + let write_result = write::, _, _, E>( + correlation_id, + &mut txn, + store, + &root_hash, + key, + &new_value, + )?; match write_result { WriteResult::Written(hash) => { root_hash = hash; @@ -878,10 +835,11 @@ where S::Error: From, E: From + From + From, { - let txn = environment.create_read_txn()?; + let txn: R::ReadTransaction = environment.create_read_txn()?; for (index, root_hash) in root_hashes.iter().enumerate() { for (key, value) in &pairs[..=index] { let result = read::<_, _, _, _, E>(correlation_id, &txn, store, root_hash, key)?; + if ReadResult::Found(*value) != result { return Ok(false); } @@ -920,7 +878,7 @@ where K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug, V: ToBytes + FromBytes + Clone + Eq, R: TransactionSource<'a, Handle = S::Handle>, - S: TrieStore, + S: TrieStore>, S::Error: From, E: From + From + From, { @@ -932,7 +890,15 @@ where let mut txn = environment.create_read_write_txn()?; for (key, value) in pairs.iter() { - match write::<_, _, _, _, E>(correlation_id, &mut txn, store, &root_hash, key, value)? { + let new_val = PanickingFromBytes::new(value.clone()); + match write::, _, _, E>( + correlation_id, + &mut txn, + store, + &root_hash, + key, + &new_val, + )? { WriteResult::Written(hash) => { root_hash = hash; } @@ -945,10 +911,12 @@ where Ok(results) } -fn writes_to_n_leaf_empty_trie_had_expected_results<'a, K, V, R, S, E>( +fn writes_to_n_leaf_empty_trie_had_expected_results<'a, K, V, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + writable_environment: &'a WR, store: &S, + writable_store: &WS, states: &[Digest], test_leaves: &[Trie], ) -> Result, E> @@ -956,17 +924,20 @@ where K: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug + Copy + Ord, V: ToBytes + FromBytes + Clone + Eq + std::fmt::Debug + Copy, R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + From + From + From + From, { let mut states = states.to_vec(); // Write set of leaves to the trie let hashes = write_leaves::<_, _, _, _, E>( correlation_id, - environment, - store, + writable_environment, + writable_store, states.last().unwrap(), test_leaves, )? diff --git a/execution_engine/src/storage/trie_store/operations/tests/scan.rs b/execution_engine/src/storage/trie_store/operations/tests/scan.rs index 5d8b74d7ea..14cfaa8816 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/scan.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/scan.rs @@ -1,3 +1,5 @@ +use std::convert::TryInto; + use casper_hashing::Digest; use super::*; @@ -5,7 +7,8 @@ use crate::{ shared::newtypes::CorrelationId, storage::{ error::{self, in_memory}, - trie_store::operations::{scan, TrieScan}, + trie::LazilyDeserializedTrie, + trie_store::operations::{scan_raw, store_wrappers, TrieScanRaw}, }, }; @@ -26,29 +29,51 @@ where let root = store .get(&txn, root_hash)? .expect("check_scan received an invalid root hash"); - let TrieScan { mut tip, parents } = - scan::(&txn, store, key, &root)?; + let root_bytes = root.to_bytes()?; + let store = store_wrappers::NonDeserializingStore::new(store); + let TrieScanRaw { mut tip, parents } = scan_raw::( + &txn, + &store, + key, + root_bytes.into(), + )?; for (index, parent) in parents.into_iter().rev() { let expected_tip_hash = { - let tip_bytes = tip.to_bytes().unwrap(); - Digest::hash(&tip_bytes) + match tip { + LazilyDeserializedTrie::Leaf(leaf_bytes) => Digest::hash(leaf_bytes.bytes()), + node @ LazilyDeserializedTrie::Node { .. } + | node @ LazilyDeserializedTrie::Extension { .. } => { + let tip_bytes = TryInto::>::try_into(node)? + .to_bytes() + .unwrap(); + Digest::hash(&tip_bytes) + } + } }; match parent { Trie::Leaf { .. } => panic!("parents should not contain any leaves"), Trie::Node { pointer_block } => { let pointer_tip_hash = pointer_block[::from(index)].map(|ptr| *ptr.hash()); assert_eq!(Some(expected_tip_hash), pointer_tip_hash); - tip = Trie::Node { pointer_block }; + tip = LazilyDeserializedTrie::Node { pointer_block }; } Trie::Extension { affix, pointer } => { let pointer_tip_hash = pointer.hash().to_owned(); assert_eq!(expected_tip_hash, pointer_tip_hash); - tip = Trie::Extension { affix, pointer }; + tip = LazilyDeserializedTrie::Extension { affix, pointer }; } } } - assert_eq!(root, tip); + + assert!( + matches!( + tip, + LazilyDeserializedTrie::Node { .. } | LazilyDeserializedTrie::Extension { .. }, + ), + "Unexpected leaf found" + ); + assert_eq!(root, tip.try_into()?); txn.commit()?; Ok(()) } diff --git a/execution_engine/src/storage/trie_store/operations/tests/write.rs b/execution_engine/src/storage/trie_store/operations/tests/write.rs index 314fdedd7c..1c4e0917a9 100644 --- a/execution_engine/src/storage/trie_store/operations/tests/write.rs +++ b/execution_engine/src/storage/trie_store/operations/tests/write.rs @@ -13,9 +13,11 @@ mod empty_tries { let context = LmdbTestContext::new(&tries).unwrap(); let initial_states = vec![root_hash]; - writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, error::Error>( + writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, &TEST_LEAVES_NON_COLLIDING[..num_leaves], @@ -32,9 +34,11 @@ mod empty_tries { let context = InMemoryTestContext::new(&tries).unwrap(); let initial_states = vec![root_hash]; - writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, in_memory::Error>( + writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, &TEST_LEAVES_NON_COLLIDING[..num_leaves], @@ -51,9 +55,11 @@ mod empty_tries { let context = LmdbTestContext::new(&tries).unwrap(); let initial_states = vec![root_hash]; - writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, error::Error>( + writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, &TEST_LEAVES[..num_leaves], @@ -70,9 +76,11 @@ mod empty_tries { let context = InMemoryTestContext::new(&tries).unwrap(); let initial_states = vec![root_hash]; - writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, in_memory::Error>( + writes_to_n_leaf_empty_trie_had_expected_results::<_, _, _, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, &TEST_LEAVES[..num_leaves], @@ -118,18 +126,27 @@ mod empty_tries { mod partial_tries { use super::*; - fn noop_writes_to_n_leaf_partial_trie_had_expected_results<'a, R, S, E>( + fn noop_writes_to_n_leaf_partial_trie_had_expected_results<'a, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + writable_store: &WS, states: &[Digest], num_leaves: usize, ) -> Result<(), E> where R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { // Check that the expected set of leaves is in the trie check_leaves::<_, _, _, _, E>( @@ -142,10 +159,10 @@ mod partial_tries { )?; // Rewrite that set of leaves - let write_results = write_leaves::<_, _, _, _, E>( + let write_results = write_leaves::( correlation_id, - environment, - store, + write_environment, + writable_store, &states[0], &TEST_LEAVES[..num_leaves], )?; @@ -173,9 +190,11 @@ mod partial_tries { let context = LmdbTestContext::new(&tries).unwrap(); let states = vec![root_hash]; - noop_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, error::Error>( + noop_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, num_leaves, @@ -192,9 +211,11 @@ mod partial_tries { let context = InMemoryTestContext::new(&tries).unwrap(); let states = vec![root_hash]; - noop_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, in_memory::Error>( + noop_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, num_leaves, @@ -203,18 +224,27 @@ mod partial_tries { } } - fn update_writes_to_n_leaf_partial_trie_had_expected_results<'a, R, S, E>( + fn update_writes_to_n_leaf_partial_trie_had_expected_results<'a, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + writable_store: &WS, states: &[Digest], num_leaves: usize, ) -> Result<(), E> where R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { let mut states = states.to_owned(); @@ -243,8 +273,8 @@ mod partial_tries { let current_root = states.last().unwrap(); let results = write_leaves::<_, _, _, _, E>( correlation_id, - environment, - store, + write_environment, + writable_store, current_root, &[leaf.to_owned()], )?; @@ -279,9 +309,11 @@ mod partial_tries { let context = LmdbTestContext::new(&tries).unwrap(); let initial_states = vec![root_hash]; - update_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, error::Error>( + update_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &initial_states, num_leaves, @@ -298,9 +330,11 @@ mod partial_tries { let context = InMemoryTestContext::new(&tries).unwrap(); let states = vec![root_hash]; - update_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, in_memory::Error>( + update_writes_to_n_leaf_partial_trie_had_expected_results::<_, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, num_leaves, @@ -313,18 +347,27 @@ mod partial_tries { mod full_tries { use super::*; - fn noop_writes_to_n_leaf_full_trie_had_expected_results<'a, R, S, E>( + fn noop_writes_to_n_leaf_full_trie_had_expected_results<'a, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, states: &[Digest], index: usize, ) -> Result<(), E> where R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { // Check that the expected set of leaves is in the trie at every state reference for (num_leaves, state) in states[..index].iter().enumerate() { @@ -341,8 +384,8 @@ mod full_tries { // Rewrite that set of leaves let write_results = write_leaves::<_, _, _, _, E>( correlation_id, - environment, - store, + write_environment, + write_store, states.last().unwrap(), &TEST_LEAVES[..index], )?; @@ -377,9 +420,11 @@ mod full_tries { context.update(&tries).unwrap(); states.push(root_hash); - noop_writes_to_n_leaf_full_trie_had_expected_results::<_, _, error::Error>( + noop_writes_to_n_leaf_full_trie_had_expected_results::<_, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, index, @@ -399,9 +444,11 @@ mod full_tries { context.update(&tries).unwrap(); states.push(root_hash); - noop_writes_to_n_leaf_full_trie_had_expected_results::<_, _, in_memory::Error>( + noop_writes_to_n_leaf_full_trie_had_expected_results::<_, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, index, @@ -410,18 +457,27 @@ mod full_tries { } } - fn update_writes_to_n_leaf_full_trie_had_expected_results<'a, R, S, E>( + fn update_writes_to_n_leaf_full_trie_had_expected_results<'a, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, states: &[Digest], num_leaves: usize, ) -> Result<(), E> where R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { let mut states = states.to_vec(); @@ -440,8 +496,8 @@ mod full_tries { // Write set of leaves to the trie let hashes = write_leaves::<_, _, _, _, E>( correlation_id, - environment, - store, + write_environment, + write_store, states.last().unwrap(), &TEST_LEAVES_UPDATED[..num_leaves], )? @@ -501,9 +557,11 @@ mod full_tries { context.update(&tries).unwrap(); states.push(root_hash); - update_writes_to_n_leaf_full_trie_had_expected_results::<_, _, error::Error>( + update_writes_to_n_leaf_full_trie_had_expected_results::<_, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, num_leaves, @@ -523,9 +581,11 @@ mod full_tries { context.update(&tries).unwrap(); states.push(root_hash); - update_writes_to_n_leaf_full_trie_had_expected_results::<_, _, in_memory::Error>( + update_writes_to_n_leaf_full_trie_had_expected_results::<_, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, num_leaves, @@ -534,17 +594,26 @@ mod full_tries { } } - fn node_writes_to_5_leaf_full_trie_had_expected_results<'a, R, S, E>( + fn node_writes_to_5_leaf_full_trie_had_expected_results<'a, R, WR, S, WS, E>( correlation_id: CorrelationId, environment: &'a R, + write_environment: &'a WR, store: &S, + write_store: &WS, states: &[Digest], ) -> Result<(), E> where R: TransactionSource<'a, Handle = S::Handle>, + WR: TransactionSource<'a, Handle = WS::Handle>, S: TrieStore, + WS: TrieStore>, S::Error: From, - E: From + From + From, + WS::Error: From, + E: From + + From + + From + + From + + From, { let mut states = states.to_vec(); let num_leaves = TEST_LEAVES_LENGTH; @@ -564,8 +633,8 @@ mod full_tries { // Write set of leaves to the trie let hashes = write_leaves::<_, _, _, _, E>( correlation_id, - environment, - store, + write_environment, + write_store, states.last().unwrap(), &TEST_LEAVES_ADJACENTS, )? @@ -625,9 +694,11 @@ mod full_tries { states.push(root_hash); } - node_writes_to_5_leaf_full_trie_had_expected_results::<_, _, error::Error>( + node_writes_to_5_leaf_full_trie_had_expected_results::<_, _, _, _, error::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, ) @@ -646,9 +717,11 @@ mod full_tries { states.push(root_hash); } - node_writes_to_5_leaf_full_trie_had_expected_results::<_, _, in_memory::Error>( + node_writes_to_5_leaf_full_trie_had_expected_results::<_, _, _, _, in_memory::Error>( correlation_id, &context.environment, + &context.environment, + &context.store, &context.store, &states, ) diff --git a/execution_engine/src/storage/trie_store/tests/mod.rs b/execution_engine/src/storage/trie_store/tests/mod.rs index a122f3ee7b..436c9bf6bf 100644 --- a/execution_engine/src/storage/trie_store/tests/mod.rs +++ b/execution_engine/src/storage/trie_store/tests/mod.rs @@ -47,10 +47,7 @@ fn create_data() -> Vec> { let ext_node: Trie = { let affix = vec![1u8, 0]; let pointer = Pointer::NodePointer(node_2_hash); - Trie::Extension { - affix: affix.into(), - pointer, - } + Trie::extension(affix, pointer) }; let ext_node_hash = Digest::hash(ext_node.to_bytes().unwrap()); diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index 50870233ee..8a26846e95 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -534,6 +534,26 @@ impl LmdbWasmTestBuilder { .expect("unable to run step request against scratch global state"); self } + /// Executes a request to call the system auction contract. + pub fn run_auction_with_scratch( + &mut self, + era_end_timestamp_millis: u64, + evicted_validators: Vec, + ) -> &mut Self { + let auction = self.get_auction_contract_hash(); + let run_request = ExecuteRequestBuilder::contract_call_by_hash( + *SYSTEM_ADDR, + auction, + METHOD_RUN_AUCTION, + runtime_args! { + ARG_ERA_END_TIMESTAMP_MILLIS => era_end_timestamp_millis, + ARG_EVICTED_VALIDATORS => evicted_validators, + }, + ) + .build(); + self.scratch_exec_and_commit(run_request).expect_success(); + self + } } impl WasmTestBuilder diff --git a/execution_engine_testing/tests/src/test/regression/ee_1119.rs b/execution_engine_testing/tests/src/test/regression/ee_1119.rs index 2c1dce3c68..561fa9116e 100644 --- a/execution_engine_testing/tests/src/test/regression/ee_1119.rs +++ b/execution_engine_testing/tests/src/test/regression/ee_1119.rs @@ -233,11 +233,11 @@ fn should_run_ee_1119_dont_slash_delegated_validators() { builder.exec(slash_request_2).expect_success().commit(); let unbond_purses: UnbondingPurses = builder.get_unbonds(); - assert_eq!(unbond_purses.len(), 1); + assert!(unbond_purses.is_empty()); assert!(!unbond_purses.contains_key(&*DEFAULT_ACCOUNT_ADDR)); - assert!(unbond_purses.get(&VALIDATOR_1_ADDR).unwrap().is_empty()); + assert!(!unbond_purses.contains_key(&VALIDATOR_1_ADDR)); let bids: Bids = builder.get_bids(); let validator_1_bid = bids.get(&VALIDATOR_1).unwrap(); diff --git a/execution_engine_testing/tests/src/test/regression/ee_1120.rs b/execution_engine_testing/tests/src/test/regression/ee_1120.rs index a69fe33b3e..3343e289ad 100644 --- a/execution_engine_testing/tests/src/test/regression/ee_1120.rs +++ b/execution_engine_testing/tests/src/test/regression/ee_1120.rs @@ -4,7 +4,7 @@ use num_traits::Zero; use once_cell::sync::Lazy; use casper_engine_test_support::{ - utils, ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, + utils, ExecuteRequestBuilder, LmdbWasmTestBuilder, DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_INITIAL_BALANCE, MINIMUM_ACCOUNT_CREATION_BALANCE, SYSTEM_ADDR, }; use casper_execution_engine::core::engine_state::{ @@ -84,7 +84,8 @@ fn should_run_ee_1120_slash_delegators() { }; let run_genesis_request = utils::create_run_genesis_request(accounts); - let mut builder = InMemoryWasmTestBuilder::default(); + let tempdir = tempfile::tempdir().unwrap(); + let mut builder = LmdbWasmTestBuilder::new_with_production_chainspec(tempdir.path()); builder.run_genesis(&run_genesis_request); let transfer_request_1 = ExecuteRequestBuilder::standard( @@ -97,7 +98,10 @@ fn should_run_ee_1120_slash_delegators() { ) .build(); - builder.exec(transfer_request_1).expect_success().commit(); + builder + .scratch_exec_and_commit(transfer_request_1) + .expect_success(); + builder.write_scratch_to_db(); let transfer_request_2 = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -109,7 +113,11 @@ fn should_run_ee_1120_slash_delegators() { ) .build(); - builder.exec(transfer_request_2).expect_success().commit(); + builder + .scratch_exec_and_commit(transfer_request_2) + .expect_success() + .commit(); + builder.write_scratch_to_db(); let auction = builder.get_auction_contract_hash(); @@ -149,19 +157,16 @@ fn should_run_ee_1120_slash_delegators() { .build(); builder - .exec(delegate_exec_request_1) - .expect_success() - .commit(); + .scratch_exec_and_commit(delegate_exec_request_1) + .expect_success(); builder - .exec(delegate_exec_request_2) - .expect_success() - .commit(); + .scratch_exec_and_commit(delegate_exec_request_2) + .expect_success(); builder - .exec(delegate_exec_request_3) - .expect_success() - .commit(); + .scratch_exec_and_commit(delegate_exec_request_3) + .expect_success(); // Ensure that initial bid entries exist for validator 1 and validator 2 let initial_bids: Bids = builder.get_bids(); @@ -209,10 +214,18 @@ fn should_run_ee_1120_slash_delegators() { ) .build(); - builder.exec(undelegate_request_1).commit().expect_success(); - builder.exec(undelegate_request_2).commit().expect_success(); - builder.exec(undelegate_request_3).commit().expect_success(); - + builder + .scratch_exec_and_commit(undelegate_request_1) + .expect_success(); + builder.write_scratch_to_db(); + builder + .scratch_exec_and_commit(undelegate_request_2) + .expect_success(); + builder.write_scratch_to_db(); + builder + .scratch_exec_and_commit(undelegate_request_3) + .expect_success(); + builder.write_scratch_to_db(); // Check unbonding purses before slashing let unbond_purses_before: UnbondingPurses = builder.get_unbonds(); @@ -289,7 +302,10 @@ fn should_run_ee_1120_slash_delegators() { ) .build(); - builder.exec(slash_request_1).expect_success().commit(); + builder + .scratch_exec_and_commit(slash_request_1) + .expect_success(); + builder.write_scratch_to_db(); // Compare bids after slashing validator 2 let bids_after: Bids = builder.get_bids(); @@ -346,7 +362,8 @@ fn should_run_ee_1120_slash_delegators() { ) .build(); - builder.exec(slash_request_2).expect_success().commit(); + builder.scratch_exec_and_commit(slash_request_2); + builder.write_scratch_to_db(); let bids_after: Bids = builder.get_bids(); assert_eq!(bids_after.len(), 2); @@ -355,12 +372,6 @@ fn should_run_ee_1120_slash_delegators() { assert!(validator_1_bid.staked_amount().is_zero()); let unbond_purses_after: UnbondingPurses = builder.get_unbonds(); - assert!(unbond_purses_after - .get(&VALIDATOR_1_ADDR) - .unwrap() - .is_empty()); - assert!(unbond_purses_after - .get(&VALIDATOR_2_ADDR) - .unwrap() - .is_empty()); + assert!(!unbond_purses_after.contains_key(&VALIDATOR_1_ADDR)); + assert!(!unbond_purses_after.contains_key(&VALIDATOR_2_ADDR)); } diff --git a/execution_engine_testing/tests/src/test/regression/gh_3710.rs b/execution_engine_testing/tests/src/test/regression/gh_3710.rs index 1e61016225..f2bf87448c 100644 --- a/execution_engine_testing/tests/src/test/regression/gh_3710.rs +++ b/execution_engine_testing/tests/src/test/regression/gh_3710.rs @@ -301,11 +301,11 @@ mod fixture { let rewards: Vec<&U512> = era_infos .iter() .flat_map(|era_info| era_info.seigniorage_allocations()) - .filter_map(|seigniorage| match seigniorage { + .map(|seigniorage| match seigniorage { SeigniorageAllocation::Validator { validator_public_key, amount, - } if validator_public_key == &*DEFAULT_ACCOUNT_PUBLIC_KEY => Some(amount), + } if validator_public_key == &*DEFAULT_ACCOUNT_PUBLIC_KEY => amount, SeigniorageAllocation::Validator { .. } => panic!("Unexpected validator"), SeigniorageAllocation::Delegator { .. } => panic!("No delegators"), }) diff --git a/execution_engine_testing/tests/src/test/regression/gov_116.rs b/execution_engine_testing/tests/src/test/regression/gov_116.rs index a86303f32b..0e5eb26a08 100644 --- a/execution_engine_testing/tests/src/test/regression/gov_116.rs +++ b/execution_engine_testing/tests/src/test/regression/gov_116.rs @@ -241,6 +241,7 @@ fn should_not_retain_genesis_validator_slot_protection_after_vesting_period_elap #[ignore] #[test] +#[allow(deprecated)] fn should_retain_genesis_validator_slot_protection() { const CASPER_VESTING_SCHEDULE_PERIOD_MILLIS: u64 = 91 * DAY_MILLIS; const CASPER_LOCKED_FUNDS_PERIOD_MILLIS: u64 = 90 * DAY_MILLIS; @@ -347,7 +348,11 @@ fn should_retain_genesis_validator_slot_protection() { pks }; assert_eq!( - next_validator_set_4, expected_validators, - "actual next validator set does not match expected validator set" + next_validator_set_4, + expected_validators, + "actual next validator set does not match expected validator set (diff {:?})", + expected_validators + .difference(&next_validator_set_4) + .collect::>(), ); } diff --git a/execution_engine_testing/tests/src/test/system_contracts/auction/bids.rs b/execution_engine_testing/tests/src/test/system_contracts/auction/bids.rs index 5032729648..df6487c2d0 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/auction/bids.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/auction/bids.rs @@ -5,28 +5,31 @@ use num_traits::{One, Zero}; use once_cell::sync::Lazy; use casper_engine_test_support::{ - utils, ExecuteRequestBuilder, InMemoryWasmTestBuilder, StepRequestBuilder, - UpgradeRequestBuilder, DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_INITIAL_BALANCE, + ExecuteRequestBuilder, InMemoryWasmTestBuilder, StepRequestBuilder, UpgradeRequestBuilder, + DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_INITIAL_BALANCE, DEFAULT_AUCTION_DELAY, DEFAULT_CHAINSPEC_REGISTRY, DEFAULT_EXEC_CONFIG, DEFAULT_GENESIS_CONFIG_HASH, - DEFAULT_GENESIS_TIMESTAMP_MILLIS, DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, DEFAULT_PROTOCOL_VERSION, - DEFAULT_UNBONDING_DELAY, MINIMUM_ACCOUNT_CREATION_BALANCE, PRODUCTION_RUN_GENESIS_REQUEST, - SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, + DEFAULT_GENESIS_TIMESTAMP_MILLIS, DEFAULT_MAX_ASSOCIATED_KEYS, + DEFAULT_MAX_RUNTIME_CALL_STACK_HEIGHT, DEFAULT_PROTOCOL_VERSION, + DEFAULT_ROUND_SEIGNIORAGE_RATE, DEFAULT_SYSTEM_CONFIG, DEFAULT_UNBONDING_DELAY, + DEFAULT_VALIDATOR_SLOTS, DEFAULT_WASM_CONFIG, MINIMUM_ACCOUNT_CREATION_BALANCE, + PRODUCTION_RUN_GENESIS_REQUEST, SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, }; use casper_execution_engine::{ core::{ engine_state::{ self, - engine_config::DEFAULT_MINIMUM_DELEGATION_AMOUNT, + engine_config::{DEFAULT_MINIMUM_DELEGATION_AMOUNT, DEFAULT_STRICT_ARGUMENT_CHECKING}, genesis::{ExecConfigBuilder, GenesisAccount, GenesisValidator}, run_genesis_request::RunGenesisRequest, - EngineConfigBuilder, Error, RewardItem, + EngineConfig, EngineConfigBuilder, Error, ExecConfig, RewardItem, + DEFAULT_MAX_QUERY_DEPTH, }, execution, }, + shared::transform::Transform, storage::global_state::in_memory::InMemoryGlobalState, }; use casper_types::{ - self, account::AccountHash, api_error::ApiError, runtime_args, @@ -38,7 +41,7 @@ use casper_types::{ ARG_NEW_VALIDATOR, ARG_PUBLIC_KEY, ARG_VALIDATOR, ERA_ID_KEY, INITIAL_ERA_ID, }, }, - EraId, Motes, ProtocolVersion, PublicKey, RuntimeArgs, SecretKey, U256, U512, + EraId, KeyTag, Motes, ProtocolVersion, PublicKey, RuntimeArgs, SecretKey, U256, U512, }; use crate::lmdb_fixture; @@ -171,6 +174,59 @@ const DAY_MILLIS: u64 = 24 * 60 * 60 * 1000; const CASPER_VESTING_SCHEDULE_PERIOD_MILLIS: u64 = 91 * DAY_MILLIS; const CASPER_LOCKED_FUNDS_PERIOD_MILLIS: u64 = 90 * DAY_MILLIS; +#[allow(deprecated)] +fn setup(accounts: Vec) -> InMemoryWasmTestBuilder { + let engine_config = EngineConfig::new( + DEFAULT_MAX_QUERY_DEPTH, + DEFAULT_MAX_ASSOCIATED_KEYS, + DEFAULT_MAX_RUNTIME_CALL_STACK_HEIGHT, + DEFAULT_MINIMUM_DELEGATION_AMOUNT, + DEFAULT_STRICT_ARGUMENT_CHECKING, + CASPER_VESTING_SCHEDULE_PERIOD_MILLIS, + None, + *DEFAULT_WASM_CONFIG, + *DEFAULT_SYSTEM_CONFIG, + ); + + let run_genesis_request = { + let exec_config = { + let wasm_config = *DEFAULT_WASM_CONFIG; + let system_config = *DEFAULT_SYSTEM_CONFIG; + let validator_slots = DEFAULT_VALIDATOR_SLOTS; + let auction_delay = DEFAULT_AUCTION_DELAY; + let locked_funds_period_millis = CASPER_LOCKED_FUNDS_PERIOD_MILLIS; + let round_seigniorage_rate = DEFAULT_ROUND_SEIGNIORAGE_RATE; + let unbonding_delay = DEFAULT_UNBONDING_DELAY; + let genesis_timestamp_millis = DEFAULT_GENESIS_TIMESTAMP_MILLIS; + #[allow(deprecated)] + ExecConfig::new( + accounts, + wasm_config, + system_config, + validator_slots, + auction_delay, + locked_funds_period_millis, + round_seigniorage_rate, + unbonding_delay, + genesis_timestamp_millis, + ) + }; + + RunGenesisRequest::new( + *DEFAULT_GENESIS_CONFIG_HASH, + *DEFAULT_PROTOCOL_VERSION, + exec_config, + DEFAULT_CHAINSPEC_REGISTRY.clone(), + ) + }; + + let mut builder = InMemoryWasmTestBuilder::new_with_config(engine_config); + + builder.run_genesis(&run_genesis_request); + + builder +} + #[ignore] #[test] fn should_add_new_bid() { @@ -185,11 +241,7 @@ fn should_add_new_bid() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let exec_request_1 = ExecuteRequestBuilder::standard( *BID_ACCOUNT_1_ADDR, @@ -229,11 +281,7 @@ fn should_increase_existing_bid() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let exec_request_1 = ExecuteRequestBuilder::standard( *BID_ACCOUNT_1_ADDR, @@ -288,11 +336,7 @@ fn should_decrease_existing_bid() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let bid_request = ExecuteRequestBuilder::standard( *BID_ACCOUNT_1_ADDR, @@ -356,11 +400,7 @@ fn should_run_delegate_and_undelegate() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let transfer_request_1 = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -536,11 +576,7 @@ fn should_calculate_era_validators() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let transfer_request_1 = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -598,7 +634,7 @@ fn should_calculate_era_validators() { assert_eq!(pre_era_id, EraId::from(0)); builder.run_auction( - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, + DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS, Vec::new(), ); @@ -1015,11 +1051,7 @@ fn should_fail_to_get_era_validators() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); assert_eq!( builder.get_validator_weights(EraId::MAX), @@ -1046,11 +1078,7 @@ fn should_use_era_validators_endpoint_for_first_era() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let validator_weights = builder .get_validator_weights(INITIAL_ERA_ID) @@ -1104,11 +1132,7 @@ fn should_calculate_era_validators_multiple_new_bids() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let genesis_validator_weights = builder .get_validator_weights(INITIAL_ERA_ID) @@ -1175,7 +1199,7 @@ fn should_calculate_era_validators_multiple_new_bids() { // run auction and compute validators for new era builder.run_auction( - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, + DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS, Vec::new(), ); // Verify first era validators @@ -1272,12 +1296,9 @@ fn undelegated_funds_should_be_released() { delegator_1_validator_1_delegate_request, ]; - let mut timestamp_millis = - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS; + let mut timestamp_millis = DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -1398,12 +1419,9 @@ fn fully_undelegated_funds_should_be_released() { delegator_1_validator_1_delegate_request, ]; - let mut timestamp_millis = - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS; - - let mut builder = InMemoryWasmTestBuilder::default(); + let mut timestamp_millis = DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS; - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -1559,12 +1577,9 @@ fn should_undelegate_delegators_when_validator_unbonds() { validator_1_partial_withdraw_bid, ]; - let mut timestamp_millis = - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS; + let mut timestamp_millis = DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -1796,12 +1811,9 @@ fn should_undelegate_delegators_when_validator_fully_unbonds() { delegator_2_delegate_request, ]; - let mut timestamp_millis = - DEFAULT_GENESIS_TIMESTAMP_MILLIS + DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS; + let mut timestamp_millis = DEFAULT_GENESIS_TIMESTAMP_MILLIS + CASPER_LOCKED_FUNDS_PERIOD_MILLIS; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -1922,8 +1934,7 @@ fn should_handle_evictions() { let era_validators: EraValidators = builder.get_era_validators(); let validators = era_validators .iter() - .rev() - .next() + .next_back() .map(|(_era_id, validators)| validators) .expect("should have validators"); validators.keys().cloned().collect::>() @@ -1982,11 +1993,7 @@ fn should_handle_evictions() { let mut timestamp = DEFAULT_GENESIS_TIMESTAMP_MILLIS; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); builder.exec(system_fund_request).commit().expect_success(); @@ -2125,11 +2132,7 @@ fn should_validate_orphaned_genesis_delegators() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } #[should_panic(expected = "DuplicatedDelegatorEntry")] @@ -2180,11 +2183,7 @@ fn should_validate_duplicated_genesis_delegators() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } #[should_panic(expected = "InvalidDelegationRate")] @@ -2205,11 +2204,7 @@ fn should_validate_delegation_rate_of_genesis_validator() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } #[should_panic(expected = "InvalidBondAmount")] @@ -2227,11 +2222,7 @@ fn should_validate_bond_amount_of_genesis_validator() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } #[ignore] @@ -2264,11 +2255,7 @@ fn should_setup_genesis_delegators() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let _account_1 = builder .get_account(*ACCOUNT_1_ADDR) @@ -2329,11 +2316,7 @@ fn should_not_partially_undelegate_uninitialized_vesting_schedule() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let fund_delegator_account = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -2403,11 +2386,7 @@ fn should_not_fully_undelegate_uninitialized_vesting_schedule() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let mut builder = setup(accounts); let fund_delegator_account = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -2976,9 +2955,7 @@ fn should_reset_delegators_stake_after_slashing() { delegator_2_validator_2_delegate_request, ]; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).expect_success().commit(); @@ -3127,11 +3104,7 @@ fn should_validate_genesis_delegators_bond_amount() { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } fn check_validator_slots_for_accounts(accounts: usize) { @@ -3161,11 +3134,7 @@ fn check_validator_slots_for_accounts(accounts: usize) { tmp }; - let run_genesis_request = utils::create_run_genesis_request(accounts); - - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&run_genesis_request); + let _builder = setup(accounts); } #[should_panic(expected = "InvalidValidatorSlots")] @@ -3267,9 +3236,7 @@ fn should_delegate_and_redelegate() { delegator_1_validator_1_delegate_request, ]; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -3492,9 +3459,7 @@ fn should_handle_redelegation_to_inactive_validator() { delegator_2_validator_1_delegate_request, ]; - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); for request in post_genesis_requests { builder.exec(request).commit().expect_success(); @@ -3592,8 +3557,12 @@ fn should_continue_auction_state_from_release_1_4_x() { let (mut builder, lmdb_fixture_state, _temp_dir) = lmdb_fixture::builder_from_global_state_fixture(lmdb_fixture::RELEASE_1_4_3); - let withdraw_purses: WithdrawPurses = builder.get_withdraw_purses(); + let withdraw_keys_before = builder + .get_keys(KeyTag::Withdraw) + .expect("should query withdraw keys"); + assert_eq!(withdraw_keys_before.len(), 1); + let withdraw_purses: WithdrawPurses = builder.get_withdraw_purses(); assert_eq!(withdraw_purses.len(), 1); let previous_protocol_version = lmdb_fixture_state.genesis_protocol_version(); @@ -3617,6 +3586,35 @@ fn should_continue_auction_state_from_release_1_4_x() { .upgrade_with_upgrade_request_and_config(None, &mut upgrade_request) .expect_upgrade_success(); + let upgrade_result = builder + .get_upgrade_result(0) + .expect("should have upgrade result") + .as_ref() + .expect("upgrade should work"); + let delete_keys_after_upgrade = upgrade_result + .execution_effect + .transforms + .iter() + .filter_map(|(key, transform)| { + if transform == &Transform::Prune { + Some(key) + } else { + None + } + }) + .collect::>(); + + assert!(!delete_keys_after_upgrade.is_empty()); + assert!(delete_keys_after_upgrade + .iter() + .all(|key| key.as_withdraw().is_some())); + + // Ensure withdraw keys are pruned + let withdraw_keys_after = builder + .get_keys(KeyTag::Withdraw) + .expect("should query withdraw keys"); + assert_eq!(withdraw_keys_after.len(), 0); + let unbonding_purses: UnbondingPurses = builder.get_unbonds(); assert_eq!(unbonding_purses.len(), 1); @@ -3786,6 +3784,22 @@ fn should_continue_auction_state_from_release_1_4_x() { redelegated_amount_1, U512::from(UNDELEGATE_AMOUNT_1 + DEFAULT_MINIMUM_DELEGATION_AMOUNT) ); + + // No new withdraw keys created after processing the auction + let withdraw_keys = builder + .get_keys(KeyTag::Withdraw) + .expect("should query withdraw keys"); + assert_eq!(withdraw_keys.len(), 0); + + // Unbond keys are deleted + let unbond_keys = builder + .get_keys(KeyTag::Unbond) + .expect("should query withdraw keys"); + assert_eq!( + unbond_keys.len(), + 0, + "auction state continued and empty unbond queue should be pruned" + ); } #[ignore] @@ -4012,9 +4026,7 @@ fn should_transfer_to_main_purse_when_validator_is_no_longer_active() { #[ignore] #[test] fn should_enforce_minimum_delegation_amount() { - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); let transfer_to_validator_1 = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, @@ -4093,9 +4105,7 @@ fn should_enforce_minimum_delegation_amount() { #[ignore] #[test] fn should_allow_delegations_with_minimal_floor_amount() { - let mut builder = InMemoryWasmTestBuilder::default(); - - builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + let mut builder = setup(DEFAULT_ACCOUNTS.clone()); let transfer_to_validator_1 = ExecuteRequestBuilder::standard( *DEFAULT_ACCOUNT_ADDR, diff --git a/execution_engine_testing/tests/src/test/system_contracts/auction_bidding.rs b/execution_engine_testing/tests/src/test/system_contracts/auction_bidding.rs index e669a7d875..edbd548816 100644 --- a/execution_engine_testing/tests/src/test/system_contracts/auction_bidding.rs +++ b/execution_engine_testing/tests/src/test/system_contracts/auction_bidding.rs @@ -1,11 +1,12 @@ use num_traits::Zero; use casper_engine_test_support::{ - utils, ExecuteRequestBuilder, InMemoryWasmTestBuilder, UpgradeRequestBuilder, DEFAULT_ACCOUNTS, - DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_PUBLIC_KEY, DEFAULT_GENESIS_TIMESTAMP_MILLIS, - DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, DEFAULT_PAYMENT, DEFAULT_PROPOSER_PUBLIC_KEY, - DEFAULT_PROTOCOL_VERSION, DEFAULT_UNBONDING_DELAY, MINIMUM_ACCOUNT_CREATION_BALANCE, - PRODUCTION_RUN_GENESIS_REQUEST, SYSTEM_ADDR, TIMESTAMP_MILLIS_INCREMENT, + utils, ExecuteRequestBuilder, InMemoryWasmTestBuilder, LmdbWasmTestBuilder, StepRequestBuilder, + UpgradeRequestBuilder, DEFAULT_ACCOUNTS, DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_PUBLIC_KEY, + DEFAULT_GENESIS_TIMESTAMP_MILLIS, DEFAULT_LOCKED_FUNDS_PERIOD_MILLIS, DEFAULT_PAYMENT, + DEFAULT_PROPOSER_PUBLIC_KEY, DEFAULT_PROTOCOL_VERSION, DEFAULT_UNBONDING_DELAY, + MINIMUM_ACCOUNT_CREATION_BALANCE, PRODUCTION_RUN_GENESIS_REQUEST, SYSTEM_ADDR, + TIMESTAMP_MILLIS_INCREMENT, }; use casper_execution_engine::core::{ engine_state::{ @@ -181,10 +182,7 @@ fn should_run_successful_bond_and_unbond_and_slashing() { builder.exec(exec_request_5).expect_success().commit(); let unbond_purses: UnbondingPurses = builder.get_unbonds(); - assert!(unbond_purses - .get(&*DEFAULT_ACCOUNT_ADDR) - .unwrap() - .is_empty()); + assert!(!unbond_purses.contains_key(&*DEFAULT_ACCOUNT_ADDR)); let bids: Bids = builder.get_bids(); let default_account_bid = bids.get(&DEFAULT_ACCOUNT_PUBLIC_KEY).unwrap(); @@ -540,10 +538,189 @@ fn should_run_successful_bond_and_unbond_with_release() { ); let unbond_purses: UnbondingPurses = builder.get_unbonds(); - assert!(unbond_purses + assert!(!unbond_purses.contains_key(&*DEFAULT_ACCOUNT_ADDR)); + + let bids: Bids = builder.get_bids(); + assert!(!bids.is_empty()); + + let bid = bids.get(&default_public_key_arg).expect("should have bid"); + let bid_purse = *bid.bonding_purse(); + assert_eq!( + builder.get_purse_balance(bid_purse), + U512::from(GENESIS_ACCOUNT_STAKE) - unbond_amount, // remaining funds + ); +} + +#[ignore] +#[test] +fn should_run_successful_bond_and_unbond_with_release_on_lmdb() { + let default_public_key_arg = DEFAULT_ACCOUNT_PUBLIC_KEY.clone(); + + let tempdir = tempfile::tempdir().expect("should create tempdir"); + + let mut builder = LmdbWasmTestBuilder::new_with_production_chainspec(tempdir.path()); + builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST); + + let default_account = builder + .get_account(*DEFAULT_ACCOUNT_ADDR) + .expect("should have default account"); + + let unbonding_purse = default_account.main_purse(); + + let exec_request = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_TRANSFER_TO_ACCOUNT, + runtime_args! { + "target" => *SYSTEM_ADDR, + "amount" => U512::from(TRANSFER_AMOUNT* 2) + }, + ) + .build(); + + builder + .scratch_exec_and_commit(exec_request) + .expect_success(); + builder.write_scratch_to_db(); + + let _system_account = builder + .get_account(*SYSTEM_ADDR) + .expect("should get account 1"); + + let _default_account = builder + .get_account(*DEFAULT_ACCOUNT_ADDR) + .expect("should get account 1"); + + let exec_request_1 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_ADD_BID, + runtime_args! { + ARG_AMOUNT => U512::from(GENESIS_ACCOUNT_STAKE), + ARG_PUBLIC_KEY => default_public_key_arg.clone(), + ARG_DELEGATION_RATE => DELEGATION_RATE, + }, + ) + .build(); + + builder + .scratch_exec_and_commit(exec_request_1) + .expect_success(); + builder.write_scratch_to_db(); + + let bids: Bids = builder.get_bids(); + let bid = bids.get(&default_public_key_arg).expect("should have bid"); + let bid_purse = *bid.bonding_purse(); + assert_eq!( + builder.get_purse_balance(bid_purse), + GENESIS_ACCOUNT_STAKE.into() + ); + + let unbond_purses: UnbondingPurses = builder.get_unbonds(); + assert_eq!(unbond_purses.len(), 0); + + // + // Advance era by calling run_auction + // + let step_request = StepRequestBuilder::new() + .with_parent_state_hash(builder.get_post_state_hash()) + .with_protocol_version(ProtocolVersion::V1_0_0) + .with_next_era_id(builder.get_era().successor()) + .with_run_auction(true) + .build(); + + builder.step_with_scratch(step_request); + + builder.write_scratch_to_db(); + + // + // Partial unbond + // + + let unbond_amount = U512::from(GENESIS_ACCOUNT_STAKE) - 1; + + let exec_request_2 = ExecuteRequestBuilder::standard( + *DEFAULT_ACCOUNT_ADDR, + CONTRACT_WITHDRAW_BID, + runtime_args! { + ARG_AMOUNT => unbond_amount, + ARG_PUBLIC_KEY => default_public_key_arg.clone(), + }, + ) + .build(); + + builder + .scratch_exec_and_commit(exec_request_2) + .expect_success(); + + builder.write_scratch_to_db(); + + let unbond_purses: UnbondingPurses = builder.get_unbonds(); + assert_eq!(unbond_purses.len(), 1); + + let unbond_list = unbond_purses .get(&*DEFAULT_ACCOUNT_ADDR) - .unwrap() - .is_empty()); + .expect("should have unbond"); + assert_eq!(unbond_list.len(), 1); + assert_eq!( + unbond_list[0].validator_public_key(), + &default_public_key_arg, + ); + assert!(unbond_list[0].is_validator()); + + assert_eq!(unbond_list[0].era_of_creation(), INITIAL_ERA_ID + 1); + + let unbond_era_1 = unbond_list[0].era_of_creation(); + + let account_balance_before_auction = builder.get_purse_balance(unbonding_purse); + + let unbond_purses: UnbondingPurses = builder.get_unbonds(); + assert_eq!(unbond_purses.len(), 1); + + let unbond_list = unbond_purses + .get(&DEFAULT_ACCOUNT_ADDR) + .expect("should have unbond"); + assert_eq!(unbond_list.len(), 1); + assert_eq!( + unbond_list[0].validator_public_key(), + &default_public_key_arg, + ); + assert!(unbond_list[0].is_validator()); + + assert_eq!( + builder.get_purse_balance(unbonding_purse), + account_balance_before_auction, // Not paid yet + ); + + let unbond_era_2 = unbond_list[0].era_of_creation(); + + assert_eq!(unbond_era_2, unbond_era_1); // era of withdrawal didn't change since first run + + let era_id_before = builder.get_era(); + // + // Advance state to hit the unbonding period + // + for _ in 0..=builder.get_unbonding_delay() { + let step_request = StepRequestBuilder::new() + .with_parent_state_hash(builder.get_post_state_hash()) + .with_protocol_version(ProtocolVersion::V1_0_0) + .with_next_era_id(builder.get_era().successor()) + .with_run_auction(true) + .build(); + + builder.step_with_scratch(step_request); + + builder.write_scratch_to_db(); + } + + let era_id_after = builder.get_era(); + + assert_ne!(era_id_before, era_id_after); + + let unbond_purses: UnbondingPurses = builder.get_unbonds(); + assert!( + !unbond_purses.contains_key(&*DEFAULT_ACCOUNT_ADDR), + "{:?}", + unbond_purses + ); let bids: Bids = builder.get_bids(); assert!(!bids.is_empty()); @@ -732,10 +909,7 @@ fn should_run_successful_unbond_funds_after_changing_unbonding_delay() { ); let unbond_purses: UnbondingPurses = builder.get_unbonds(); - assert!(unbond_purses - .get(&*DEFAULT_ACCOUNT_ADDR) - .unwrap() - .is_empty()); + assert!(!unbond_purses.contains_key(&*DEFAULT_ACCOUNT_ADDR)); let bids: Bids = builder.get_bids(); assert!(!bids.is_empty()); diff --git a/hashing/src/chunk_with_proof.rs b/hashing/src/chunk_with_proof.rs index d93951b914..445954baa6 100644 --- a/hashing/src/chunk_with_proof.rs +++ b/hashing/src/chunk_with_proof.rs @@ -140,7 +140,7 @@ mod tests { fn prepare_bytes(length: usize) -> Vec { let mut rng = rand::thread_rng(); - (0..length).into_iter().map(|_| rng.gen()).collect() + (0..length).map(|_| rng.gen()).collect() } fn random_chunk_with_proof() -> ChunkWithProof { @@ -206,7 +206,6 @@ mod tests { .unwrap(); assert!((0..number_of_chunks) - .into_iter() .map(|chunk_index| { ChunkWithProof::new(data.as_slice(), chunk_index).unwrap() }) .all(|chunk_with_proof| chunk_with_proof.verify().is_ok())); } diff --git a/json_rpc/src/lib.rs b/json_rpc/src/lib.rs index f82a79cce6..0e9fc049a9 100644 --- a/json_rpc/src/lib.rs +++ b/json_rpc/src/lib.rs @@ -40,7 +40,7 @@ //! let path = "rpc"; //! let max_body_bytes = 1024; //! let allow_unknown_fields = false; -//! let route = casper_json_rpc::route(path, max_body_bytes, handlers, allow_unknown_fields); +//! let route = casper_json_rpc::route(path, max_body_bytes, handlers, allow_unknown_fields, None); //! //! // Convert it into a `Service` and run it. //! let make_svc = hyper::service::make_service_fn(move |_| { @@ -96,6 +96,7 @@ pub use response::Response; const JSON_RPC_VERSION: &str = "2.0"; /// Specifies the CORS origin +#[derive(Debug)] pub enum CorsOrigin { /// Any (*) origin is allowed. Any, @@ -103,32 +104,31 @@ pub enum CorsOrigin { Specified(String), } -/// Constructs a set of warp filters suitable for use in a JSON-RPC server. -/// -/// `path` specifies the exact HTTP path for JSON-RPC requests, e.g. "rpc" will match requests on -/// exactly "/rpc", and not "/rpc/other". -/// -/// `max_body_bytes` sets an upper limit for the number of bytes in the HTTP request body. For -/// further details, see -/// [`warp::filters::body::content_length_limit`](https://docs.rs/warp/latest/warp/filters/body/fn.content_length_limit.html). -/// -/// `handlers` is the map of functions to which incoming requests will be dispatched. These are -/// keyed by the JSON-RPC request's "method". -/// -/// If `allow_unknown_fields` is `false`, requests with unknown fields will cause the server to -/// respond with an error. -/// -/// For further details, see the docs for the [`filters`] functions. -pub fn route>( - path: P, - max_body_bytes: u32, - handlers: RequestHandlers, - allow_unknown_fields: bool, -) -> BoxedFilter<(impl Reply,)> { - filters::base_filter(path, max_body_bytes) - .and(filters::main_filter(handlers, allow_unknown_fields)) - .recover(filters::handle_rejection) - .boxed() +impl CorsOrigin { + /// Converts the [`CorsOrigin`] into a CORS [`Builder`](warp::cors::Builder). + #[inline] + pub fn to_cors_builder(&self) -> warp::cors::Builder { + match self { + CorsOrigin::Any => warp::cors().allow_any_origin(), + CorsOrigin::Specified(origin) => warp::cors().allow_origin(origin.as_str()), + } + } + + /// Parses a [`CorsOrigin`] from a given configuration string. + /// + /// The input string will be parsed as follows: + /// + /// * `""` (empty string): No CORS Origin (i.e. returns [`None`]). + /// * `"*"`: [`CorsOrigin::Any`]. + /// * otherwise, returns `CorsOrigin::Specified(raw)`. + #[inline] + pub fn parse_str>(raw: T) -> Option { + match raw.as_ref() { + "" => None, + "*" => Some(CorsOrigin::Any), + _ => Some(CorsOrigin::Specified(raw.to_string())), + } + } } /// Constructs a set of warp filters suitable for use in a JSON-RPC server. @@ -146,32 +146,52 @@ pub fn route>( /// If `allow_unknown_fields` is `false`, requests with unknown fields will cause the server to /// respond with an error. /// -/// Note that this is a convenience function combining the lower-level functions in [`filters`] -/// along with [a warp CORS filter](https://docs.rs/warp/latest/warp/filters/cors/index.html) which +/// If `cors_header` is `Some`, it is used to add a [a warp CORS +/// filter](https://docs.rs/warp/latest/warp/filters/cors/index.html) which +/// /// * allows any origin or specified origin /// * allows "content-type" as a header /// * allows the method "POST" /// /// For further details, see the docs for the [`filters`] functions. -pub fn route_with_cors>( +pub fn route>( path: P, max_body_bytes: u32, handlers: RequestHandlers, allow_unknown_fields: bool, - cors_header: &CorsOrigin, -) -> BoxedFilter<(impl Reply,)> { - filters::base_filter(path, max_body_bytes) + cors_header: Option<&CorsOrigin>, +) -> BoxedFilter<(Box,)> { + let base = filters::base_filter(path, max_body_bytes) .and(filters::main_filter(handlers, allow_unknown_fields)) - .recover(filters::handle_rejection) - .with(match cors_header { - CorsOrigin::Any => warp::cors() - .allow_any_origin() - .allow_header(CONTENT_TYPE) - .allow_method(Method::POST), - CorsOrigin::Specified(origin) => warp::cors() - .allow_origin(origin.as_str()) - .allow_header(CONTENT_TYPE) - .allow_method(Method::POST), - }) - .boxed() + .recover(filters::handle_rejection); + + if let Some(cors_origin) = cors_header { + let cors = cors_origin + .to_cors_builder() + .allow_header(CONTENT_TYPE) + .allow_method(Method::POST) + .build(); + base.with(cors).map(box_reply).boxed() + } else { + base.map(box_reply).boxed() + } +} + +/// Boxes a reply of a warp filter. +/// +/// Can be combined with [`Filter::boxed`] through [`Filter::map`] to erase the type on filters: +/// +/// ```rust +/// use warp::{Filter, filters::BoxedFilter, http::Response, reply::Reply}; +/// # use casper_json_rpc::box_reply; +/// +/// let filter: BoxedFilter<(Box,)> = warp::any() +/// .map(|| Response::builder().body("hello world")) +/// .map(box_reply).boxed(); +/// # drop(filter); +/// ``` +#[inline(always)] +pub fn box_reply(reply: T) -> Box { + let boxed: Box = Box::new(reply); + boxed } diff --git a/juliet/.gitignore b/juliet/.gitignore new file mode 100644 index 0000000000..0df6c7d69b --- /dev/null +++ b/juliet/.gitignore @@ -0,0 +1,2 @@ +coverage/ +lcov.info diff --git a/juliet/Cargo.toml b/juliet/Cargo.toml new file mode 100644 index 0000000000..fcd602adb0 --- /dev/null +++ b/juliet/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "juliet" +version = "0.1.0" +edition = "2021" +authors = [ "Marc Brinkmann " ] +exclude = [ "proptest-regressions" ] + +[dependencies] +array-init = "2.1.0" +bimap = "0.6.3" +bytemuck = { version = "1.13.1", features = [ "derive" ] } +bytes = "1.4.0" +futures = "0.3.28" +hex_fmt = "0.3.0" +once_cell = "1.18.0" +strum = { version = "0.25.0", features = ["derive"] } +thiserror = "1.0.40" +tokio = { version = "1.29.1", features = [ "macros", "io-util", "sync", "time" ] } +tracing = { version = "0.1.37", optional = true } + +[dev-dependencies] +# TODO: Upgrade `derive_more` to non-beta version, once released. +derive_more = { version = "1.0.0-beta.2", features = [ "debug" ] } +tokio = { version = "1.29.1", features = [ + "macros", + "net", + "rt-multi-thread", + "time", +] } +proptest = "1.1.0" +proptest-attr-macro = "1.0.0" +proptest-derive = "0.3.0" +rand = "0.8.5" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = [ "env-filter" ] } +assert_matches = "1.5.0" +static_assertions = "1.1.0" + +[[example]] +name = "fizzbuzz" +required-features = [ "tracing" ] diff --git a/juliet/README.md b/juliet/README.md new file mode 100644 index 0000000000..ee2b2551c3 --- /dev/null +++ b/juliet/README.md @@ -0,0 +1,36 @@ +# Juliet protocol implementation + +This crate implements the Juliet multiplexing protocol as laid out in the [Juliet RFC](https://github.com/marc-casperlabs/juliet-rfc/blob/master/juliet.md). It aims to be a secure, simple, easy to verify/review implementation that is still reasonably performant. + +## Benefits + + The Juliet protocol comes with a core set of features, such as + +* carefully designed with security and DoS resilience as its foremoast goal, +* customizable frame sizes, +* up to 256 multiplexed, interleaved channels, +* backpressure support fully baked in, and +* low overhead (4 bytes per frame + 1-5 bytes depending on payload length). + +This crate's implementation includes benefits such as + +* a side-effect-free implementation of the Juliet protocol, +* an `async` IO layer integrated with the [`bytes`](https://docs.rs/bytes) crate to use it, and +* a type-safe RPC layer built on top. + +## Examples + +For a quick usage example, see `examples/fizzbuzz.rs`. + +## `tracing` support + +The crate has an optional dependency on the [`tracing`](https://docs.rs/tracing) crate, which, if enabled, allows detailed insights through logs. If the feature is not enabled, no log statements are compiled in. + +Log levels in general are used as follows: + +* `ERROR` and `WARN`: Actual issues that are not protocol level errors -- peer errors are expected and do not warrant a `WARN` level. +* `INFO`: Insights into received high level events (e.g. connection, disconnection, etc), except information concerning individual requests/messages. +* `DEBUG`: Detailed insights down to the level of individual requests, but not frames. A multi-megabyte single message transmission will NOT clog the logs. +* `TRACE`: Like `DEBUG`, but also including frame and wire-level information, as well as local functions being called. + +At `INFO`, it is thus conceivable for a peer to maliciously spam local logs, although with some effort if connection attempts are rate limited. At `DEBUG` or lower, this becomes trivial. diff --git a/juliet/examples/fizzbuzz.rs b/juliet/examples/fizzbuzz.rs new file mode 100644 index 0000000000..a4b8bc6e89 --- /dev/null +++ b/juliet/examples/fizzbuzz.rs @@ -0,0 +1,178 @@ +//! A juliet-based fizzbuzz server and client. +//! +//! To run this example, in one terminal, launch the server: +//! +//! ``` +//! cargo run --example fizzbuzz --features tracing -- server +//! ``` +//! +//! Then, in a second terminal launch the client: +//! +//! ``` +//! cargo run --example fizzbuzz --features tracing +//! ``` +//! +//! You should see [Fizz buzz](https://en.wikipedia.org/wiki/Fizz_buzz) solutions being calculated +//! on the server side and sent back. + +use std::{fmt::Write, net::SocketAddr, time::Duration}; + +use bytes::BytesMut; +use juliet::{ + io::IoCoreBuilder, + protocol::ProtocolBuilder, + rpc::{IncomingRequest, RpcBuilder}, + ChannelConfiguration, ChannelId, +}; +use rand::Rng; +use tokio::net::{TcpListener, TcpStream}; +use tracing::{debug, error, info, warn}; + +const SERVER_ADDR: &str = "127.0.0.1:12345"; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("juliet=trace".parse().unwrap()) + .add_directive("fizzbuzz=trace".parse().unwrap()), + ) + .init(); + + // Create a new protocol instance with two channels, allowing three requests in flight each. + let protocol_builder = ProtocolBuilder::<2>::with_default_channel_config( + ChannelConfiguration::default() + .with_request_limit(3) + .with_max_request_payload_size(4) + .with_max_response_payload_size(512), + ); + + // Create the IO layer, buffering at most two messages on the wait queue per channel. + let io_builder = IoCoreBuilder::new(protocol_builder) + .buffer_size(ChannelId::new(0), 2) + .buffer_size(ChannelId::new(1), 2); + + // Create the final RPC builder - we will use this on every connection. + let rpc_builder = Box::leak(Box::new(RpcBuilder::new(io_builder))); + + let mut args = std::env::args(); + args.next().expect("did not expect missing argv0"); + let is_server = args.next().map(|a| a == "server").unwrap_or_default(); + + if is_server { + let listener = TcpListener::bind(SERVER_ADDR) + .await + .expect("failed to listen"); + info!("listening on {}", SERVER_ADDR); + loop { + match listener.accept().await { + Ok((client, addr)) => { + info!("new connection from {}", addr); + tokio::spawn(handle_client(addr, client, rpc_builder)); + } + Err(io_err) => { + warn!("acceptance failure: {:?}", io_err); + } + } + } + } else { + let remote_server = TcpStream::connect(SERVER_ADDR) + .await + .expect("failed to connect to server"); + info!("connected to server {}", SERVER_ADDR); + + let (reader, writer) = remote_server.into_split(); + let (client, mut server) = rpc_builder.build(reader, writer); + + // We are not using the server functionality, but still need to run it for IO reasons. + tokio::spawn(async move { + if let Err(err) = server.next_request().await { + error!(%err, "server read error"); + } + }); + + for num in 0..u32::MAX { + let request_guard = client + .create_request(ChannelId::new(0)) + .with_payload(num.to_be_bytes().to_vec().into()) + .queue_for_sending() + .await; + + debug!("sent request {}", num); + match request_guard.wait_for_response().await { + Ok(response) => { + let decoded = + String::from_utf8(response.expect("should have payload").to_vec()) + .expect("did not expect invalid UTF8"); + info!("{} -> {}", num, decoded); + } + Err(err) => { + error!("server error: {}", err); + break; + } + } + } + } +} + +/// Handles a incoming client connection. +async fn handle_client( + addr: SocketAddr, + mut client: TcpStream, + rpc_builder: &RpcBuilder, +) { + let (reader, writer) = client.split(); + let (client, mut server) = rpc_builder.build(reader, writer); + + loop { + match server.next_request().await { + Ok(opt_incoming_request) => { + if let Some(incoming_request) = opt_incoming_request { + tokio::spawn(handle_request(incoming_request)); + } else { + // Client exited. + info!("client {} disconnected", addr); + break; + } + } + Err(err) => { + warn!("client {} error: {}", addr, err); + break; + } + } + } + + // We are a server, we won't make any requests of our own, but we need to keep the client + // around, since dropping the client will trigger a server shutdown. + drop(client); +} + +/// Handles a single request made by a client (on the server). +async fn handle_request(incoming_request: IncomingRequest) { + let processing_time = rand::thread_rng().gen_range(5..20) * Duration::from_millis(100); + tokio::time::sleep(processing_time).await; + + let payload = incoming_request + .payload() + .as_ref() + .expect("should have payload"); + let num = + u32::from_be_bytes(<[u8; 4]>::try_from(payload.as_ref()).expect("could not decode u32")); + + // Construct the response. + let mut response_payload = BytesMut::new(); + if num % 3 == 0 { + response_payload.write_str("Fizz ").unwrap(); + } + if num % 5 == 0 { + response_payload.write_str("Buzz ").unwrap(); + } + if response_payload.is_empty() { + write!(response_payload, "{}", num).unwrap(); + } + + // Send it back. + incoming_request.respond(Some(response_payload.freeze())); +} diff --git a/juliet/proptest-regressions/header.txt b/juliet/proptest-regressions/header.txt new file mode 100644 index 0000000000..7cc8d26d55 --- /dev/null +++ b/juliet/proptest-regressions/header.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc f122aa653a1e96699ace549caf46dc063d11f10b612839616aedf6bf6053f3fe # shrinks to raw = [8, 0, 0, 0] diff --git a/juliet/proptest-regressions/io.txt b/juliet/proptest-regressions/io.txt new file mode 100644 index 0000000000..a5c396e11f --- /dev/null +++ b/juliet/proptest-regressions/io.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a5ecee32b10b8720f0f7b09871835a7a9fd674f8b5b9c1c9ac68e3fb977c0345 # shrinks to input = [] +cc b44cf1d77da7a1db17b3174b7bd9b55dbe835cc5e85acd5fd3ec137714ef50d3 # shrinks to input = [30, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +cc 3cd7b8fb915fa8d98871218c077ab02a99b66eaf5d3306738331a55daddf9891 # shrinks to input = [117, 157, 0, 5, 0, 0, 0, 0, 0, 186, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 93, 0, 0, 41, 0, 0, 223, 0, 0, 130, 169, 29, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] diff --git a/juliet/proptest-regressions/lib.txt b/juliet/proptest-regressions/lib.txt new file mode 100644 index 0000000000..4bd2b15808 --- /dev/null +++ b/juliet/proptest-regressions/lib.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 298f935141dc04a8afb87a0f78f9491eb0fb39330b74592eb42fb3e78a859d61 # shrinks to raw = 0 diff --git a/juliet/proptest-regressions/multiframe.txt b/juliet/proptest-regressions/multiframe.txt new file mode 100644 index 0000000000..eb23f72509 --- /dev/null +++ b/juliet/proptest-regressions/multiframe.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 9b7fb8eced05b4d28bbcbcfa173487e6a8b2891b1b3a0f6ebd0210d34fe7e0be # shrinks to payload = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 116, 42, 17, 106, 128, 80, 246, 96, 235, 166, 22, 253, 165, 154, 37, 70, 38, 92, 11, 109, 221, 241, 175, 189, 113, 116, 175, 151, 6, 85, 70, 38, 56, 3, 253, 23, 124, 247, 63, 191, 244, 161, 167, 201, 29, 1, 136, 238, 198, 134, 89, 143, 216, 224, 86, 251, 87, 241, 243, 81, 191, 160, 56, 236, 121, 57, 49, 163, 176, 54, 44, 228, 84, 228, 231, 101, 223, 238, 38, 242, 183, 213, 23, 237, 146, 17, 186, 166, 170, 51, 6, 20, 144, 245, 228, 109, 102, 82, 191, 80, 235, 75, 54, 255, 182, 190, 12, 232, 101, 148, 205, 153, 104, 145, 235, 83, 232, 38, 34, 195, 3, 197, 101, 161, 2, 21, 186, 38, 182, 119, 27, 85, 170, 188, 114, 230, 55, 158, 163, 211, 201, 151, 211, 46, 238, 192, 59, 124, 228, 115, 232, 26, 88, 26, 149, 51, 88, 108, 159, 30, 245, 74, 235, 53, 135, 239, 61, 255, 170, 10, 149, 44, 207, 150, 187, 16, 37, 61, 51, 136, 162, 45, 243, 124, 230, 104, 237, 210, 97, 172, 180, 251, 11, 96, 248, 221, 236, 98, 66, 94, 54, 111, 143, 228, 31, 122, 191, 121, 19, 111, 169, 67, 132, 14, 205, 111, 152, 93, 21, 210, 182, 18, 161, 87, 244, 129, 62, 238, 28, 144, 166, 20, 56, 93, 173, 101, 219, 26, 203, 193, 102, 39, 236, 215, 31, 16, 206, 165, 179, 230, 37, 207, 222, 31, 7, 182, 255, 236, 248, 169, 132, 78, 187, 95, 250, 241, 199, 238, 246, 130, 90, 198, 144, 81, 170, 157, 63, 34, 1, 183, 218, 179, 142, 146, 83, 175, 241, 120, 245, 163, 6, 222, 198, 196, 105, 217, 188, 114, 138, 196, 187, 215, 232, 138, 147, 198, 34, 131, 151, 50, 178, 184, 108, 56, 147, 49, 40, 251, 188, 20, 166, 60, 77, 235, 153, 13, 25, 228, 219, 15, 139, 229, 60, 50, 198, 100, 221, 237, 17, 220, 16, 236, 238, 27, 20, 217, 26, 92, 86, 152], garbage = [19, 209, 226, 16, 122, 243, 10, 110, 138, 205] diff --git a/juliet/proptest-regressions/protocol/multiframe.txt b/juliet/proptest-regressions/protocol/multiframe.txt new file mode 100644 index 0000000000..5a725e106f --- /dev/null +++ b/juliet/proptest-regressions/protocol/multiframe.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 6e7fd627a8f19cd62a9ddcaa90d051076fcfbbce9735fe0b25f9e68f2272dc7e # shrinks to actions = [SendSingleFrame { header: [Request chan: 0 id: 0], payload: [] }] diff --git a/juliet/proptest-regressions/varint.txt b/juliet/proptest-regressions/varint.txt new file mode 100644 index 0000000000..5d4542e68f --- /dev/null +++ b/juliet/proptest-regressions/varint.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 87df179402b16f961c3c1062d8f62213848f06da82e2bf34d288903128849f1b # shrinks to value = 0 diff --git a/juliet/src/header.rs b/juliet/src/header.rs new file mode 100644 index 0000000000..9d65feb6ca --- /dev/null +++ b/juliet/src/header.rs @@ -0,0 +1,405 @@ +//! `juliet` header parsing and serialization. +//! +//! This module is typically only used by the protocol implementation (see +//! [`protocol`](crate::protocol)), but may be of interested to those writing low level tooling. +use std::fmt::{Debug, Display}; + +use bytemuck::{Pod, Zeroable}; +use hex_fmt::HexFmt; +use strum::{EnumCount, EnumIter, FromRepr}; +use thiserror::Error; + +use crate::{ChannelId, Id}; + +/// Header structure. +/// +/// Implements [`AsRef`], which will return a byte slice with the correct encoding of the header +/// that can be sent directly to a peer. +#[derive(Copy, Clone, Eq, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct Header([u8; Header::SIZE]); + +impl Display for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", HexFmt(&self.0)) + } +} + +impl Debug for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_error() { + write!( + f, + "[err:{:?} chan: {} id: {}]", + self.error_kind(), + self.channel(), + self.id() + ) + } else { + write!( + f, + "[{:?} chan: {} id: {}]", + self.kind(), + self.channel(), + self.id() + ) + } + } +} + +/// Error kind, from the kind byte. +#[derive(Copy, Clone, Debug, EnumCount, EnumIter, Error, FromRepr, Eq, PartialEq)] +#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +#[repr(u8)] +pub enum ErrorKind { + /// Application defined error. + #[error("application defined error")] + Other = 0, + /// The maximum frame size has been exceeded. This error cannot occur in this implementation, + /// which operates solely on streams. + #[error("maximum frame size exceeded")] + MaxFrameSizeExceeded = 1, + /// An invalid header was received. + #[error("invalid header")] + InvalidHeader = 2, + /// A segment was sent with a frame where none was allowed, or a segment was too small or + /// missing. + #[error("segment violation")] + SegmentViolation = 3, + /// A `varint32` could not be decoded. + #[error("bad varint")] + BadVarInt = 4, + /// Invalid channel: A channel number greater than the highest channel number was received. + #[error("invalid channel")] + InvalidChannel = 5, + /// A new request or response was sent without completing the previous one. + #[error("multi-frame in progress")] + InProgress = 6, + /// The indicated size of the response would exceed the configured limit. + #[error("response too large")] + ResponseTooLarge = 7, + /// The indicated size of the request would exceed the configured limit. + #[error("request too large")] + RequestTooLarge = 8, + /// Peer attempted to create two in-flight requests with the same ID on the same channel. + #[error("duplicate request")] + DuplicateRequest = 9, + /// Sent a response for request not in-flight. + #[error("response for fictitious request")] + FictitiousRequest = 10, + /// The dynamic request limit has been exceeded. + #[error("request limit exceeded")] + RequestLimitExceeded = 11, + /// Response cancellation for a request not in-flight. + #[error("cancellation for fictitious request")] + FictitiousCancel = 12, + /// Peer sent a request cancellation exceeding the cancellation allowance. + #[error("cancellation limit exceeded")] + CancellationLimitExceeded = 13, +} + +/// Frame kind, from the kind byte. +#[derive(Copy, Clone, Debug, EnumCount, EnumIter, Eq, FromRepr, PartialEq)] +#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +#[repr(u8)] +pub enum Kind { + /// A request with no payload. + Request = 0, + /// A response with no payload. + Response = 1, + /// A request that includes a payload. + RequestPl = 2, + /// A response that includes a payload. + ResponsePl = 3, + /// Cancellation of a request. + CancelReq = 4, + /// Cancellation of a response. + CancelResp = 5, +} + +impl Header { + /// The size (in bytes) of a header. + pub(crate) const SIZE: usize = 4; + /// Bitmask returning the error bit of the kind byte. + const KIND_ERR_BIT: u8 = 0b1000_0000; + /// Bitmask returning the error kind inside the kind byte. + const KIND_ERR_MASK: u8 = 0b0000_1111; + /// Bitmask returning the frame kind inside the kind byte. + const KIND_MASK: u8 = 0b0000_0111; + + /// Creates a new non-error header. + #[inline(always)] + pub const fn new(kind: Kind, channel: ChannelId, id: Id) -> Self { + let id = id.get().to_le_bytes(); + Header([kind as u8, channel.get(), id[0], id[1]]) + } + + /// Creates a new error header. + #[inline(always)] + pub const fn new_error(kind: ErrorKind, channel: ChannelId, id: Id) -> Self { + let id = id.get().to_le_bytes(); + Header([ + kind as u8 | Header::KIND_ERR_BIT, + channel.get(), + id[0], + id[1], + ]) + } + + /// Parse a header from raw bytes. + /// + /// Returns `None` if the given `raw` bytes are not a valid header. + #[inline(always)] + pub const fn parse(mut raw: [u8; Header::SIZE]) -> Option { + // Zero-out reserved bits. + raw[0] &= Self::KIND_ERR_MASK | Self::KIND_MASK | Self::KIND_ERR_BIT; + + let header = Header(raw); + + // Check that the kind byte is within valid range. + if header.is_error() { + if (header.kind_byte() & Self::KIND_ERR_MASK) >= ErrorKind::COUNT as u8 { + return None; + } + } else { + if (header.kind_byte() & Self::KIND_MASK) >= Kind::COUNT as u8 { + return None; + } + + // Ensure the 4th bit is not set, since the error kind bits are superset of kind bits. + if header.kind_byte() & Self::KIND_MASK != header.kind_byte() { + return None; + } + } + + Some(header) + } + + /// Returns the raw kind byte. + #[inline(always)] + const fn kind_byte(self) -> u8 { + self.0[0] + } + + /// Returns the channel. + #[inline(always)] + pub const fn channel(self) -> ChannelId { + ChannelId::new(self.0[1]) + } + + /// Returns the id. + #[inline(always)] + pub const fn id(self) -> Id { + let [_, _, id @ ..] = self.0; + Id::new(u16::from_le_bytes(id)) + } + + /// Returns whether the error bit is set. + #[inline(always)] + pub const fn is_error(self) -> bool { + self.kind_byte() & Self::KIND_ERR_BIT == Self::KIND_ERR_BIT + } + + /// Returns whether or not the given header is a request header. + #[inline] + pub const fn is_request(self) -> bool { + if !self.is_error() { + matches!(self.kind(), Kind::Request | Kind::RequestPl) + } else { + false + } + } + + /// Returns the error kind. + /// + /// # Panics + /// + /// Will panic if `Self::is_error()` is not `true`. + #[inline(always)] + pub const fn error_kind(self) -> ErrorKind { + debug_assert!(self.is_error()); + match ErrorKind::from_repr(self.kind_byte() & Self::KIND_ERR_MASK) { + Some(value) => value, + None => { + // While this is representable, it would violate the invariant of this type that is + // enforced by [`Header::parse`]. + unreachable!() + } + } + } + + /// Returns the frame kind. + /// + /// # Panics + /// + /// Will panic if `Self::is_error()` is `true`. + #[inline(always)] + pub const fn kind(self) -> Kind { + debug_assert!(!self.is_error()); + + match Kind::from_repr(self.kind_byte() & Self::KIND_MASK) { + Some(kind) => kind, + None => { + // Invariant enfored by [`Header::parse`]. + unreachable!() + } + } + } + + /// Creates a new header with the same id and channel but an error kind. + #[inline] + pub(crate) const fn with_err(self, kind: ErrorKind) -> Self { + Header::new_error(kind, self.channel(), self.id()) + } +} + +impl From
for [u8; Header::SIZE] { + fn from(value: Header) -> Self { + value.0 + } +} + +impl AsRef<[u8; Header::SIZE]> for Header { + fn as_ref(&self) -> &[u8; Header::SIZE] { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use bytemuck::Zeroable; + use proptest::{ + arbitrary::any, + prelude::Arbitrary, + prop_oneof, + strategy::{BoxedStrategy, Strategy}, + }; + use proptest_attr_macro::proptest; + + use crate::{ChannelId, Id}; + + use super::{ErrorKind, Header, Kind}; + + /// Proptest strategy for `Header`s. + fn arb_header() -> impl Strategy { + prop_oneof![ + any::<(Kind, ChannelId, Id)>().prop_map(|(kind, chan, id)| Header::new(kind, chan, id)), + any::<(ErrorKind, ChannelId, Id)>() + .prop_map(|(err_kind, chan, id)| Header::new_error(err_kind, chan, id)), + ] + } + + impl Arbitrary for Header { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_header().boxed() + } + + type Strategy = BoxedStrategy
; + } + + #[test] + fn known_headers() { + let input = [0x86, 0x48, 0xAA, 0xBB]; + let expected = + Header::new_error(ErrorKind::InProgress, ChannelId::new(0x48), Id::new(0xBBAA)); + + assert_eq!( + Header::parse(input).expect("could not parse header"), + expected + ); + assert_eq!(<[u8; Header::SIZE]>::from(expected), input); + } + + #[proptest] + fn roundtrip_valid_headers(header: Header) { + let raw: [u8; Header::SIZE] = header.into(); + + assert_eq!( + Header::parse(raw).expect("failed to roundtrip header"), + header + ); + + // Verify the `kind` and `err_kind` methods don't panic. + if header.is_error() { + header.error_kind(); + } else { + header.kind(); + } + + // Verify `is_request` does not panic. + header.is_request(); + + // Ensure `is_request` returns the correct value. + if !header.is_error() { + if matches!(header.kind(), Kind::Request) || matches!(header.kind(), Kind::RequestPl) { + assert!(header.is_request()); + } else { + assert!(!header.is_request()); + } + } + } + + #[proptest] + fn fuzz_header(raw: [u8; Header::SIZE]) { + if let Some(header) = Header::parse(raw) { + let rebuilt = if header.is_error() { + Header::new_error(header.error_kind(), header.channel(), header.id()) + } else { + Header::new(header.kind(), header.channel(), header.id()) + }; + + // Ensure reserved bits are zeroed upon reading. + let reencoded: [u8; Header::SIZE] = rebuilt.into(); + assert_eq!(rebuilt, header); + assert_eq!(reencoded, <[u8; Header::SIZE]>::from(header)); + + // Ensure debug doesn't panic. + assert_eq!(format!("{:?}", header), format!("{:?}", header)); + + // Check bytewise it is the same. + assert_eq!(&reencoded[..], header.as_ref()); + } + + // Otherwise all good, simply failed to parse. + } + + #[test] + fn fuzz_header_regressions() { + // Bit 4, which is not `RESERVED`, but only valid for errors. + let raw = [8, 0, 0, 0]; + assert!(Header::parse(raw).is_none()); + + // Two reserved bits set. + let raw = [48, 0, 0, 0]; + assert!(Header::parse(raw).is_some()); + } + + #[test] + fn header_parsing_fails_if_kind_out_of_range() { + let invalid_err_header = [0b1000_1111, 00, 00, 00]; + assert_eq!(Header::parse(invalid_err_header), None); + + let invalid_ok_header = [0b0000_0111, 00, 00, 00]; + assert_eq!(Header::parse(invalid_ok_header), None); + } + + #[test] + fn ensure_zeroed_header_works() { + assert_eq!( + Header::zeroed(), + Header::new(Kind::Request, ChannelId(0), Id(0)) + ) + } + + #[proptest] + fn err_header_construction(header: Header, error_kind: ErrorKind) { + let combined = header.with_err(error_kind); + + assert_eq!(header.channel(), combined.channel()); + assert_eq!(header.id(), combined.id()); + assert!(combined.is_error()); + assert_eq!(combined.error_kind(), error_kind); + } +} diff --git a/juliet/src/io.rs b/juliet/src/io.rs new file mode 100644 index 0000000000..7dbdda3bdb --- /dev/null +++ b/juliet/src/io.rs @@ -0,0 +1,1460 @@ +//! `juliet` IO layer +//! +//! The IO layer combines a lower-level transport like a TCP Stream with the +//! [`JulietProtocol`](crate::protocol::JulietProtocol) protocol implementation and some memory +//! buffers to provide a working high-level transport for juliet messages. It allows users of this +//! layer to send messages over multiple channels, without having to worry about frame multiplexing +//! or request limits. +//! +//! ## Usage +//! +//! Most, if not all functionality is provided by the [`IoCore`] type, which is constructed +//! using an [`IoCoreBuilder`] (see [`IoCoreBuilder::new`]). Similarly to [`JulietProtocol`] the +//! `N` denotes the number of predefined channels. +//! +//! ## Incoming data +//! +//! Once instantiated, the [`IoCore`] **must** have its [`IoCore::next_event`] function called +//! continuously, see its documentation for details. Doing so will also yield all incoming events +//! and data. +//! +//! ## Outgoing data +//! +//! The [`RequestHandle`] provided by [`IoCoreBuilder::build`] is used to send requests to the peer. +//! It should also be kept around even if no requests are sent, as dropping it is used to signal the +//! [`IoCore`] to close the connection. + +use std::{ + collections::{BTreeSet, VecDeque}, + fmt::{self, Display, Formatter}, + io, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, +}; + +use bimap::BiMap; +use bytes::{Buf, Bytes, BytesMut}; +use thiserror::Error; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + sync::{ + mpsc::{self, error::TryRecvError, UnboundedReceiver, UnboundedSender}, + OwnedSemaphorePermit, Semaphore, TryAcquireError, + }, +}; + +use crate::{ + header::Header, + protocol::{ + payload_is_multi_frame, CompletedRead, FrameIter, JulietProtocol, LocalProtocolViolation, + OutgoingFrame, OutgoingMessage, ProtocolBuilder, + }, + util::PayloadFormat, + ChannelId, Id, Outcome, +}; + +/// An item in the outgoing queue. +/// +/// Requests are not transformed into messages in the queue to conserve limited request ID space. +#[derive(Debug)] +enum QueuedItem { + /// An outgoing request. + Request { + /// Channel to send it out on. + channel: ChannelId, + /// [`IoId`] mapped to the request. + io_id: IoId, + /// The requests payload. + payload: Option, + /// The semaphore permit for the request. + permit: OwnedSemaphorePermit, + }, + /// Cancellation of one of our own requests. + RequestCancellation { + /// [`IoId`] mapped to the request that should be cancelled. + io_id: IoId, + }, + /// Outgoing response to a received request. + Response { + /// Channel the original request was received on. + channel: ChannelId, + /// Id of the original request. + id: Id, + /// Payload to send along with the response. + payload: Option, + }, + /// A cancellation response. + ResponseCancellation { + /// Channel the original request was received on. + channel: ChannelId, + /// Id of the original request. + id: Id, + }, + /// An error. + Error { + /// Channel to send error on. + channel: ChannelId, + /// Id to send with error. + id: Id, + /// Error payload. + payload: Bytes, + }, +} + +impl Display for QueuedItem { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + QueuedItem::Request { + channel, + io_id, + payload, + permit: _, + } => { + write!(f, "Request {{ channel: {}, io_id: {}", channel, io_id)?; + if let Some(payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + f.write_str(" }") + } + QueuedItem::RequestCancellation { io_id } => { + write!(f, "RequestCancellation {{ io_id: {} }}", io_id) + } + QueuedItem::Response { + channel, + id, + payload, + } => { + write!(f, "Response {{ channel: {}, id: {}", channel, id)?; + if let Some(payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + f.write_str(" }") + } + QueuedItem::ResponseCancellation { channel, id } => { + write!( + f, + "ResponseCancellation {{ channel: {}, id: {} }}", + channel, id + ) + } + QueuedItem::Error { + channel, + id, + payload, + } => { + write!( + f, + "Error {{ channel: {}, id: {}, payload: {} }}", + channel, + id, + PayloadFormat(payload) + ) + } + } + } +} + +impl QueuedItem { + /// Retrieves the payload from the queued item. + fn into_payload(self) -> Option { + match self { + QueuedItem::Request { payload, .. } => payload, + QueuedItem::Response { payload, .. } => payload, + QueuedItem::RequestCancellation { .. } => None, + QueuedItem::ResponseCancellation { .. } => None, + QueuedItem::Error { payload, .. } => Some(payload), + } + } +} + +/// [`IoCore`] event processing error. +/// +/// A [`CoreError`] always indicates that the underlying [`IoCore`] has encountered a fatal error +/// and no further communication should take part. +#[derive(Debug, Error)] +pub enum CoreError { + /// Failed to read from underlying reader. + #[error("read failed")] + ReadFailed(#[source] io::Error), + /// Failed to write using underlying writer. + #[error("write failed")] + WriteFailed(#[source] io::Error), + /// Remote peer will/has disconnect(ed), but sent us an error message before. + #[error("remote peer sent error [channel {}/id {}]: {} (payload: {} bytes)", + header.channel(), + header.id(), + header.error_kind(), + data.as_ref().map(|b| b.len()).unwrap_or(0)) + ] + RemoteReportedError { + /// Header of the reported error. + header: Header, + /// The error payload, if the error kind was + /// [`ErrorKind::Other`](crate::header::ErrorKind::Other). + data: Option, + }, + /// The remote peer violated the protocol and has been sent an error. + #[error("error sent to peer: {0}")] + RemoteProtocolViolation(Header), + #[error("local protocol violation")] + /// Local protocol violation - caller violated the crate's API. + LocalProtocolViolation(#[from] LocalProtocolViolation), + /// Internal error. + /// + /// An error occurred that should be impossible, this is indicative of a bug in this library. + #[error("internal consistency error: {0}")] + InternalError(&'static str), +} + +/// An IO layer request ID. +/// +/// Request layer IO IDs are unique across the program per request that originated from the local +/// endpoint. They are used to allow for buffering large numbers of items without exhausting the +/// pool of protocol level request IDs, which are limited to `u16`s. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct IoId(u64); + +impl Display for IoId { + #[inline(always)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +/// IO layer for the juliet protocol. +/// +/// The central structure for the IO layer built on top of the juliet protocol, one instance per +/// connection. It manages incoming (`R`) and outgoing (`W`) transports, as well as a queue for +/// items to be sent. +/// +/// Once instantiated, a continuous polling of [`IoCore::next_event`] is expected. +#[derive(Debug)] +pub struct IoCore { + /// The actual protocol state. + juliet: JulietProtocol, + + /// Underlying transport, reader. + reader: R, + /// Underlying transport, writer. + writer: W, + /// Read buffer for incoming data. + buffer: BytesMut, + /// How many bytes are required until the next parse. + /// + /// Used to ensure we don't attempt to parse too often. + next_parse_at: usize, + /// Whether or not we are shutting down due to an error. + shutting_down_due_to_err: bool, + + /// The frame in the process of being sent, which may be partially transferred already. + current_frame: Option, + /// The headers of active current multi-frame transfers. + active_multi_frame: [Option
; N], + /// Frames waiting to be sent. + ready_queue: VecDeque, + /// Messages that are not yet ready to be sent. + wait_queue: [VecDeque; N], + /// Receiver for new messages to be queued. + receiver: UnboundedReceiver, + /// Mapping for outgoing requests, mapping internal IDs to public ones. + request_map: BiMap, + /// A set of channels whose wait queues should be checked again for data to send. + dirty_channels: BTreeSet, +} + +/// Shared data between a handles and the core itself. +#[derive(Debug)] +#[repr(transparent)] +struct IoShared { + /// Tracks how many requests are in the wait queue. + /// + /// Tickets are freed once the item is in the wait queue, thus the semaphore permit count + /// controls how many requests can be buffered in addition to those already permitted due to + /// the protocol. + /// + /// The maximum number of available tickets must be >= 1 for the IO layer to function. + buffered_requests: [Arc; N], +} + +/// Events produced by the IO layer. +/// +/// Every event must be handled, see event details on how to do so. +#[derive(Debug)] +#[must_use] +pub enum IoEvent { + /// A new request has been received. + /// + /// Eventually a received request must be handled by one of the following: + /// + /// * A response sent (through [`Handle::enqueue_response`]). + /// * A response cancellation sent (through [`Handle::enqueue_response_cancellation`]). + /// * The connection being closed, either regularly or due to an error, on either side. + /// * The reception of an [`IoEvent::RequestCancelled`] with the same ID and channel. + NewRequest { + /// Channel the new request arrived on. + channel: ChannelId, + /// Request ID (set by peer). + id: Id, + /// The payload provided with the request. + payload: Option, + }, + /// A received request has been cancelled. + RequestCancelled { + /// Channel the original request arrived on. + channel: ChannelId, + /// Request ID (set by peer). + id: Id, + }, + /// A response has been received. + /// + /// For every [`IoId`] there will eventually be exactly either one + /// [`IoEvent::ReceivedResponse`] or [`IoEvent::ReceivedCancellationResponse`], unless the + /// connection is shutdown beforehand. + ReceivedResponse { + /// The local request ID for which the response was sent. + io_id: IoId, + /// The payload of the response. + payload: Option, + }, + /// A response cancellation has been received. + /// + /// Indicates the peer is not going to answer the request. + /// + /// For every [`IoId`] there will eventually be exactly either one + /// [`IoEvent::ReceivedResponse`] or [`IoEvent::ReceivedCancellationResponse`], unless the + /// connection is shutdown beforehand. + ReceivedCancellationResponse { + /// The local request ID which will not be answered. + io_id: IoId, + }, +} + +impl Display for IoEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + IoEvent::NewRequest { + channel, + id, + payload, + } => { + write!(f, "NewRequest {{ channel: {}, id: {}", channel, id)?; + if let Some(ref payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + f.write_str(" }") + } + + IoEvent::RequestCancelled { channel, id } => { + write!(f, "RequestCancalled {{ channel: {}, id: {} }}", channel, id) + } + IoEvent::ReceivedResponse { io_id, payload } => { + write!(f, "ReceivedResponse {{ io_id: {}", io_id)?; + if let Some(ref payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + f.write_str(" }") + } + IoEvent::ReceivedCancellationResponse { io_id } => { + write!(f, "ReceivedCancellationResponse {{ io_id: {} }}", io_id) + } + } + } +} + +/// A builder for the [`IoCore`]. +#[derive(Debug)] +pub struct IoCoreBuilder { + /// The builder for the underlying protocol. + protocol: ProtocolBuilder, + /// Number of additional requests to buffer, per channel. + buffer_size: [usize; N], +} + +impl IoCoreBuilder { + /// Creates a new builder for an [`IoCore`]. + #[inline] + pub const fn new(protocol: ProtocolBuilder) -> Self { + Self::with_default_buffer_size(protocol, 1) + } + + /// Creates a new builder for an [`IoCore`], initializing all buffer sizes to the given default. + #[inline] + pub const fn with_default_buffer_size( + protocol: ProtocolBuilder, + default_buffer_size: usize, + ) -> Self { + Self { + protocol, + buffer_size: [default_buffer_size; N], + } + } + + /// Sets the wait queue buffer size for a given channel. + /// + /// # Panics + /// + /// Will panic if given an invalid channel or a size less than one. + pub const fn buffer_size(mut self, channel: ChannelId, size: usize) -> Self { + assert!(size > 0, "cannot have a memory buffer size of zero"); + + self.buffer_size[channel.get() as usize] = size; + + self + } + + /// Builds a new [`IoCore`] with a [`RequestHandle`]. + /// + /// See [`IoCore::next_event`] for details on how to handle the core. The [`RequestHandle`] can + /// be used to send requests. + pub fn build(&self, reader: R, writer: W) -> (IoCore, RequestHandle) { + let (sender, receiver) = mpsc::unbounded_channel(); + + let core = IoCore { + juliet: self.protocol.build(), + reader, + writer, + buffer: BytesMut::new(), + next_parse_at: 0, + shutting_down_due_to_err: false, + current_frame: None, + active_multi_frame: [Default::default(); N], + ready_queue: Default::default(), + wait_queue: array_init::array_init(|_| Default::default()), + receiver, + request_map: Default::default(), + dirty_channels: Default::default(), + }; + + let shared = Arc::new(IoShared { + buffered_requests: array_init::map_array_init(&self.buffer_size, |&sz| { + Arc::new(Semaphore::new(sz)) + }), + }); + let handle = RequestHandle { + shared, + sender, + next_io_id: Default::default(), + }; + + (core, handle) + } +} + +impl IoCore +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + /// Retrieves the next event. + /// + /// This is the central loop of the IO layer. It polls all underlying transports and + /// reads/writes if data is available, until enough processing has been done to produce an + /// [`IoEvent`]. Thus any application using the IO layer should loop over calling this function. + /// + /// Polling of this function must continue only until `Err(_)` or `Ok(None)` is returned, + /// indicating that the connection should be closed or has been closed. + pub async fn next_event(&mut self) -> Result, CoreError> { + loop { + self.process_dirty_channels()?; + + if self.next_parse_at <= self.buffer.remaining() { + // Simplify reasoning about this code. + self.next_parse_at = 0; + + match self.juliet.process_incoming(&mut self.buffer) { + Outcome::Incomplete(n) => { + // Simply reset how many bytes we need until the next parse. + self.next_parse_at = self.buffer.remaining() + n.get() as usize; + } + Outcome::Fatal(err_msg) => { + // The remote messed up, begin shutting down due to an error. + self.inject_error(err_msg); + } + Outcome::Success(successful_read) => { + // Check if we have produced an event. + return self.handle_completed_read(successful_read).map(Some); + } + } + } + + // TODO: Can we find something more elegant than this abomination? + #[inline(always)] + async fn write_all_buf_if_some( + writer: &mut W, + buf: Option<&mut impl Buf>, + ) -> Result<(), io::Error> { + if let Some(buf) = buf { + writer.write_all_buf(buf).await + } else { + Ok(()) + } + } + + if self.current_frame.is_none() && !self.ready_queue.is_empty() { + self.ready_next_frame()?; + } + + tokio::select! { + biased; // We actually like the bias, avoid the randomness overhead. + + write_result = write_all_buf_if_some(&mut self.writer, self.current_frame.as_mut()) + , if self.current_frame.is_some() => { + + write_result.map_err(CoreError::WriteFailed)?; + + // If we just finished sending an error, it's time to exit. + let frame_sent = self.current_frame.take().unwrap(); + + #[cfg(feature = "tracing")] + { + tracing::trace!(frame=%frame_sent, "sent"); + } + + if frame_sent.header().is_error() { + // We finished sending an error frame, time to exit. + return Err(CoreError::RemoteProtocolViolation(frame_sent.header())); + } + } + + // Reading incoming data. + read_result = read_until_bytesmut(&mut self.reader, &mut self.buffer, self.next_parse_at), if !self.shutting_down_due_to_err => { + // Our read function will not return before `read_until_bytesmut` has completed. + let read_complete = read_result.map_err(CoreError::ReadFailed)?; + + if !read_complete { + // Remote peer hung up. + return Ok(None); + } + + // Fall through to start of loop, which parses data read. + } + + // Processing locally queued things. + incoming = self.receiver.recv(), if !self.shutting_down_due_to_err => { + match incoming { + Some(item) => { + self.handle_incoming_item(item)?; + } + None => { + // If the receiver was closed it means that we locally shut down the + // connection. + #[cfg(feature = "tracing")] + tracing::info!("local shutdown"); + return Ok(None); + } + } + + loop { + match self.receiver.try_recv() { + Ok(item) => { + self.handle_incoming_item(item)?; + } + Err(TryRecvError::Disconnected) => { + // While processing incoming items, the last handle was closed. + #[cfg(feature = "tracing")] + tracing::debug!("last local io handle closed, shutting down"); + return Ok(None); + } + Err(TryRecvError::Empty) => { + // Everything processed. + break + } + } + } + } + } + } + } + + /// Ensures the next message sent is an error message. + /// + /// Clears all buffers related to sending and closes the local incoming channel. + fn inject_error(&mut self, err_msg: OutgoingMessage) { + // Stop accepting any new local data. + self.receiver.close(); + + // Set the error state. + self.shutting_down_due_to_err = true; + + // We do not continue parsing, ever again. + self.next_parse_at = usize::MAX; + + // Clear queues and data structures that are no longer needed. + self.buffer.clear(); + self.ready_queue.clear(); + self.request_map.clear(); + for queue in &mut self.wait_queue { + queue.clear(); + } + + // Ensure the error message is the next frame sent. + self.ready_queue.push_front(err_msg.frames()); + } + + /// Processes a completed read into a potential event. + fn handle_completed_read( + &mut self, + completed_read: CompletedRead, + ) -> Result { + #[cfg(feature = "tracing")] + tracing::debug!(%completed_read, "completed read"); + match completed_read { + CompletedRead::ErrorReceived { header, data } => { + // We've received an error from the peer, they will be closing the connection. + Err(CoreError::RemoteReportedError { header, data }) + } + CompletedRead::NewRequest { + channel, + id, + payload, + } => { + // Requests have their id passed through, since they are not given an `IoId`. + Ok(IoEvent::NewRequest { + channel, + id, + payload, + }) + } + CompletedRead::RequestCancellation { channel, id } => { + Ok(IoEvent::RequestCancelled { channel, id }) + } + + // It is not our job to ensure we do not receive duplicate responses or cancellations; + // this is taken care of by `JulietProtocol`. + CompletedRead::ReceivedResponse { + channel, + id, + payload, + } => self + .request_map + .remove_by_right(&(channel, id)) + .ok_or(CoreError::InternalError( + "juliet protocol should have dropped response after cancellation", + )) + .map(move |(io_id, _)| IoEvent::ReceivedResponse { io_id, payload }), + CompletedRead::ResponseCancellation { channel, id } => { + // Responses are mapped to the respective `IoId`. + self.request_map + .remove_by_right(&(channel, id)) + .ok_or(CoreError::InternalError( + "juliet protocol should not have allowed fictitious response through", + )) + .map(|(io_id, _)| IoEvent::ReceivedCancellationResponse { io_id }) + } + } + } + + /// Handles a new item to send out that arrived through the incoming channel. + fn handle_incoming_item(&mut self, item: QueuedItem) -> Result<(), LocalProtocolViolation> { + // Check if the item is sendable immediately. + if let Some(channel) = item_should_wait(&item, &self.juliet, &self.active_multi_frame) { + #[cfg(feature = "tracing")] + tracing::debug!(%item, "postponing send"); + self.wait_queue[channel.get() as usize].push_back(item); + return Ok(()); + } + + #[cfg(feature = "tracing")] + tracing::debug!(%item, "ready to send"); + self.send_to_ready_queue(item, false) + } + + /// Sends an item directly to the ready queue, causing it to be sent out eventually. + /// + /// `item` is passed as a mutable reference for compatibility with functions like `retain_mut`, + /// but will be left with all payloads removed, thus should likely not be reused. + fn send_to_ready_queue( + &mut self, + item: QueuedItem, + check_for_cancellation: bool, + ) -> Result<(), LocalProtocolViolation> { + match item { + QueuedItem::Request { + io_id, + channel, + payload, + permit, + } => { + // "Chase" our own requests here -- if the request was still in the wait queue, + // we can cancel it by checking if the `IoId` has been removed in the meantime. + // + // Note that this only cancels multi-frame requests. + if check_for_cancellation && !self.request_map.contains_left(&io_id) { + // We just ignore the request, as it has been cancelled in the meantime. + } else { + let msg = self.juliet.create_request(channel, payload)?; + let id = msg.header().id(); + self.request_map.insert(io_id, (channel, id)); + self.ready_queue.push_back(msg.frames()); + } + + drop(permit); + } + QueuedItem::RequestCancellation { io_id } => { + if let Some((channel, id)) = self.request_map.get_by_left(&io_id) { + if let Some(msg) = self.juliet.cancel_request(*channel, *id)? { + self.ready_queue.push_back(msg.frames()); + } + } else { + // Already cancelled or answered by peer - no need to do anything. + } + } + + // `juliet` already tracks whether we still need to send the cancellation. + // Unlike requests, we do not attempt to fish responses out of the queue, + // cancelling a response after it has been created should be rare. + QueuedItem::Response { + id, + channel, + payload, + } => { + if let Some(msg) = self.juliet.create_response(channel, id, payload)? { + self.ready_queue.push_back(msg.frames()) + } + } + QueuedItem::ResponseCancellation { id, channel } => { + if let Some(msg) = self.juliet.cancel_response(channel, id)? { + self.ready_queue.push_back(msg.frames()); + } + } + + // Errors go straight to the front of the line. + QueuedItem::Error { + id, + channel, + payload, + } => { + let err_msg = self.juliet.custom_error(channel, id, payload)?; + self.inject_error(err_msg); + } + } + + Ok(()) + } + + /// Clears a potentially finished frame and returns the next frame to send. + /// + /// Returns `None` if no frames are ready to be sent. Note that there may be frames waiting + /// that cannot be sent due them being multi-frame messages when there already is a multi-frame + /// message in progress, or request limits are being hit. + fn ready_next_frame(&mut self) -> Result<(), LocalProtocolViolation> { + debug_assert!(self.current_frame.is_none()); // Must be guaranteed by caller. + + // Try to fetch a frame from the ready queue. If there is nothing, we are stuck until the + // next time the wait queue is processed or new data arrives. + let (frame, additional_frames) = match self.ready_queue.pop_front() { + Some(item) => item, + None => return Ok(()), + } + .next_owned(self.juliet.max_frame_size()); + + // If there are more frames after this one, schedule the remainder. + if let Some(next_frame_iter) = additional_frames { + self.ready_queue.push_back(next_frame_iter); + } else { + // No additional frames. Check if sending the next frame finishes a multi-frame message. + let about_to_finish = frame.header(); + if let Some(ref active_multi) = + self.active_multi_frame[about_to_finish.channel().get() as usize] + { + if about_to_finish == *active_multi { + // Once the scheduled frame is processed, we will finished the multi-frame + // transfer, so we can allow for the next multi-frame transfer to be scheduled. + self.active_multi_frame[about_to_finish.channel().get() as usize] = None; + + // There is a chance another multi-frame messages became ready now. + self.dirty_channels.insert(about_to_finish.channel()); + } + } + } + + self.current_frame = Some(frame); + Ok(()) + } + + /// Process the wait queue of all channels marked dirty, promoting messages that are ready to be + /// sent to the ready queue. + fn process_dirty_channels(&mut self) -> Result<(), CoreError> { + while let Some(channel) = self.dirty_channels.pop_first() { + let wait_queue_len = self.wait_queue[channel.get() as usize].len(); + + // The code below is not as bad it looks complexity wise, anticipating two common cases: + // + // 1. A multi-frame read has finished, with capacity for requests to spare. Only + // multi-frame requests will be waiting in the wait queue, so we will likely pop the + // first item, only scanning the rest once. + // 2. One or more requests finished, so we also have a high chance of picking the first + // few requests out of the queue. + + for _ in 0..(wait_queue_len) { + let item = self.wait_queue[channel.get() as usize].pop_front().ok_or( + CoreError::InternalError("did not expect wait_queue to disappear"), + )?; + + if item_should_wait(&item, &self.juliet, &self.active_multi_frame).is_some() { + // Put it right back into the queue. + self.wait_queue[channel.get() as usize].push_back(item); + } else { + self.send_to_ready_queue(item, true)?; + } + } + } + + Ok(()) + } +} + +/// Determines whether an item is ready to be moved from the wait queue from the ready queue. +fn item_should_wait( + item: &QueuedItem, + juliet: &JulietProtocol, + active_multi_frame: &[Option
; N], +) -> Option { + let (payload, channel) = match item { + QueuedItem::Request { + channel, payload, .. + } => { + // Check if we cannot schedule due to the message exceeding the request limit. + if !juliet + .allowed_to_send_request(*channel) + .expect("should not be called with invalid channel") + { + return Some(*channel); + } + + (payload, channel) + } + QueuedItem::Response { + channel, payload, .. + } => (payload, channel), + + // Other messages are always ready. + QueuedItem::RequestCancellation { .. } + | QueuedItem::ResponseCancellation { .. } + | QueuedItem::Error { .. } => return None, + }; + + let active_multi_frame = active_multi_frame[channel.get() as usize]; + + // Check if we cannot schedule due to the message being multi-frame and there being a + // multi-frame send in progress: + if active_multi_frame.is_some() { + if let Some(payload) = payload { + if payload_is_multi_frame(juliet.max_frame_size(), payload.len()) { + return Some(*channel); + } + } + } + + // Otherwise, this should be a legitimate add to the run queue. + None +} + +/// A handle to the input queue to the [`IoCore`] that allows sending requests and responses. +/// +/// The handle is roughly three pointers in size and can be cloned at will. Dropping the last handle +/// will cause the [`IoCore`] to shutdown and close the connection. +/// +/// ## Sending requests +/// +/// To send a request, a holder of this handle must first reserve a slot in the memory buffer of the +/// [`IoCore`] using either [`RequestHandle::try_reserve_request`] or +/// [`RequestHandle::reserve_request`], then [`RequestHandle::downgrade`] this request handle to a +/// regular [`Handle`] and [`Handle::enqueue_request`] with the given [`RequestTicket`]. +#[derive(Clone, Debug)] +pub struct RequestHandle { + /// Shared portion of the [`IoCore`], required for backpressuring onto clients. + shared: Arc>, + /// Sender for queue items. + sender: UnboundedSender, + /// The next generation [`IoId`]. + /// + /// IoIDs are just generated sequentially until they run out (which at 1 billion at second + /// takes roughly 10^22 years). + next_io_id: Arc, +} + +/// Simple [`IoCore`] handle. +/// +/// Functions similarly to [`RequestHandle`], but has a no capability of creating new requests, as +/// it lacks access to the internal [`IoId`] generator. +/// +/// Like [`RequestHandle`], the existance of this handle will keep [`IoCore`] alive; dropping the +/// last one will shut it down. +/// +/// ## Usage +/// +/// To send any sort of message, response, cancellation or error, use one of the `enqueue_*` +/// methods. The [`io`] layer does some, but not complete bookkeeping, if a complete solution is +/// required, use the [`rpc`](crate::rpc) layer instead. +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Handle { + /// Sender for queue items. + sender: UnboundedSender, +} + +/// An error that can occur while attempting to enqueue an item. +#[derive(Debug, Error)] +pub enum EnqueueError { + /// The IO core was shut down, there is no connection anymore to send through. + #[error("IO closed")] + Closed(Option), + /// The request limit for locally buffered requests was hit, try again. + #[error("request limit hit")] + BufferLimitHit(Option), +} + +/// A reserved slot in the memory buffer of [`IoCore`], on a specific channel. +/// +/// Dropping the ticket will free up the slot again. +#[derive(Debug)] +pub struct RequestTicket { + /// Channel the slot is reserved in. + channel: ChannelId, + /// The semaphore permit that makes it work. + permit: OwnedSemaphorePermit, + /// Pre-allocated [`IoId`]. + io_id: IoId, +} + +impl Display for RequestTicket { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "RequestTicket {{ channel: {}, io_id: {} }}", + self.channel, self.io_id + ) + } +} + +/// A failure to reserve a slot in the queue. +pub enum ReservationError { + /// No buffer space available. + /// + /// The caller is free to retry later. + NoBufferSpaceAvailable, + /// Connection closed. + /// + /// The [`IoCore`] has shutdown or is shutting down, it is no longer possible to reserve slots. + Closed, +} + +impl RequestHandle { + /// Attempts to reserve a new request ticket. + #[inline] + pub fn try_reserve_request( + &self, + channel: ChannelId, + ) -> Result { + match self.shared.buffered_requests[channel.get() as usize] + .clone() + .try_acquire_owned() + { + Ok(permit) => Ok(RequestTicket { + channel, + permit, + io_id: IoId(self.next_io_id.fetch_add(1, Ordering::Relaxed)), + }), + + Err(TryAcquireError::Closed) => Err(ReservationError::Closed), + Err(TryAcquireError::NoPermits) => Err(ReservationError::NoBufferSpaceAvailable), + } + } + + /// Reserves a new request ticket. + #[inline] + pub async fn reserve_request(&self, channel: ChannelId) -> Option { + self.shared.buffered_requests[channel.get() as usize] + .clone() + .acquire_owned() + .await + .map(|permit| RequestTicket { + channel, + permit, + io_id: IoId(self.next_io_id.fetch_add(1, Ordering::Relaxed)), + }) + .ok() + } + + /// Downgrades a [`RequestHandle`] to a [`Handle`]. + #[inline(always)] + pub fn downgrade(self) -> Handle { + Handle { + sender: self.sender, + } + } +} + +impl Handle { + /// Enqueues a new request. + /// + /// Returns an [`IoId`] that can be used to refer to the request if successful. Returns the + /// payload as an error if the underlying IO layer has been closed. + /// + /// See [`RequestHandle`] for details on how to obtain a [`RequestTicket`]. + #[inline] + pub fn enqueue_request( + &mut self, + RequestTicket { + channel, + permit, + io_id, + }: RequestTicket, + payload: Option, + ) -> Result> { + // TODO: Panic if given semaphore ticket from wrong instance? + + self.sender + .send(QueuedItem::Request { + io_id, + channel, + payload, + permit, + }) + .map(|()| { + #[cfg(feature = "tracing")] + tracing::debug!(%io_id, %channel, "successfully enqueued"); + }) + .map_err(|send_err| { + #[cfg(feature = "tracing")] + tracing::debug!("failed to enqueue, remote closed"); + send_err.0.into_payload() + })?; + + Ok(io_id) + } + + /// Enqueues a response to an existing request. + /// + /// Callers are supposed to send only one response or cancellation per incoming request. + pub fn enqueue_response( + &self, + channel: ChannelId, + id: Id, + payload: Option, + ) -> Result<(), EnqueueError> { + self.sender + .send(QueuedItem::Response { + channel, + id, + payload, + }) + .map_err(|send_err| EnqueueError::Closed(send_err.0.into_payload())) + } + + /// Enqueues a cancellation to an existing outgoing request. + /// + /// If the request has already been answered or cancelled, the enqueue cancellation will + /// ultimately have no effect. + pub fn enqueue_request_cancellation(&self, io_id: IoId) -> Result<(), EnqueueError> { + self.sender + .send(QueuedItem::RequestCancellation { io_id }) + .map_err(|send_err| EnqueueError::Closed(send_err.0.into_payload())) + } + + /// Enqueues a cancellation as a response to a received request. + /// + /// Callers are supposed to send only one response or cancellation per incoming request. + pub fn enqueue_response_cancellation( + &self, + channel: ChannelId, + id: Id, + ) -> Result<(), EnqueueError> { + self.sender + .send(QueuedItem::ResponseCancellation { id, channel }) + .map_err(|send_err| EnqueueError::Closed(send_err.0.into_payload())) + } + + /// Enqueues an error. + /// + /// Enqueuing an error causes the [`IoCore`] to begin shutting down immediately, only making an + /// effort to finish sending the error before doing so. + pub fn enqueue_error( + &self, + channel: ChannelId, + id: Id, + payload: Bytes, + ) -> Result<(), EnqueueError> { + self.sender + .send(QueuedItem::Error { + id, + channel, + payload, + }) + .map_err(|send_err| EnqueueError::Closed(send_err.0.into_payload())) + } +} + +/// Read bytes into a buffer. +/// +/// Similar to [`AsyncReadExt::read_buf`], except it performs zero or more read calls until at least +/// `target` bytes are in `buf`. Specifically, this function will +/// +/// 1. Read bytes from `reader`, put them into `buf`, until there are at least `target` bytes +/// available in `buf` ready for consumption. +/// 2. Immediately retry when encountering any [`io::ErrorKind::Interrupted`] errors. +/// 3. Propagate upwards any other errors. +/// 4. Return `false` with less than `target` bytes available in `buf if the connection was closed. +/// 5. Return `true` on success, i.e. `buf` contains at least `target` bytes. +/// +/// # Cancellation safety +/// +/// This function is cancellation safe in the same way that [`AsyncReadExt::read_buf`] is. +async fn read_until_bytesmut<'a, R>( + reader: &'a mut R, + buf: &mut BytesMut, + target: usize, +) -> io::Result +where + R: AsyncReadExt + Sized + Unpin, +{ + let extra_required = target.saturating_sub(buf.remaining()); + buf.reserve(extra_required); + + while buf.remaining() < target { + match reader.read_buf(buf).await { + Ok(0) => return Ok(false), + Ok(_) => { + // We read some more bytes, continue. + } + Err(err) if matches!(err.kind(), io::ErrorKind::Interrupted) => { + // Ignore `Interrupted` errors, just retry. + } + Err(err) => return Err(err), + } + } + + Ok(true) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::VecDeque, + io, + pin::Pin, + task::{Context, Poll}, + }; + + use bytes::BytesMut; + use futures::{Future, FutureExt}; + use proptest_attr_macro::proptest; + use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; + + use super::read_until_bytesmut; + + /// A reader simulating a stuttering transmission. + #[derive(Debug, Default)] + struct StutteringReader { + /// Input events happening in the future. + input: VecDeque>>>, + } + + impl StutteringReader { + /// Adds a successful read to the reader. + fn push_data>>(&mut self, data: T) { + self.input.push_back(Ok(Some(data.into()))); + } + + /// Adds a delay, causing `Poll::Pending` to be returned by `AsyncRead::poll_read`. + fn push_pause(&mut self) { + self.input.push_back(Ok(None)); + } + + /// Adds an error to be produced by the reader. + fn push_error(&mut self, e: io::Error) { + self.input.push_back(Err(e)) + } + + /// Splits up a sequence of bytes into a series of reads, delays and intermittent + /// `Interrupted` errors. + /// + /// Assumes that `input_sequence` is a randomized byte string, as it will be used as a + /// source of entropy. + fn push_randomized_sequence(&mut self, mut input_sequence: &[u8]) { + /// Prime group order and maximum sequence length. + const ORDER: u8 = 13; + + fn gadd(a: u8, b: u8) -> u8 { + (a % ORDER + b % ORDER) % ORDER + } + + // State manipulated for pseudo-randomness. + let mut state = 5; + + while !input_sequence.is_empty() { + // Mix in bytes from the input sequence. + state = gadd(state, input_sequence[0]); + + // Decide what to do next: + match state { + // 1/ORDER chance of a pause. + 3 => self.push_pause(), + // 1/ORDER chance of an "interrupted" error. + 7 => self.push_error(io::Error::new(io::ErrorKind::Interrupted, "interrupted")), + // otherwise, determine a random chunk length and add a successful read. + _ => { + // We will read 1-13 bytes. + let max_run_length = + ((input_sequence[0] % ORDER + 1) as usize).min(input_sequence.len()); + + assert!(max_run_length > 0); + + self.push_data(&input_sequence[..max_run_length]); + + // Remove from input sequence. + input_sequence = &input_sequence[max_run_length..]; + + if input_sequence.is_empty() { + break; + } + } + } + + // Increment state if it would be cyclical otherwise. + if state == gadd(state, input_sequence[0]) { + state = (state + 1) % ORDER; + } + } + } + } + + impl AsyncRead for StutteringReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.input.pop_front() { + Some(Ok(Some(data))) => { + // Slightly slower to initialize twice, but safer. We don't need peak + // performance for this test code. + let dest = buf.initialize_unfilled(); + let split_point = dest.len().min(data.len()); + + let (to_write, remainder) = data.split_at(split_point); + dest[0..split_point].copy_from_slice(to_write); + buf.advance(to_write.len()); + + // If we did not read the entire chunk, add back to input stream. + if !remainder.is_empty() { + self.input.push_front(Ok(Some(remainder.into()))); + } + + Poll::Ready(Ok(())) + } + Some(Ok(None)) => { + // Return one pending, but ensure we're woken up immediately afterwards. + + let waker = cx.waker().clone(); + waker.wake(); + + Poll::Pending + } + Some(Err(e)) => { + // Return the scheduled error. + Poll::Ready(Err(e)) + } + None => { + // No data to read, the 0-byte read will be detected by the caller. + + Poll::Ready(Ok(())) + } + } + } + } + + #[test] + fn stuttering_reader_reads_correctly() { + let mut reader = StutteringReader::default(); + + reader.push_data(&b"foo"[..]); + reader.push_error(io::Error::new(io::ErrorKind::Interrupted, "interrupted")); + reader.push_data(&b"bar"[..]); + reader.push_pause(); + reader.push_data(&b"baz"[..]); + reader.push_pause(); + reader.push_error(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe")); + + let mut buf = [0u8; 1024]; + + let bytes_read = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect("should not fail"); + + assert_eq!(bytes_read, 3); + assert_eq!(&buf[..3], b"foo"); + + // Interrupted error. + let interrupted_err = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect_err("should fail"); + assert_eq!(interrupted_err.to_string(), "interrupted"); + + // Let's try a partial read next. + + let bytes_read = reader + .read(&mut buf[0..2]) + .now_or_never() + .expect("should be ready") + .expect("should not fail"); + + assert_eq!(bytes_read, 2); + assert_eq!(&buf[..2], b"ba"); + + let bytes_read = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect("should not fail"); + + assert_eq!(bytes_read, 1); + assert_eq!(&buf[..1], b"r"); + + assert!( + reader.read(&mut buf).now_or_never().is_none(), + "expected pending read" + ); + + // The waker has been called again already, so we attempt another read. + let bytes_read = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect("should not fail"); + + assert_eq!(bytes_read, 3); + assert_eq!(&buf[..3], b"baz"); + + assert!( + reader.read(&mut buf).now_or_never().is_none(), + "expected pending read" + ); + + let broken_pipe_err = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect_err("should fail"); + assert_eq!(broken_pipe_err.to_string(), "broken pipe"); + + // The final read should be a 0-length read. + let bytes_read = reader + .read(&mut buf) + .now_or_never() + .expect("should be ready") + .expect("should not fail"); + + assert_eq!(bytes_read, 0); + } + + #[proptest] + fn randomized_sequences_build_correctly(input: Vec) { + let mut reader = StutteringReader::default(); + reader.push_randomized_sequence(&input); + + let mut output: Vec = Vec::with_capacity(input.len()); + let mut buffer = [0u8; 512]; + loop { + match reader.read(&mut buffer).now_or_never() { + None => { + // `Poll::Pending`, ignore and try again. + } + Some(Ok(0)) => { + // We are done reading. + break; + } + Some(Ok(n)) => { + output.extend(&buffer[..n]); + } + Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => { + // Try again. + } + Some(Err(e)) => { + panic!("did not expect error {}", e); + } + } + } + + assert_eq!(output, input); + } + + /// Polls a future in a busy loop. + fn poll_forever(mut fut: F) -> ::Output { + loop { + let waker = futures::task::noop_waker(); + let mut cx = Context::from_waker(&waker); + + let fut_pinned = unsafe { Pin::new_unchecked(&mut fut) }; + match fut_pinned.poll(&mut cx) { + Poll::Ready(val) => return val, + Poll::Pending => continue, + } + } + } + + #[proptest] + fn read_until_bytesmut_into_empty_buffer_succeeds(input: Vec) { + // We are trying to read any sequence that is guaranteed to finish into an empty buffer: + for n in 1..(input.len()) { + let mut reader = StutteringReader::default(); + reader.push_randomized_sequence(&input); + + let mut buf = BytesMut::new(); + let read_successful = poll_forever(read_until_bytesmut(&mut reader, &mut buf, n)) + .expect("reading should not fail"); + + assert!(read_successful); + assert_eq!(buf[..n], input[..n]); + } + } + + #[proptest] + fn read_until_bytesmut_eventually_fills_buffer(input: Vec) { + // Given a stuttering reader with the correct amount of input available, check if we can + // fill it going one-by-one. + let mut reader = StutteringReader::default(); + reader.push_randomized_sequence(&input); + + let mut buf = BytesMut::new(); + + for target in 0..=input.len() { + let read_complete = poll_forever(read_until_bytesmut(&mut reader, &mut buf, target)) + .expect("reading should not fail"); + + assert!(read_complete); + } + + assert_eq!(buf.to_vec(), input); + } + + #[proptest] + fn read_until_bytesmut_gives_up_if_not_enough_available(input: Vec) { + for read_past in 1..(3 * input.len()) { + // Trying to read past a closed connection should result in `false` being returned. + let mut reader = StutteringReader::default(); + reader.push_randomized_sequence(&input); + + let mut buf = BytesMut::new(); + + let read_complete = poll_forever(read_until_bytesmut( + &mut reader, + &mut buf, + input.len() + read_past, + )) + .expect("reading should not fail"); + + assert!(!read_complete); + + // We still should find out input in `buf`. + assert_eq!(buf.to_vec(), input); + } + } +} diff --git a/juliet/src/lib.rs b/juliet/src/lib.rs new file mode 100644 index 0000000000..9ba4cc0579 --- /dev/null +++ b/juliet/src/lib.rs @@ -0,0 +1,420 @@ +#![doc(html_root_url = "https://docs.rs/juliet/0.1.0")] +#![doc( + html_favicon_url = "https://raw.githubusercontent.com/casper-network/casper-node/master/images/CasperLabs_Logo_Favicon_RGB_50px.png", + html_logo_url = "https://raw.githubusercontent.com/casper-network/casper-node/master/images/CasperLabs_Logo_Symbol_RGB.png", + test(attr(deny(warnings))) +)] +#![warn(missing_docs, trivial_casts, trivial_numeric_casts)] +#![doc = include_str!("../README.md")] + +//! +//! +//! ## General usage +//! +//! This crate is split into three layers, whose usage depends on an application's specific use +//! case. At the very core sits the [`protocol`] module, which is a side-effect-free implementation +//! of the protocol. The caller is responsible for all IO flowing in and out, but it is instructed +//! by the state machine what to do next. +//! +//! If there is no need to roll custom IO, the [`io`] layer provides a complete `tokio`-based +//! solution that operates on [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`]. It handles +//! multiplexing input, output and scheduling, as well as buffering messages using a wait and a +//! ready queue. +//! +//! Most users of the library will likely use the highest level layer, [`rpc`] instead. It sits on +//! top the raw [`io`] layer and wraps all the functionality in safe Rust types, making misuse of +//! the underlying protocol hard, if not impossible. + +pub mod header; +pub mod io; +pub mod protocol; +pub mod rpc; +pub(crate) mod util; +pub mod varint; + +use std::{ + fmt::{self, Display}, + num::NonZeroU32, +}; + +/// A channel identifier. +/// +/// Newtype wrapper to prevent accidental mixups between regular [`u8`]s and those used as channel +/// IDs. Does not indicate whether or not a channel ID is actually valid, i.e. a channel that +/// exists. +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] +#[repr(transparent)] +pub struct ChannelId(u8); + +impl Display for ChannelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl ChannelId { + /// Creates a new channel ID. + #[inline(always)] + pub const fn new(chan: u8) -> Self { + ChannelId(chan) + } + + /// Returns the channel ID as [`u8`]. + #[inline(always)] + pub const fn get(self) -> u8 { + self.0 + } +} + +impl From for u8 { + #[inline(always)] + fn from(value: ChannelId) -> Self { + value.get() + } +} + +/// An identifier for a `juliet` message. +/// +/// Newtype wrapper to prevent accidental mixups between regular [`u16`]s and those used as IDs. +/// Does not indicate whether or not an ID refers to an existing request. +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] +#[repr(transparent)] +pub struct Id(u16); + +impl Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Id { + /// Creates a new identifier. + #[inline(always)] + pub const fn new(id: u16) -> Self { + Id(id) + } + + /// Returns the channel ID as [`u16`]. + #[inline(always)] + pub const fn get(self) -> u16 { + self.0 + } +} + +impl From for u16 { + #[inline(always)] + fn from(value: Id) -> Self { + value.get() + } +} + +/// The outcome of a parsing operation on a potentially incomplete buffer. +#[derive(Debug, Eq, PartialEq)] +#[must_use] +pub enum Outcome { + /// The given data was incomplete, at least the given amount of additional bytes is needed. + Incomplete(NonZeroU32), + /// An fatal error was found in the given input. + Fatal(E), + /// The parse was successful and the underlying buffer has been modified to extract `T`. + Success(T), +} + +impl Outcome { + /// Expects the outcome, similar to [`std::result::Result::expect`]. + /// + /// Returns the value of [`Outcome::Success`]. + /// + /// # Panics + /// + /// Will panic if the [`Outcome`] is not [`Outcome::Success`]. + #[inline] + #[track_caller] + pub fn expect(self, msg: &str) -> T { + match self { + Outcome::Success(value) => value, + Outcome::Incomplete(_) => panic!("incomplete: {}", msg), + Outcome::Fatal(_) => panic!("error: {}", msg), + } + } + + /// Maps the error of an [`Outcome`]. + #[inline] + pub fn map_err(self, f: F) -> Outcome + where + F: FnOnce(E) -> E2, + { + match self { + Outcome::Incomplete(n) => Outcome::Incomplete(n), + Outcome::Fatal(err) => Outcome::Fatal(f(err)), + Outcome::Success(value) => Outcome::Success(value), + } + } + + /// Helper function to construct an [`Outcome::Incomplete`]. + #[inline] + #[track_caller] + pub fn incomplete(remaining: usize) -> Outcome { + Outcome::Incomplete( + NonZeroU32::new(u32::try_from(remaining).expect("did not expect large usize")) + .expect("did not expect 0-byte `Incomplete`"), + ) + } + + /// Converts an [`Outcome`] into a result, panicking on [`Outcome::Incomplete`]. + /// + /// This function should never be used outside tests. + #[cfg(test)] + #[track_caller] + pub fn to_result(self) -> Result { + match self { + Outcome::Incomplete(missing) => { + panic!( + "did not expect incompletion by {} bytes converting to result", + missing + ) + } + Outcome::Fatal(e) => Err(e), + Outcome::Success(s) => Ok(s), + } + } +} + +/// `try!` for [`Outcome`]. +/// +/// Will pass [`Outcome::Incomplete`] and [`Outcome::Fatal`] upwards, or unwrap the value found in +/// [`Outcome::Success`]. +#[macro_export] +macro_rules! try_outcome { + ($src:expr) => { + match $src { + Outcome::Incomplete(n) => return Outcome::Incomplete(n), + Outcome::Fatal(err) => return Outcome::Fatal(err.into()), + Outcome::Success(value) => value, + } + }; +} + +/// Channel configuration values that needs to be agreed upon by all clients. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct ChannelConfiguration { + /// Maximum number of requests allowed on the channel. + request_limit: u16, + /// Maximum size of a request sent across the channel. + max_request_payload_size: u32, + /// Maximum size of a response sent across the channel. + max_response_payload_size: u32, +} + +impl Default for ChannelConfiguration { + fn default() -> Self { + Self::new() + } +} + +impl ChannelConfiguration { + /// Creates a new [`ChannelConfiguration`] with default values. + pub const fn new() -> Self { + Self { + request_limit: 1, + max_request_payload_size: 0, + max_response_payload_size: 0, + } + } + + /// Creates a configuration with the given request limit (default is 1). + pub const fn with_request_limit(mut self, request_limit: u16) -> ChannelConfiguration { + self.request_limit = request_limit; + self + } + + /// Creates a configuration with the given maximum size for request payloads (default is 0). + /// + /// There is nothing magical about payload sizes, a size of 0 allows for payloads that are no + /// longer than 0 bytes in size. On the protocol level, there is a distinction between a request + /// with a zero-sized payload and no payload. + pub const fn with_max_request_payload_size( + mut self, + max_request_payload_size: u32, + ) -> ChannelConfiguration { + self.max_request_payload_size = max_request_payload_size; + self + } + + /// Creates a configuration with the given maximum size for response payloads (default is 0). + /// + /// There is nothing magical about payload sizes, a size of 0 allows for payloads that are no + /// longer than 0 bytes in size. On the protocol level, there is a distinction between a + /// response with a zero-sized payload and no payload. + pub const fn with_max_response_payload_size( + mut self, + max_response_payload_size: u32, + ) -> ChannelConfiguration { + self.max_response_payload_size = max_response_payload_size; + self + } +} + +#[cfg(test)] +mod tests { + use proptest::{ + prelude::Arbitrary, + strategy::{Map, Strategy}, + }; + use proptest_attr_macro::proptest; + + use crate::{ChannelConfiguration, ChannelId, Id, Outcome}; + + impl Arbitrary for ChannelId { + type Parameters = ::Parameters; + + #[inline] + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + ::arbitrary_with(args).prop_map(Self::new) + } + + type Strategy = Map<::Strategy, fn(u8) -> Self>; + } + + impl Arbitrary for Id { + type Parameters = ::Parameters; + + #[inline] + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + ::arbitrary_with(args).prop_map(Self::new) + } + + type Strategy = Map<::Strategy, fn(u16) -> Self>; + } + + #[proptest] + fn id_type_smoke_tests(raw: u16) { + let id = Id::new(raw); + assert_eq!(id.get(), raw); + assert_eq!(u16::from(id), raw); + assert_eq!(raw.to_string(), id.to_string()); + } + + #[proptest] + fn channel_type_smoke_tests(raw: u8) { + let channel_id = ChannelId::new(raw); + assert_eq!(channel_id.get(), raw); + assert_eq!(u8::from(channel_id), raw); + assert_eq!(raw.to_string(), channel_id.to_string()); + } + + #[test] + fn outcome_incomplete_works_on_non_zero() { + assert!(matches!( + Outcome::<(), ()>::incomplete(1), + Outcome::Incomplete(_) + )); + + assert!(matches!( + Outcome::<(), ()>::incomplete(100), + Outcome::Incomplete(_) + )); + + assert!(matches!( + Outcome::<(), ()>::incomplete(u32::MAX as usize), + Outcome::Incomplete(_) + )); + } + + #[test] + #[should_panic(expected = "did not expect 0-byte `Incomplete`")] + fn outcome_incomplete_panics_on_0() { + let _ = Outcome::<(), ()>::incomplete(0); + } + + #[test] + #[should_panic(expected = "did not expect large usize")] + fn outcome_incomplete_panics_past_u32_max() { + let _ = Outcome::<(), ()>::incomplete(u32::MAX as usize + 1); + } + + #[test] + fn outcome_expect_works_on_success() { + let outcome: Outcome = Outcome::Success(12); + assert_eq!(outcome.expect("should not panic"), 12); + } + + #[test] + #[should_panic(expected = "is incomplete")] + fn outcome_expect_panics_on_incomplete() { + let outcome: Outcome = Outcome::incomplete(1); + outcome.expect("is incomplete"); + } + + #[test] + #[should_panic(expected = "is fatal")] + fn outcome_expect_panics_on_fatal() { + let outcome: Outcome = Outcome::Fatal(()); + outcome.expect("is fatal"); + } + + #[test] + fn outcome_map_err_works_correctly() { + let plus_1 = |x: u8| x as u16 + 1; + + let success = Outcome::Success(1); + assert_eq!(success.map_err(plus_1), Outcome::Success(1)); + + let incomplete = Outcome::<(), u8>::incomplete(1); + assert_eq!( + incomplete.map_err(plus_1), + Outcome::<(), u16>::incomplete(1) + ); + + let fatal = Outcome::Fatal(1); + assert_eq!(fatal.map_err(plus_1), Outcome::<(), u16>::Fatal(2)); + } + + #[test] + fn outcome_to_result_works_correctly() { + let success = Outcome::<_, ()>::Success(1); + assert_eq!(success.to_result(), Ok(1)); + + let fatal = Outcome::<(), _>::Fatal(1); + assert_eq!(fatal.to_result(), Err(1)); + } + + #[test] + #[should_panic(expected = "did not expect incompletion by 1 bytes converting to result")] + fn outcome_to_result_panics_on_incomplete() { + let _ = Outcome::<(), u8>::incomplete(1).to_result(); + } + + #[test] + fn try_outcome_works() { + fn try_outcome_func(input: Outcome) -> Outcome { + let value = try_outcome!(input); + Outcome::Success(value as u16 + 1) + } + + assert_eq!(try_outcome_func(Outcome::Success(1)), Outcome::Success(2)); + assert_eq!( + try_outcome_func(Outcome::incomplete(123)), + Outcome::incomplete(123) + ); + assert_eq!(try_outcome_func(Outcome::Fatal(-123)), Outcome::Fatal(-123)); + } + + #[test] + fn channel_configuration_can_be_built() { + let mut chan_cfg = ChannelConfiguration::new(); + assert_eq!(chan_cfg, ChannelConfiguration::default()); + + chan_cfg = chan_cfg.with_request_limit(123); + assert_eq!(chan_cfg.request_limit, 123); + + chan_cfg = chan_cfg.with_max_request_payload_size(99); + assert_eq!(chan_cfg.request_limit, 123); + assert_eq!(chan_cfg.max_request_payload_size, 99); + + chan_cfg = chan_cfg.with_max_response_payload_size(77); + assert_eq!(chan_cfg.request_limit, 123); + assert_eq!(chan_cfg.max_request_payload_size, 99); + assert_eq!(chan_cfg.max_response_payload_size, 77); + } +} diff --git a/juliet/src/protocol.rs b/juliet/src/protocol.rs new file mode 100644 index 0000000000..e880ddc908 --- /dev/null +++ b/juliet/src/protocol.rs @@ -0,0 +1,2512 @@ +//! Protocol parsing state machine. +//! +//! The [`JulietProtocol`] type is designed to encapsulate the entire juliet protocol without any +//! dependencies on IO facilities; it can thus be dropped into almost any environment (`std::io`, +//! various `async` runtimes, etc.) with no changes. +//! +//! ## Usage +//! +//! An instance of [`JulietProtocol`] must be created using [`JulietProtocol::builder`], the +//! resulting builder can be used to fine-tune the configuration of the given protocol. The +//! parameter `N` denotes the number of valid channels, which must be set at compile time. See the +//! type's documentation for more details. +//! +//! ## Efficiency +//! +//! In general, all bulky data used in the protocol is as zero-copy as possible, for example large +//! messages going out in multiple frames will still share the one original payload buffer passed in +//! at construction. The "exception" to this is the re-assembly of multi-frame messages, which +//! causes fragments to be copied once to form a contiguous byte sequence for the payload to avoid +//! memory-exhaustion attacks based on the semantics of the underlying [`bytes::BytesMut`]. + +mod multiframe; +mod outgoing_message; + +use std::{collections::HashSet, fmt::Display, num::NonZeroU32}; + +use bytes::{Buf, Bytes, BytesMut}; +use thiserror::Error; + +use self::multiframe::MultiframeReceiver; +pub use self::outgoing_message::{FrameIter, OutgoingFrame, OutgoingMessage}; +use crate::{ + header::{self, ErrorKind, Header, Kind}, + try_outcome, + util::{Index, PayloadFormat}, + varint::{decode_varint32, Varint32}, + ChannelConfiguration, ChannelId, Id, + Outcome::{self, Fatal, Incomplete, Success}, +}; + +/// A channel ID to fill in when the channel is actually unknown or not relevant. +/// +/// Note that this is not a reserved channel, just a default chosen -- it may clash with an +/// actually active channel. +const UNKNOWN_CHANNEL: ChannelId = ChannelId::new(0); + +/// An ID to fill in when the ID should not matter. +/// +/// Not a reserved id, it may clash with existing ones. +const UNKNOWN_ID: Id = Id::new(0); + +/// Maximum frame size. +/// +/// The maximum configured frame size is subject to some invariants and is wrapped into a newtype +/// for convenience. +#[derive(Copy, Clone, Debug)] +#[repr(transparent)] +pub struct MaxFrameSize(u32); + +impl MaxFrameSize { + /// The minimum sensible frame size maximum. + /// + /// Set to fit at least a full preamble and a single byte of payload. + pub const MIN: u32 = Header::SIZE as u32 + Varint32::MAX_LEN as u32 + 1; + + /// Recommended default for the maximum frame size. + /// + /// Chosen according to the Juliet RFC. + pub const DEFAULT: MaxFrameSize = MaxFrameSize(4096); + + /// Constructs a new maximum frame size. + /// + /// # Panics + /// + /// Will panic if the given maximum frame size is less than [`MaxFrameSize::MIN`]. + #[inline(always)] + pub const fn new(max_frame_size: u32) -> Self { + assert!( + max_frame_size >= Self::MIN, + "given maximum frame size is below permissible minimum for maximum frame size" + ); + MaxFrameSize(max_frame_size) + } + + /// Returns the maximum frame size. + #[inline(always)] + pub const fn get(self) -> u32 { + self.0 + } + + /// Returns the maximum frame size cast as `usize`. + #[inline(always)] + pub const fn get_usize(self) -> usize { + // Safe cast on all 32-bit and up systems. + self.0 as usize + } + + /// Returns the maximum frame size without the header size. + #[inline(always)] + pub const fn without_header(self) -> usize { + self.get_usize() - Header::SIZE + } +} + +impl Default for MaxFrameSize { + #[inline(always)] + fn default() -> Self { + MaxFrameSize::DEFAULT + } +} + +/// A parser/state machine that processes an incoming stream and is able to construct messages to +/// send out. +/// +/// `N` denotes the number of valid channels, which should be fixed and agreed upon by both peers +/// prior to initialization. +/// +/// ## Input +/// +/// This type does not handle IO, rather it expects a growing [`BytesMut`] buffer to be passed in, +/// containing incoming data, using the [`JulietProtocol::process_incoming`] method. +/// +/// ## Output +/// +/// Multiple methods create [`OutgoingMessage`] values: +/// +/// * [`JulietProtocol::create_request`] +/// * [`JulietProtocol::create_response`] +/// * [`JulietProtocol::cancel_request`] +/// * [`JulietProtocol::cancel_response`] +/// * [`JulietProtocol::custom_error`] +/// +/// Their return types are usually converted into frames via [`OutgoingMessage::frames()`] and need +/// to be sent to the peer. +#[derive(Debug)] +#[cfg_attr(test, derive(Clone))] +pub struct JulietProtocol { + /// Bi-directional channels. + channels: [Channel; N], + /// The maximum size for a single frame. + max_frame_size: MaxFrameSize, +} + +/// A builder for a [`JulietProtocol`] instance. +/// +/// Created using [`JulietProtocol::builder`]. +/// +/// # Note +/// +/// Typically a single instance of the [`ProtocolBuilder`] can be kept around in an application +/// handling multiple connections, as its [`ProtocolBuilder::build()`] method can be reused for +/// every new connection instance. +#[derive(Debug)] +pub struct ProtocolBuilder { + /// Configuration for every channel. + channel_config: [ChannelConfiguration; N], + /// Maximum frame size. + max_frame_size: MaxFrameSize, +} + +impl Default for ProtocolBuilder { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl ProtocolBuilder { + /// Creates a new protocol builder with default configuration for every channel. + pub const fn new() -> Self { + Self::with_default_channel_config(ChannelConfiguration::new()) + } + + /// Creates a new protocol builder with all channels preconfigured using the given config. + #[inline] + pub const fn with_default_channel_config(config: ChannelConfiguration) -> Self { + Self { + channel_config: [config; N], + max_frame_size: MaxFrameSize::DEFAULT, + } + } + + /// Update the channel configuration for a given channel. + pub const fn channel_config( + mut self, + channel: ChannelId, + config: ChannelConfiguration, + ) -> Self { + self.channel_config[channel.get() as usize] = config; + self + } + + /// Constructs a new protocol instance from the given builder. + pub fn build(&self) -> JulietProtocol { + let channels: [Channel; N] = + array_init::map_array_init(&self.channel_config, |cfg| Channel::new(*cfg)); + + JulietProtocol { + channels, + max_frame_size: self.max_frame_size, + } + } + + /// Sets the maximum frame size. + /// + /// # Panics + /// + /// Will panic if the maximum size is too small to hold a header, payload length and at least + /// one byte of payload (see [`MaxFrameSize::MIN`]). + pub const fn max_frame_size(mut self, max_frame_size: u32) -> Self { + self.max_frame_size = MaxFrameSize::new(max_frame_size); + self + } +} + +/// Per-channel data. +/// +/// Used internally by the protocol to keep track. This data structure closely tracks the +/// information specified in the juliet RFC. +#[derive(Debug)] +#[cfg_attr(test, derive(Clone))] +struct Channel { + /// A set of request IDs from requests received that have not been answered with a response or + /// cancellation yet. + incoming_requests: HashSet, + /// A set of request IDs of requests made, for which no response or cancellation has been + /// received yet. + outgoing_requests: HashSet, + /// The multiframe receiver state machine. + /// + /// Every channel allows for at most one multi-frame message to be in progress at the same + /// time. + current_multiframe_receiver: MultiframeReceiver, + /// Number of requests received minus number of cancellations received. + /// + /// Capped at the request limit. + cancellation_allowance: u16, + /// Protocol-specific configuration values. + config: ChannelConfiguration, + /// The last request ID generated. + prev_request_id: u16, +} + +impl Channel { + /// Creates a new channel, based on the given configuration. + #[inline(always)] + fn new(config: ChannelConfiguration) -> Self { + Channel { + incoming_requests: Default::default(), + outgoing_requests: Default::default(), + current_multiframe_receiver: MultiframeReceiver::default(), + cancellation_allowance: 0, + config, + prev_request_id: 0, + } + } + + /// Returns whether or not the peer has exhausted the number of in-flight requests allowed. + #[inline] + pub fn is_at_max_incoming_requests(&self) -> bool { + self.incoming_requests.len() >= self.config.request_limit as usize + } + + /// Increments the cancellation allowance if possible. + /// + /// This method should be called everytime a valid request is received. + #[inline] + fn increment_cancellation_allowance(&mut self) { + if self.cancellation_allowance < self.config.request_limit { + self.cancellation_allowance += 1; + } + } + + /// Generates an unused ID for an outgoing request on this channel. + /// + /// Returns `None` if the entire ID space has been exhausted. Note that this should never + /// occur under reasonable conditions, as the request limit should be less than [`u16::MAX`]. + #[inline] + fn generate_request_id(&mut self) -> Option { + if self.outgoing_requests.len() == u16::MAX as usize { + // We've exhausted the entire ID space. + return None; + } + + let mut candidate = Id(self.prev_request_id.wrapping_add(1)); + while self.outgoing_requests.contains(&candidate) { + candidate = Id(candidate.0.wrapping_add(1)); + } + + self.prev_request_id = candidate.0; + + Some(candidate) + } + + /// Returns whether or not it is permissible to send another request on given channel. + #[inline] + pub fn allowed_to_send_request(&self) -> bool { + self.outgoing_requests.len() < self.config.request_limit as usize + } + + /// Creates a new request, bypassing all client-side checks. + /// + /// Low-level function that does nothing but create a syntactically correct request and track + /// its outgoing ID. This function is not meant to be called outside of this module or its unit + /// tests. See [`JulietProtocol::create_request`] instead. + #[inline(always)] + fn create_unchecked_request( + &mut self, + channel_id: ChannelId, + payload: Option, + ) -> OutgoingMessage { + // The `unwrap_or` below should never be triggered, as long as `u16::MAX` or less + // requests are currently in flight, which is always the case with safe API use. + let id = self.generate_request_id().unwrap_or(Id(0)); + + // Record the outgoing request for later. + self.outgoing_requests.insert(id); + + if let Some(payload) = payload { + let header = Header::new(header::Kind::RequestPl, channel_id, id); + OutgoingMessage::new(header, Some(payload)) + } else { + let header = Header::new(header::Kind::Request, channel_id, id); + OutgoingMessage::new(header, None) + } + } +} + +/// Creates a new response without checking or altering channel states. +/// +/// Low-level function exposed for testing. Does not affect the tracking of IDs, thus can be used to +/// send duplicate or ficticious responses. +#[inline(always)] +fn create_unchecked_response( + channel: ChannelId, + id: Id, + payload: Option, +) -> OutgoingMessage { + if let Some(payload) = payload { + let header = Header::new(header::Kind::ResponsePl, channel, id); + OutgoingMessage::new(header, Some(payload)) + } else { + let header = Header::new(header::Kind::Response, channel, id); + OutgoingMessage::new(header, None) + } +} + +/// Creates a request cancellation without checks. +/// +/// Low-level function exposed for testing. Does not verify that the given request exists or has not +/// been cancelled before. +#[inline(always)] +fn create_unchecked_request_cancellation(channel: ChannelId, id: Id) -> OutgoingMessage { + let header = Header::new(header::Kind::CancelReq, channel, id); + OutgoingMessage::new(header, None) +} + +/// Creates a response cancellation without checks. +/// +/// Low-level function exposed for testing. Does not verify that the given request has been received +/// or a response sent already. +fn create_unchecked_response_cancellation(channel: ChannelId, id: Id) -> OutgoingMessage { + let header = Header::new(header::Kind::CancelResp, channel, id); + OutgoingMessage::new(header, None) +} + +/// A successful read from the peer. +#[must_use] +#[derive(Debug, Eq, PartialEq)] +pub enum CompletedRead { + /// An error has been received. + /// + /// The connection on our end should be closed, the peer will do the same. + ErrorReceived { + /// The error header. + header: Header, + /// The error data (only with [`ErrorKind::Other`]). + data: Option, + }, + /// A new request has been received. + NewRequest { + /// The channel of the request. + channel: ChannelId, + /// The ID of the request. + id: Id, + /// Request payload. + payload: Option, + }, + /// A response to one of our requests has been received. + ReceivedResponse { + /// The channel of the response. + channel: ChannelId, + /// The ID of the request received. + id: Id, + /// The response payload. + payload: Option, + }, + /// A request was cancelled by the peer. + RequestCancellation { + /// The channel of the request cancellation. + channel: ChannelId, + /// ID of the request to be cancelled. + id: Id, + }, + /// A response was cancelled by the peer. + ResponseCancellation { + /// The channel of the response cancellation. + channel: ChannelId, + /// The ID of the response to be cancelled. + id: Id, + }, +} + +impl Display for CompletedRead { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CompletedRead::ErrorReceived { header, data } => { + write!(f, "ErrorReceived {{ header: {}", header)?; + + if let Some(data) = data { + write!(f, ", data: {}", PayloadFormat(data))?; + } + + f.write_str(" }") + } + CompletedRead::NewRequest { + channel, + id, + payload, + } => { + write!(f, "NewRequest {{ channel: {}, id: {}", channel, id)?; + + if let Some(payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + + f.write_str(" }") + } + CompletedRead::ReceivedResponse { + channel, + id, + payload, + } => { + write!(f, "ReceivedResponse {{ channel: {}, id: {}", channel, id)?; + + if let Some(payload) = payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + + f.write_str(" }") + } + CompletedRead::RequestCancellation { channel, id } => { + write!( + f, + "RequestCancellation {{ channel: {}, id: {} }}", + channel, id + ) + } + CompletedRead::ResponseCancellation { channel, id } => { + write!( + f, + "ResponseCancellation {{ channel: {}, id: {} }}", + channel, id + ) + } + } + } +} + +/// The caller of the this crate has violated the protocol. +/// +/// A correct implementation of a client should never encounter this, thus simply unwrapping every +/// instance of this as part of a `Result<_, LocalProtocolViolation>` is usually a valid choice. +/// +/// Higher level layers like [`rpc`](crate::rpc) should make it impossible to encounter +/// [`LocalProtocolViolation`]s. +#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)] +pub enum LocalProtocolViolation { + /// A request was not sent because doing so would exceed the request limit on channel. + /// + /// Wait for additional requests to be cancelled or answered. Calling + /// [`JulietProtocol::allowed_to_send_request()`] beforehand is recommended. + #[error("sending would exceed request limit")] + WouldExceedRequestLimit, + /// The channel given does not exist. + /// + /// The given [`ChannelId`] exceeds `N` of [`JulietProtocol`]. + #[error("invalid channel")] + InvalidChannel(ChannelId), + /// The given payload exceeds the configured limit. + /// + /// See [`ChannelConfiguration::with_max_request_payload_size()`] and + /// [`ChannelConfiguration::with_max_response_payload_size()`] for details. + #[error("payload exceeds configured limit")] + PayloadExceedsLimit, + /// The given error payload exceeds a single frame. + /// + /// Error payloads may not span multiple frames, shorten the payload or increase frame size. + #[error("error payload would be multi-frame")] + ErrorPayloadIsMultiFrame, +} + +macro_rules! log_frame { + ($header:expr) => { + #[cfg(feature = "tracing")] + tracing::trace!(header=%$header, "received"); + }; + ($header:expr, $payload:expr) => { + #[cfg(feature = "tracing")] + tracing::trace!(header=%$header, payload=%crate::util::PayloadFormat(&$payload), "received"); + }; +} + +impl JulietProtocol { + /// Creates a new juliet protocol builder instance. + #[inline] + pub const fn builder(config: ChannelConfiguration) -> ProtocolBuilder { + ProtocolBuilder { + channel_config: [config; N], + max_frame_size: MaxFrameSize::DEFAULT, + } + } + + /// Looks up a given channel by ID. + /// + /// Returns a `LocalProtocolViolation` if called with non-existent channel. + #[inline(always)] + const fn lookup_channel(&self, channel: ChannelId) -> Result<&Channel, LocalProtocolViolation> { + if channel.0 as usize >= N { + Err(LocalProtocolViolation::InvalidChannel(channel)) + } else { + Ok(&self.channels[channel.0 as usize]) + } + } + + /// Looks up a given channel by ID, mutably. + /// + /// Returns a `LocalProtocolViolation` if called with non-existent channel. + #[inline(always)] + fn lookup_channel_mut( + &mut self, + channel: ChannelId, + ) -> Result<&mut Channel, LocalProtocolViolation> { + if channel.0 as usize >= N { + Err(LocalProtocolViolation::InvalidChannel(channel)) + } else { + Ok(&mut self.channels[channel.0 as usize]) + } + } + + /// Returns the configured maximum frame size. + #[inline(always)] + pub const fn max_frame_size(&self) -> MaxFrameSize { + self.max_frame_size + } + + /// Returns whether or not it is permissible to send another request on given channel. + #[inline] + pub fn allowed_to_send_request( + &self, + channel: ChannelId, + ) -> Result { + self.lookup_channel(channel) + .map(Channel::allowed_to_send_request) + } + + /// Creates a new request to be sent. + /// + /// The outgoing request message's ID will be recorded in the outgoing set, for this reason a + /// caller must send the returned outgoing message or it will be considered in-flight + /// perpetually, unless explicitly cancelled. + /// + /// The resulting messages may be multi-frame messages, see + /// [`OutgoingMessage::is_multi_frame()`]) for details. + /// + /// # Local protocol violations + /// + /// Will return a [`LocalProtocolViolation`] when attempting to send on an invalid channel, the + /// payload exceeds the configured maximum for the channel, or if the request rate limit has + /// been exceeded. Call [`JulietProtocol::allowed_to_send_request`] before calling + /// `create_request` to avoid this. + pub fn create_request( + &mut self, + channel: ChannelId, + payload: Option, + ) -> Result { + let chan = self.lookup_channel_mut(channel)?; + + if let Some(ref payload) = payload { + if payload.len() > chan.config.max_request_payload_size as usize { + return Err(LocalProtocolViolation::PayloadExceedsLimit); + } + } + + if !chan.allowed_to_send_request() { + return Err(LocalProtocolViolation::WouldExceedRequestLimit); + } + + Ok(chan.create_unchecked_request(channel, payload)) + } + + /// Creates a new response to be sent. + /// + /// If the ID was not in the outgoing set, it is assumed to have been cancelled earlier, thus no + /// response should be sent and `None` is returned by this method. + /// + /// Calling this method frees up a request ID, thus giving the remote peer permission to make + /// additional requests. While a legitimate peer will not know about the free ID until is has + /// received either a response or cancellation sent from the local end, an hostile peer could + /// attempt to spam if it knew the ID was going to be available quickly. For this reason, it is + /// recommended to not create responses too eagerly, rather only one at a time after the + /// previous response has finished sending. + /// + /// # Local protocol violations + /// + /// Will return a [`LocalProtocolViolation`] when attempting to send on an invalid channel or + /// the payload exceeds the configured maximum for the channel. + pub fn create_response( + &mut self, + channel: ChannelId, + id: Id, + payload: Option, + ) -> Result, LocalProtocolViolation> { + let chan = self.lookup_channel_mut(channel)?; + + if !chan.incoming_requests.remove(&id) { + // The request has been cancelled, no need to send a response. + return Ok(None); + } + + if let Some(ref payload) = payload { + if payload.len() > chan.config.max_response_payload_size as usize { + return Err(LocalProtocolViolation::PayloadExceedsLimit); + } + } + + Ok(Some(create_unchecked_response(channel, id, payload))) + } + + /// Creates a cancellation for an outgoing request. + /// + /// If the ID is not in the outgoing set, due to already being responded to or cancelled, `None` + /// will be returned. + /// + /// If the caller does not track the use of IDs separately to the [`JulietProtocol`] structure, + /// it is possible to cancel an ID that has already been reused. To avoid this, a caller should + /// take measures to ensure that only response or cancellation is ever sent for a given request. + /// + /// # Local protocol violations + /// + /// Will return a [`LocalProtocolViolation`] when attempting to send on an invalid channel. + pub fn cancel_request( + &mut self, + channel: ChannelId, + id: Id, + ) -> Result, LocalProtocolViolation> { + let chan = self.lookup_channel_mut(channel)?; + + if !chan.outgoing_requests.contains(&id) { + // The request has received a response already, no need to cancel. Note that merely + // sending the cancellation is not enough here, we still expect either cancellation or + // response from the peer. + return Ok(None); + } + + Ok(Some(create_unchecked_request_cancellation(channel, id))) + } + + /// Creates a cancellation of an incoming request. + /// + /// Incoming request cancellations are used to indicate that the local peer cannot or will not + /// respond to a given request. Since only either a response or a cancellation can be sent for + /// any given request, this function will return `None` if the given ID cannot be found in the + /// outbound set. + /// + /// # Local protocol violations + /// + /// Will return a [`LocalProtocolViolation`] when attempting to send on an invalid channel. + pub fn cancel_response( + &mut self, + channel: ChannelId, + id: Id, + ) -> Result, LocalProtocolViolation> { + let chan = self.lookup_channel_mut(channel)?; + + if !chan.incoming_requests.remove(&id) { + // The request has been cancelled, no need to send a response. + return Ok(None); + } + + Ok(Some(create_unchecked_response_cancellation(channel, id))) + } + + /// Creates an error message with type [`ErrorKind::Other`]. + /// + /// The resulting [`OutgoingMessage`] is the last message that should be sent to the peer, the + /// caller should ensure no more messages are sent. + /// + /// # Local protocol violations + /// + /// Will return a [`LocalProtocolViolation`] when attempting to send on an invalid channel. + pub fn custom_error( + &mut self, + channel: ChannelId, + id: Id, + payload: Bytes, + ) -> Result { + let header = Header::new_error(header::ErrorKind::Other, channel, id); + + let msg = OutgoingMessage::new(header, Some(payload)); + if msg.is_multi_frame(self.max_frame_size) { + Err(LocalProtocolViolation::ErrorPayloadIsMultiFrame) + } else { + Ok(msg) + } + } + + /// Processes incoming data from a buffer. + /// + /// This is the main ingress function of [`JulietProtocol`]. `buffer` should continuously be + /// appended with all incoming data; the [`Outcome`] returned indicates when the function should + /// be called next: + /// + /// * [`Outcome::Success`] indicates `process_incoming` should be called again as early as + /// possible, since additional messages may already be contained in `buffer`. + /// * [`Outcome::Incomplete`] tells the caller to not call `process_incoming` again before at + /// least `n` additional bytes have been added to buffer. + /// * [`Outcome::Fatal`] indicates that the remote peer violated the protocol, the returned + /// [`Header`] should be attempted to be sent to the peer before the connection is being + /// closed. + /// + /// This method transparently handles multi-frame sends, any incomplete messages will be + /// buffered internally until they are complete. + /// + /// Any successful frame read will cause `buffer` to be advanced by the length of the frame, + /// thus eventually freeing the data if not held elsewhere. + /// + /// **Important**: This functions `Err` value is an [`OutgoingMessage`] to be sent to the peer. + /// It must be the final message sent and should be sent as soon as possible, with the + /// connection being close afterwards. + pub fn process_incoming( + &mut self, + buffer: &mut BytesMut, + ) -> Outcome { + // First, attempt to complete a frame. + loop { + // We do not have enough data to extract a header, indicate and return. + if buffer.len() < Header::SIZE { + return Incomplete(NonZeroU32::new((Header::SIZE - buffer.len()) as u32).unwrap()); + } + + let header_raw: [u8; Header::SIZE] = buffer[0..Header::SIZE].try_into().unwrap(); + let header = match Header::parse(header_raw) { + Some(header) => header, + None => { + // The header was invalid, return an error. + #[cfg(feature = "tracing")] + tracing::debug!(?header_raw, "received invalid header"); + return Fatal(OutgoingMessage::new( + Header::new_error(ErrorKind::InvalidHeader, UNKNOWN_CHANNEL, UNKNOWN_ID), + None, + )); + } + }; + + // We have a valid header, check if it is an error. + if header.is_error() { + match header.error_kind() { + ErrorKind::Other => { + // The error data is varint encoded, but must not exceed a single frame. + let tail = &buffer[Header::SIZE..]; + + // This can be confusing for the other end, receiving an error for their + // error, but they should not send malformed errors in the first place! + let parsed_length = + try_outcome!(decode_varint32(tail).map_err(|_overflow| { + OutgoingMessage::new(header.with_err(ErrorKind::BadVarInt), None) + })); + + // Create indices into buffer. + let preamble_end = + Index::new(buffer, Header::SIZE + parsed_length.offset.get() as usize); + let payload_length = parsed_length.value as usize; + let frame_end = Index::new(buffer, *preamble_end + payload_length); + + // No multi-frame messages allowed! + if *frame_end > self.max_frame_size.get_usize() { + return err_msg(header, ErrorKind::SegmentViolation); + } + + if buffer.len() < *frame_end { + return Outcome::incomplete(*frame_end - buffer.len()); + } + + buffer.advance(*preamble_end); + let payload = buffer.split_to(payload_length).freeze(); + + log_frame!(header, payload); + return Success(CompletedRead::ErrorReceived { + header, + data: Some(payload), + }); + } + _ => { + log_frame!(header); + return Success(CompletedRead::ErrorReceived { header, data: None }); + } + } + } + + // At this point we are guaranteed a valid non-error frame, verify its channel. + let channel = match self.channels.get_mut(header.channel().get() as usize) { + Some(channel) => channel, + None => return err_msg(header, ErrorKind::InvalidChannel), + }; + + match header.kind() { + Kind::Request => { + if channel.is_at_max_incoming_requests() { + return err_msg(header, ErrorKind::RequestLimitExceeded); + } + + if !channel.incoming_requests.insert(header.id()) { + return err_msg(header, ErrorKind::DuplicateRequest); + } + channel.increment_cancellation_allowance(); + + // At this point, we have a valid request and its ID has been added to our + // incoming set. All we need to do now is to remove it from the buffer. + buffer.advance(Header::SIZE); + + log_frame!(header); + return Success(CompletedRead::NewRequest { + channel: header.channel(), + id: header.id(), + payload: None, + }); + } + Kind::Response => { + if !channel.outgoing_requests.remove(&header.id()) { + return err_msg(header, ErrorKind::FictitiousRequest); + } else { + log_frame!(header); + + buffer.advance(Header::SIZE); + return Success(CompletedRead::ReceivedResponse { + channel: header.channel(), + id: header.id(), + payload: None, + }); + } + } + Kind::RequestPl => { + // Make a note whether or not we are continuing an existing request. + let is_new_request = + channel.current_multiframe_receiver.is_new_transfer(header); + + let multiframe_outcome: Option = + try_outcome!(channel.current_multiframe_receiver.accept( + header, + buffer, + self.max_frame_size, + channel.config.max_request_payload_size, + ErrorKind::RequestTooLarge + )); + + // If we made it to this point, we have consumed the frame. Record it. + + if is_new_request { + // Requests must be eagerly (first frame) rejected if exceeding the limit. + if channel.is_at_max_incoming_requests() { + return err_msg(header, ErrorKind::RequestLimitExceeded); + } + + // We also check for duplicate requests early to avoid reading them. + if !channel.incoming_requests.insert(header.id()) { + return err_msg(header, ErrorKind::DuplicateRequest); + } + channel.increment_cancellation_allowance(); + } + + if let Some(payload) = multiframe_outcome { + // Message is complete. + let payload = payload.freeze(); + + return Success(CompletedRead::NewRequest { + channel: header.channel(), + id: header.id(), + payload: Some(payload), + }); + } else { + // We need more frames to complete the payload. Do nothing and attempt + // to read the next frame. + } + } + Kind::ResponsePl => { + let is_new_response = + channel.current_multiframe_receiver.is_new_transfer(header); + + // Ensure it is not a bogus response. + if is_new_response && !channel.outgoing_requests.contains(&header.id()) { + return err_msg(header, ErrorKind::FictitiousRequest); + } + + let multiframe_outcome: Option = + try_outcome!(channel.current_multiframe_receiver.accept( + header, + buffer, + self.max_frame_size, + channel.config.max_response_payload_size, + ErrorKind::ResponseTooLarge + )); + + // If we made it to this point, we have consumed the frame. + if is_new_response && !channel.outgoing_requests.remove(&header.id()) { + return err_msg(header, ErrorKind::FictitiousRequest); + } + + if let Some(payload) = multiframe_outcome { + // Message is complete. + let payload = payload.freeze(); + + return Success(CompletedRead::ReceivedResponse { + channel: header.channel(), + id: header.id(), + payload: Some(payload), + }); + } else { + // We need more frames to complete the payload. Do nothing and attempt + // to read the next frame. + } + } + Kind::CancelReq => { + // Cancellations can be sent multiple times and are not checked to avoid + // cancellation races. For security reasons they are subject to an allowance. + + if channel.cancellation_allowance == 0 { + return err_msg(header, ErrorKind::CancellationLimitExceeded); + } + channel.cancellation_allowance -= 1; + buffer.advance(Header::SIZE); + + #[cfg(feature = "tracing")] + { + tracing::debug!(%header, "received request cancellation"); + } + + // Multi-frame transfers that have not yet been completed are a special case, + // since they have never been reported, we can cancel these internally. + if let Some(in_progress_header) = + channel.current_multiframe_receiver.in_progress_header() + { + // We already know it is a cancellation and we are on the correct channel. + if in_progress_header.id() == header.id() { + // Cancel transfer. + channel.current_multiframe_receiver = MultiframeReceiver::default(); + // Remove tracked request. + channel.incoming_requests.remove(&header.id()); + } + } + + // Check incoming request. If it was already cancelled or answered, ignore, as + // it is valid to send wrong cancellation up to the cancellation allowance. + // + // An incoming request may have also already been answered, which is also + // reason to ignore it. + // + // However, we cannot remove it here, as we need to track whether we have sent + // something back. + if !channel.incoming_requests.contains(&header.id()) { + // Already answered, ignore the late cancellation. + } else { + return Success(CompletedRead::RequestCancellation { + channel: header.channel(), + id: header.id(), + }); + } + } + Kind::CancelResp => { + if channel.outgoing_requests.remove(&header.id()) { + log_frame!(header); + buffer.advance(Header::SIZE); + + return Success(CompletedRead::ResponseCancellation { + channel: header.channel(), + id: header.id(), + }); + } else { + return err_msg(header, ErrorKind::FictitiousCancel); + } + } + } + } + } +} + +/// Turn a header and an [`ErrorKind`] into an outgoing message. +/// +/// Pure convenience function for the common use case of producing a response message from a +/// received header with an appropriate error. +#[inline(always)] +fn err_msg(header: Header, kind: ErrorKind) -> Outcome { + log_frame!(header); + Fatal(OutgoingMessage::new(header.with_err(kind), None)) +} + +/// Determines whether or not a payload with the given size is a multi-frame payload when sent +/// using the provided maximum frame size. +/// +/// # Panics +/// +/// Panics in debug mode if the given payload length is larger than `u32::MAX`. +#[inline] +pub const fn payload_is_multi_frame(max_frame_size: MaxFrameSize, payload_len: usize) -> bool { + debug_assert!( + payload_len <= u32::MAX as usize, + "payload cannot exceed `u32::MAX`" + ); + + payload_len as u64 + Header::SIZE as u64 + (Varint32::encode(payload_len as u32)).len() as u64 + > max_frame_size.get() as u64 +} + +#[cfg(test)] +mod tests { + use std::{collections::HashSet, fmt::Debug, ops::Not}; + + use assert_matches::assert_matches; + use bytes::{Buf, Bytes, BytesMut}; + use proptest_attr_macro::proptest; + use proptest_derive::Arbitrary; + use static_assertions::const_assert; + use strum::{EnumIter, IntoEnumIterator}; + + use crate::{ + header::{ErrorKind, Header, Kind}, + protocol::{ + create_unchecked_response, multiframe::MultiframeReceiver, payload_is_multi_frame, + CompletedRead, LocalProtocolViolation, + }, + varint::Varint32, + ChannelConfiguration, ChannelId, Id, Outcome, + }; + + use super::{ + create_unchecked_request_cancellation, create_unchecked_response_cancellation, err_msg, + Channel, JulietProtocol, MaxFrameSize, OutgoingMessage, ProtocolBuilder, + }; + + /// A generic payload that can be used in testing. + #[derive(Arbitrary, Clone, Copy, Debug, EnumIter)] + enum VaryingPayload { + /// No payload at all. + None, + /// A payload that fits into a single frame (using `TestingSetup`'s defined limits). + SingleFrame, + /// A payload that spans more than one frame. + MultiFrame, + /// A payload that exceeds the request size limit. + TooLarge, + } + + impl VaryingPayload { + /// Returns all valid payload sizes. + fn all_valid() -> impl Iterator { + [ + VaryingPayload::None, + VaryingPayload::SingleFrame, + VaryingPayload::MultiFrame, + ] + .into_iter() + } + + /// Returns whether the resulting payload would be `Option::None`. + fn is_none(self) -> bool { + match self { + VaryingPayload::None => true, + VaryingPayload::SingleFrame => false, + VaryingPayload::MultiFrame => false, + VaryingPayload::TooLarge => false, + } + } + + /// Returns the kind header required if this payload is used in a request. + fn request_kind(self) -> Kind { + if self.is_none() { + Kind::Request + } else { + Kind::RequestPl + } + } + + /// Returns the kind header required if this payload is used in a response. + fn response_kind(self) -> Kind { + if self.is_none() { + Kind::Response + } else { + Kind::ResponsePl + } + } + + /// Produce the actual payload. + fn get(self) -> Option { + self.get_slice().map(Bytes::from_static) + } + + /// Produce the payloads underlying slice. + fn get_slice(self) -> Option<&'static [u8]> { + const SHORT_PAYLOAD: &[u8] = b"asdf"; + const_assert!( + SHORT_PAYLOAD.len() + <= TestingSetup::MAX_FRAME_SIZE as usize - Header::SIZE - Varint32::MAX_LEN + ); + + const LONG_PAYLOAD: &[u8] = + b"large payload large payload large payload large payload large payload large payload"; + const_assert!(LONG_PAYLOAD.len() > TestingSetup::MAX_FRAME_SIZE as usize); + + const OVERLY_LONG_PAYLOAD: &[u8] = b"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh"; + const_assert!(OVERLY_LONG_PAYLOAD.len() > TestingSetup::MAX_PAYLOAD_SIZE as usize); + + match self { + VaryingPayload::None => None, + VaryingPayload::SingleFrame => Some(SHORT_PAYLOAD), + VaryingPayload::MultiFrame => Some(LONG_PAYLOAD), + VaryingPayload::TooLarge => Some(OVERLY_LONG_PAYLOAD), + } + } + } + + #[test] + fn max_frame_size_works() { + let sz = MaxFrameSize::new(1234); + assert_eq!(sz.get(), 1234); + assert_eq!(sz.without_header(), 1230); + + // Smallest allowed: + assert_eq!(MaxFrameSize::MIN, 10); + let small = MaxFrameSize::new(10); + assert_eq!(small.get(), 10); + assert_eq!(small.without_header(), 6); + } + + #[test] + #[should_panic(expected = "permissible minimum for maximum frame size")] + fn max_frame_size_panics_on_too_small_size() { + MaxFrameSize::new(MaxFrameSize::MIN - 1); + } + + #[test] + fn request_id_generation_generates_unique_ids() { + let mut channel = Channel::new(Default::default()); + + // IDs are sequential. + assert_eq!(channel.generate_request_id(), Some(Id::new(1))); + assert_eq!(channel.generate_request_id(), Some(Id::new(2))); + assert_eq!(channel.generate_request_id(), Some(Id::new(3))); + + // Manipulate internal counter, expecting rollover. + channel.prev_request_id = u16::MAX - 2; + assert_eq!(channel.generate_request_id(), Some(Id::new(u16::MAX - 1))); + assert_eq!(channel.generate_request_id(), Some(Id::new(u16::MAX))); + assert_eq!(channel.generate_request_id(), Some(Id::new(0))); + assert_eq!(channel.generate_request_id(), Some(Id::new(1))); + + // Insert some request IDs to mark them as used, causing them to be skipped. + channel.outgoing_requests.extend([1, 2, 3, 5].map(Id::new)); + assert_eq!(channel.generate_request_id(), Some(Id::new(4))); + assert_eq!(channel.generate_request_id(), Some(Id::new(6))); + } + + #[test] + fn allowed_to_send_throttles_when_appropriate() { + // A channel with a request limit of 0 is unusable, but legal. + assert!( + !Channel::new(ChannelConfiguration::new().with_request_limit(0)) + .allowed_to_send_request() + ); + + // Capacity: 1 + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(1)); + assert!(channel.allowed_to_send_request()); + + // Incoming requests should not affect this. + channel.incoming_requests.insert(Id::new(1234)); + channel.incoming_requests.insert(Id::new(5678)); + channel.incoming_requests.insert(Id::new(9010)); + assert!(channel.allowed_to_send_request()); + + // Fill up capacity. + channel.outgoing_requests.insert(Id::new(1)); + assert!(!channel.allowed_to_send_request()); + + // Capacity: 2 + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(2)); + assert!(channel.allowed_to_send_request()); + channel.outgoing_requests.insert(Id::new(1)); + assert!(channel.allowed_to_send_request()); + channel.outgoing_requests.insert(Id::new(2)); + assert!(!channel.allowed_to_send_request()); + } + + #[test] + fn is_at_max_incoming_requests_works() { + // A channel with a request limit of 0 is legal. + assert!( + Channel::new(ChannelConfiguration::new().with_request_limit(0)) + .is_at_max_incoming_requests() + ); + + // Capacity: 1 + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(1)); + assert!(!channel.is_at_max_incoming_requests()); + + // Inserting outgoing requests should not prompt any change to incoming. + channel.outgoing_requests.insert(Id::new(1234)); + channel.outgoing_requests.insert(Id::new(4567)); + assert!(!channel.is_at_max_incoming_requests()); + + channel.incoming_requests.insert(Id::new(1)); + assert!(channel.is_at_max_incoming_requests()); + + // Capacity: 2 + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(2)); + assert!(!channel.is_at_max_incoming_requests()); + channel.incoming_requests.insert(Id::new(1)); + assert!(!channel.is_at_max_incoming_requests()); + channel.incoming_requests.insert(Id::new(2)); + assert!(channel.is_at_max_incoming_requests()); + } + + #[test] + fn cancellation_allowance_incrementation_works() { + // With a 0 request limit, we also don't allow any cancellations. + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(0)); + channel.increment_cancellation_allowance(); + + assert_eq!(channel.cancellation_allowance, 0); + + // Ensure that the cancellation allowance cannot exceed request limit. + let mut channel = Channel::new(ChannelConfiguration::new().with_request_limit(3)); + channel.increment_cancellation_allowance(); + assert_eq!(channel.cancellation_allowance, 1); + channel.increment_cancellation_allowance(); + assert_eq!(channel.cancellation_allowance, 2); + channel.increment_cancellation_allowance(); + assert_eq!(channel.cancellation_allowance, 3); + channel.increment_cancellation_allowance(); + assert_eq!(channel.cancellation_allowance, 3); + channel.increment_cancellation_allowance(); + assert_eq!(channel.cancellation_allowance, 3); + } + + #[test] + fn test_channel_lookups_work() { + let mut protocol: JulietProtocol<3> = ProtocolBuilder::new().build(); + + // We mark channels by inserting an ID into them, that way we can ensure we're not getting + // back the same channel every time. + protocol + .lookup_channel_mut(ChannelId(0)) + .expect("channel missing") + .outgoing_requests + .insert(Id::new(100)); + protocol + .lookup_channel_mut(ChannelId(1)) + .expect("channel missing") + .outgoing_requests + .insert(Id::new(101)); + protocol + .lookup_channel_mut(ChannelId(2)) + .expect("channel missing") + .outgoing_requests + .insert(Id::new(102)); + assert!(matches!( + protocol.lookup_channel_mut(ChannelId(3)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(3))) + )); + assert!(matches!( + protocol.lookup_channel_mut(ChannelId(4)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(4))) + )); + assert!(matches!( + protocol.lookup_channel_mut(ChannelId(255)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(255))) + )); + + // Now look up the channels and ensure they contain the right values + assert_eq!( + protocol + .lookup_channel(ChannelId(0)) + .expect("channel missing") + .outgoing_requests, + HashSet::from([Id::new(100)]) + ); + assert_eq!( + protocol + .lookup_channel(ChannelId(1)) + .expect("channel missing") + .outgoing_requests, + HashSet::from([Id::new(101)]) + ); + assert_eq!( + protocol + .lookup_channel(ChannelId(2)) + .expect("channel missing") + .outgoing_requests, + HashSet::from([Id::new(102)]) + ); + assert!(matches!( + protocol.lookup_channel(ChannelId(3)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(3))) + )); + assert!(matches!( + protocol.lookup_channel(ChannelId(4)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(4))) + )); + assert!(matches!( + protocol.lookup_channel(ChannelId(255)), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(255))) + )); + } + + #[proptest] + fn err_msg_works(header: Header) { + for err_kind in ErrorKind::iter() { + let outcome = err_msg::<()>(header, err_kind); + if let Outcome::Fatal(msg) = outcome { + assert_eq!(msg.header().id(), header.id()); + assert_eq!(msg.header().channel(), header.channel()); + assert!(msg.header().is_error()); + assert_eq!(msg.header().error_kind(), err_kind); + } else { + panic!("expected outcome to be fatal"); + } + } + } + + #[test] + fn multi_frame_estimation_works() { + let max_frame_size = MaxFrameSize::new(512); + + // Note: 512 takes two bytes to encode, so the total overhead is 6 bytes. + + assert!(!payload_is_multi_frame(max_frame_size, 0)); + assert!(!payload_is_multi_frame(max_frame_size, 1)); + assert!(!payload_is_multi_frame(max_frame_size, 5)); + assert!(!payload_is_multi_frame(max_frame_size, 6)); + assert!(!payload_is_multi_frame(max_frame_size, 7)); + assert!(!payload_is_multi_frame(max_frame_size, 505)); + assert!(!payload_is_multi_frame(max_frame_size, 506)); + assert!(payload_is_multi_frame(max_frame_size, 507)); + assert!(payload_is_multi_frame(max_frame_size, 508)); + assert!(payload_is_multi_frame(max_frame_size, u32::MAX as usize)); + } + + #[test] + fn create_requests_with_correct_input_sets_state_accordingly() { + for payload in VaryingPayload::all_valid() { + // Configure a protocol with payload, at least 10 bytes segment size. + let mut protocol = ProtocolBuilder::<5>::with_default_channel_config( + ChannelConfiguration::new() + .with_request_limit(1) + .with_max_request_payload_size(1024), + ) + .max_frame_size(20) + .build(); + + let channel = ChannelId::new(2); + let other_channel = ChannelId::new(0); + + assert!(protocol + .allowed_to_send_request(channel) + .expect("channel should exist")); + + let req = protocol + .create_request(channel, payload.get()) + .expect("should be able to create request"); + + assert_eq!(req.header().channel(), channel); + assert_eq!(req.header().kind(), payload.request_kind()); + + // We expect exactly one id in the outgoing set. + assert_eq!( + protocol + .lookup_channel(channel) + .expect("should have channel") + .outgoing_requests, + [Id::new(1)].into() + ); + + // We've used up the default limit of one. + assert!(!protocol + .allowed_to_send_request(channel) + .expect("channel should exist")); + + // We should still be able to create requests on a different channel. + assert!(protocol + .lookup_channel(other_channel) + .expect("channel 0 should exist") + .outgoing_requests + .is_empty()); + + let other_req = protocol + .create_request(other_channel, payload.get()) + .expect("should be able to create request"); + + assert_eq!(other_req.header().channel(), other_channel); + assert_eq!(other_req.header().kind(), payload.request_kind()); + + // We expect exactly one id in the outgoing set of each channel now. + assert_eq!( + protocol + .lookup_channel(channel) + .expect("should have channel") + .outgoing_requests, + [Id::new(1)].into() + ); + assert_eq!( + protocol + .lookup_channel(other_channel) + .expect("should have channel") + .outgoing_requests, + [Id::new(1)].into() + ); + } + } + + #[test] + fn create_requests_with_invalid_inputs_fails() { + for payload in VaryingPayload::all_valid() { + // Configure a protocol with payload, at least 10 bytes segment size. + let mut protocol = ProtocolBuilder::<2>::with_default_channel_config( + ChannelConfiguration::new() + .with_max_request_payload_size(512) + .with_max_response_payload_size(512), + ) + .build(); + + let channel = ChannelId::new(1); + + // Try an invalid channel, should result in an error. + assert!(matches!( + protocol.create_request(ChannelId::new(2), payload.get()), + Err(LocalProtocolViolation::InvalidChannel(ChannelId(2))) + )); + + assert!(protocol + .allowed_to_send_request(channel) + .expect("channel should exist")); + let _ = protocol + .create_request(channel, payload.get()) + .expect("should be able to create request"); + + assert!(matches!( + protocol.create_request(channel, payload.get()), + Err(LocalProtocolViolation::WouldExceedRequestLimit) + )); + } + } + + #[test] + fn create_response_with_correct_input_clears_state_accordingly() { + for payload in VaryingPayload::all_valid() { + let mut protocol = ProtocolBuilder::<4>::with_default_channel_config( + ChannelConfiguration::new() + .with_max_request_payload_size(512) + .with_max_response_payload_size(512), + ) + .build(); + + let channel = ChannelId::new(3); + + // Inject a channel to have already received two requests. + let req_id = Id::new(9); + let leftover_id = Id::new(77); + protocol + .lookup_channel_mut(channel) + .expect("should find channel") + .incoming_requests + .extend([req_id, leftover_id]); + + // Responding to a non-existent request should not result in a message. + assert!(protocol + .create_response(channel, Id::new(12), payload.get()) + .expect("should allow attempting to respond to non-existent request") + .is_none()); + + // Actual response. + let resp = protocol + .create_response(channel, req_id, payload.get()) + .expect("should allow responding to request") + .expect("should actually answer request"); + + assert_eq!(resp.header().channel(), channel); + assert_eq!(resp.header().id(), req_id); + assert_eq!(resp.header().kind(), payload.response_kind()); + + // Outgoing set should be empty afterwards. + assert_eq!( + protocol + .lookup_channel(channel) + .expect("should find channel") + .incoming_requests, + [leftover_id].into() + ); + } + } + + #[test] + fn custom_errors_are_possible() { + let mut protocol = ProtocolBuilder::<4>::new().build(); + + // The channel ID for custom errors can be arbitrary! + let id = Id::new(12345); + let channel = ChannelId::new(123); + let outgoing = protocol + .custom_error(channel, id, Bytes::new()) + .expect("should be able to send custom error"); + + assert_eq!(outgoing.header().id(), id); + assert_eq!(outgoing.header().channel(), channel); + assert_eq!(outgoing.header().error_kind(), ErrorKind::Other); + } + + /// A simplified setup for testing back and forth between two peers. + #[derive(Clone, Debug)] + struct TestingSetup { + /// Alice's protocol state. + alice: JulietProtocol<{ Self::NUM_CHANNELS as usize }>, + /// Bob's protocol state. + bob: JulietProtocol<{ Self::NUM_CHANNELS as usize }>, + /// The channel communication is sent across for these tests. + common_channel: ChannelId, + /// Maximum frame size in test environment. + max_frame_size: MaxFrameSize, + } + + /// Peer selection. + /// + /// Used to select a target when interacting with the test environment. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + + enum Peer { + /// Alice. + Alice, + /// Bob, aka "not Alice". + Bob, + } + + impl Not for Peer { + type Output = Self; + + fn not(self) -> Self::Output { + match self { + Alice => Bob, + Bob => Alice, + } + } + } + + use Peer::{Alice, Bob}; + + impl TestingSetup { + const MAX_PAYLOAD_SIZE: u32 = 512; + const MAX_FRAME_SIZE: u32 = 20; + const NUM_CHANNELS: u8 = 4; + + /// Instantiates a new testing setup. + fn new() -> Self { + let max_frame_size = MaxFrameSize::new(Self::MAX_FRAME_SIZE); + let pb = ProtocolBuilder::with_default_channel_config( + ChannelConfiguration::new() + .with_request_limit(2) + .with_max_request_payload_size(Self::MAX_PAYLOAD_SIZE) + .with_max_response_payload_size(Self::MAX_PAYLOAD_SIZE), + ) + .max_frame_size(max_frame_size.get()); + let common_channel = ChannelId(Self::NUM_CHANNELS - 1); + + let alice = pb.build(); + let bob = pb.build(); + + TestingSetup { + alice, + bob, + common_channel, + max_frame_size, + } + } + + /// Retrieves a handle to the protocol state of the given peer. + #[inline] + fn get_peer_mut(&mut self, peer: Peer) -> &mut JulietProtocol<4> { + match peer { + Alice => &mut self.alice, + Bob => &mut self.bob, + } + } + + /// Take `msg` and send it to peer `dest`. + /// + /// Will check that the message is fully processed and removed on [`Outcome::Success`]. + fn recv_on( + &mut self, + dest: Peer, + msg: OutgoingMessage, + ) -> Result { + let msg_bytes = msg.to_bytes(self.max_frame_size); + let mut msg_bytes_buffer = BytesMut::from(msg_bytes.as_ref()); + + let orig_self = self.clone(); + + let expected = self + .get_peer_mut(dest) + .process_incoming(&mut msg_bytes_buffer) + .to_result() + .map(|v| { + assert!( + msg_bytes_buffer.is_empty(), + "client should have consumed input" + ); + v + }); + + // Test parsing of partially received data. + // + // This loop runs through almost every sensibly conceivable size of chunks in which data + // can be transmitted and simulates a trickling reception. The original state of the + // receiving facilities is cloned first, and the outcome of the trickle reception is + // compared against the reference of receiving in one go from earlier (`expected`). + for transmission_chunk_size in 1..=(self.max_frame_size.get() as usize * 2 + 1) { + let mut unsent = msg_bytes.clone(); + let mut buffer = BytesMut::new(); + let mut this = orig_self.clone(); + + let result = loop { + // Put more data from unsent into the buffer. + let chunk = unsent.split_to(transmission_chunk_size.min(unsent.remaining())); + buffer.extend(chunk); + + let outcome = this.get_peer_mut(dest).process_incoming(&mut buffer); + + if matches!(outcome, Outcome::Incomplete(_)) { + if unsent.is_empty() { + panic!( + "got incompletion before completion while attempting to send \ + message piecewise in {} byte chunks", + transmission_chunk_size + ); + } + + // Continue reading until complete. + continue; + } + + break outcome.to_result(); + }; + + assert_eq!(result, expected, "should not see difference between trickling reception and single send reception"); + } + + expected + } + + /// Take `msg` and send it to peer `dest`. + /// + /// Will check that the message is fully processed and removed, and a new header read + /// expected next. + fn expect_consumes(&mut self, dest: Peer, msg: OutgoingMessage) { + let mut msg_bytes = BytesMut::from(msg.to_bytes(self.max_frame_size).as_ref()); + + let outcome = self.get_peer_mut(dest).process_incoming(&mut msg_bytes); + + assert!(msg_bytes.is_empty(), "client should have consumed input"); + + assert_matches!(outcome, Outcome::Incomplete(n) if n.get() == 4); + } + + /// Creates a new request on peer `origin`, the sends it to the other peer. + /// + /// Returns the outcome of the other peer's reception. + #[track_caller] + fn create_and_send_request( + &mut self, + origin: Peer, + payload: Option, + ) -> Result { + let channel = self.common_channel; + let msg = self + .get_peer_mut(origin) + .create_request(channel, payload) + .expect("should be able to create request"); + + self.recv_on(!origin, msg) + } + + /// Similar to `create_and_send_request`, but bypasses all checks. + /// + /// Allows for sending requests that are normally not allowed by the protocol API. + #[track_caller] + fn inject_and_send_request( + &mut self, + origin: Peer, + payload: Option, + ) -> Result { + let channel_id = self.common_channel; + let origin_channel = self + .get_peer_mut(origin) + .lookup_channel_mut(channel_id) + .expect("channel does not exist, why?"); + + // Create request, bypassing all checks usually performed by the protocol. + let msg = origin_channel.create_unchecked_request(channel_id, payload); + + // Send to peer and return outcome. + self.recv_on(!origin, msg) + } + + /// Creates a new request cancellation on peer `origin`, the sends it to the other peer. + /// + /// Returns the outcome of the other peer's reception. + #[track_caller] + fn cancel_request_and_send( + &mut self, + origin: Peer, + id: Id, + ) -> Option> { + let channel = self.common_channel; + let msg = self + .get_peer_mut(origin) + .cancel_request(channel, id) + .expect("should be able to create request cancellation")?; + + Some(self.recv_on(!origin, msg)) + } + + /// Creates a new response cancellation on peer `origin`, the sends it to the other peer. + /// + /// Returns the outcome of the other peer's reception. + #[track_caller] + fn cancel_response_and_send( + &mut self, + origin: Peer, + id: Id, + ) -> Option> { + let channel = self.common_channel; + let msg = self + .get_peer_mut(origin) + .cancel_response(channel, id) + .expect("should be able to create response cancellation")?; + + Some(self.recv_on(!origin, msg)) + } + + /// Creates a new response on peer `origin`, the sends it to the other peer. + /// + /// Returns the outcome of the other peer's reception. If no response was scheduled for + /// sending, returns `None`. + #[track_caller] + fn create_and_send_response( + &mut self, + origin: Peer, + id: Id, + payload: Option, + ) -> Option> { + let channel = self.common_channel; + + let msg = self + .get_peer_mut(origin) + .create_response(channel, id, payload) + .expect("should be able to create response")?; + + Some(self.recv_on(!origin, msg)) + } + + /// Similar to `create_and_send_response`, but bypasses all checks. + /// + /// Allows for sending requests that are normally not allowed by the protocol API. + #[track_caller] + fn inject_and_send_response( + &mut self, + origin: Peer, + id: Id, + payload: Option, + ) -> Result { + let channel_id = self.common_channel; + + let msg = create_unchecked_response(channel_id, id, payload); + + // Send to peer and return outcome. + self.recv_on(!origin, msg) + } + + /// Similar to `create_and_send_response_cancellation`, but bypasses all checks. + /// + /// Allows for sending request cancellations that are not allowed by the protocol API. + #[track_caller] + fn inject_and_send_response_cancellation( + &mut self, + origin: Peer, + id: Id, + ) -> Result { + let channel_id = self.common_channel; + + let msg = create_unchecked_response_cancellation(channel_id, id); + + // Send to peer and return outcome. + self.recv_on(!origin, msg) + } + + /// Asserts the given completed read is a [`CompletedRead::NewRequest`] with the given ID + /// and payload. + /// + /// # Panics + /// + /// Will panic if the assertion fails. + #[track_caller] + fn assert_is_new_request( + &self, + expected_id: Id, + expected_payload: Option<&[u8]>, + completed_read: CompletedRead, + ) { + assert_matches!( + completed_read, + CompletedRead::NewRequest { + channel, + id, + payload + } => { + assert_eq!(channel, self.common_channel); + assert_eq!(id, expected_id); + assert_eq!(payload.as_deref(), expected_payload); + } + ); + } + + /// Asserts the given completed read is a [`CompletedRead::RequestCancellation`] with the + /// given ID. + /// + /// # Panics + /// + /// Will panic if the assertion fails. + #[track_caller] + fn assert_is_request_cancellation(&self, expected_id: Id, completed_read: CompletedRead) { + assert_matches!( + completed_read, + CompletedRead::RequestCancellation { + channel, + id, + } => { + assert_eq!(channel, self.common_channel); + assert_eq!(id, expected_id); + } + ); + } + + /// Asserts the given completed read is a [`CompletedRead::ReceivedResponse`] with the given + /// ID and payload. + /// + /// # Panics + /// + /// Will panic if the assertion fails. + #[track_caller] + fn assert_is_received_response( + &self, + expected_id: Id, + expected_payload: Option<&[u8]>, + completed_read: CompletedRead, + ) { + assert_matches!( + completed_read, + CompletedRead::ReceivedResponse { + channel, + id, + payload + } => { + assert_eq!(channel, self.common_channel); + assert_eq!(id, expected_id); + assert_eq!(payload.as_deref(), expected_payload); + } + ); + } + + /// Asserts the given completed read is a [`CompletedRead::ResponseCancellation`] with the + /// given ID. + /// + /// # Panics + /// + /// Will panic if the assertion fails. + #[track_caller] + fn assert_is_response_cancellation(&self, expected_id: Id, completed_read: CompletedRead) { + assert_matches!( + completed_read, + CompletedRead::ResponseCancellation { + channel, + id, + } => { + assert_eq!(channel, self.common_channel); + assert_eq!(id, expected_id); + } + ); + } + + /// Asserts given `Result` is of type `Err` and its message contains a specific header. + /// + /// # Panics + /// + /// Will panic if the assertion fails. + #[track_caller] + fn assert_is_error_message( + &self, + error_kind: ErrorKind, + id: Id, + result: Result, + ) { + let err = result.expect_err("expected an error, got positive outcome instead"); + let header = err.header(); + assert_eq!(header.error_kind(), error_kind); + assert_eq!(header.id(), id); + assert_eq!(header.channel(), self.common_channel); + } + } + + #[test] + fn use_case_req_ok() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + + let expected_id = Id::new(1); + let bob_completed_read = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request"); + env.assert_is_new_request(expected_id, payload.get_slice(), bob_completed_read); + + // Return a response. + let alice_completed_read = env + .create_and_send_response(Bob, expected_id, payload.get()) + .expect("did not expect response to be dropped") + .expect("should not fail to process response on alice"); + env.assert_is_received_response(expected_id, payload.get_slice(), alice_completed_read); + } + } + + // A request followed by a response can take multiple orders, all of which are valid: + + // Alice:Request, Alice:Cancel, Bob:Respond (cancellation ignored) + // Alice:Request, Alice:Cancel, Bob:Cancel (cancellation honored or Bob cancelled) + // Alice:Request, Bob:Respond, Alice:Cancel (cancellation not in time) + // Alice:Request, Bob:Cancel, Alice:Cancel (cancellation acknowledged) + + // Alice's cancellation can also be on the wire at the same time as Bob's responses. + // Alice:Request, Bob:Respond, Alice:CancelSim (cancellation arrives after response) + // Alice:Request, Bob:Cancel, Alice:CancelSim (cancellation arrives after cancellation) + + /// Sets up the environment with Alice's initial request. + fn env_with_initial_areq(payload: VaryingPayload) -> (TestingSetup, Id) { + let mut env = TestingSetup::new(); + + let expected_id = Id::new(1); + + // Alice sends a request first. + let bob_initial_completed_read = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request"); + env.assert_is_new_request(expected_id, payload.get_slice(), bob_initial_completed_read); + + (env, expected_id) + } + + #[test] + fn use_case_areq_acnc_brsp() { + // Alice:Request, Alice:Cancel, Bob:Respond + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + let bob_read_of_cancel = env + .cancel_request_and_send(Alice, id) + .expect("alice should send cancellation") + .expect("bob should produce cancellation"); + env.assert_is_request_cancellation(id, bob_read_of_cancel); + + // Bob's application doesn't notice and sends the response anyway. It should at arrive + // at Alice's to confirm the cancellation. + let alices_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + + env.assert_is_received_response(id, payload.get_slice(), alices_read); + } + } + + #[test] + fn use_case_areq_acnc_bcnc() { + // Alice:Request, Alice:Cancel, Bob:Respond + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + + // Alice directly follows with a cancellation. + let bob_read_of_cancel = env + .cancel_request_and_send(Alice, id) + .expect("alice should send cancellation") + .expect("bob should produce cancellation"); + env.assert_is_request_cancellation(id, bob_read_of_cancel); + + // Bob's application confirms with a response cancellation. + let alices_read = env + .cancel_response_and_send(Bob, id) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + env.assert_is_response_cancellation(id, alices_read); + } + } + + #[test] + fn use_case_areq_brsp_acnc() { + // Alice:Request, Bob:Respond, Alice:Cancel + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + + // Bob's application responds. + let alices_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + env.assert_is_received_response(id, payload.get_slice(), alices_read); + + // Alice's app attempts to send a cancellation, which should be swallowed. + assert!(env.cancel_request_and_send(Alice, id).is_none()); + } + } + + #[test] + fn use_case_areq_bcnc_acnc() { + // Alice:Request, Bob:Respond, Alice:Cancel + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + + // Bob's application answers with a response cancellation. + let alices_read = env + .cancel_response_and_send(Bob, id) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + env.assert_is_response_cancellation(id, alices_read); + + // Alice's app attempts to send a cancellation, which should be swallowed. + assert!(env.cancel_request_and_send(Alice, id).is_none()); + } + } + + #[test] + fn use_case_areq_brsp_acncsim() { + // Alice:Request, Bob:Respond, Alice:CancelSim + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + + // Bob's application responds. + let alices_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + env.assert_is_received_response(id, payload.get_slice(), alices_read); + + // Alice's app attempts to send a cancellation due to a race condition. + env.expect_consumes( + Bob, + create_unchecked_request_cancellation(env.common_channel, id), + ); + } + } + + #[test] + fn use_case_areq_bcnc_acncsim() { + // Alice:Request, Bob:Respond, Alice:CancelSim + for payload in VaryingPayload::all_valid() { + let (mut env, id) = env_with_initial_areq(payload); + + // Bob's application cancels. + let alices_read = env + .cancel_response_and_send(Bob, id) + .expect("bob must send the response") + .expect("bob should be ablet to create the response"); + + env.assert_is_response_cancellation(id, alices_read); + env.expect_consumes( + Bob, + create_unchecked_request_cancellation(env.common_channel, id), + ); + } + } + + #[test] + fn env_req_exceed_in_flight_limit() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + let bob_completed_read_1 = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request 1"); + env.assert_is_new_request(Id::new(1), payload.get_slice(), bob_completed_read_1); + + let bob_completed_read_2 = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request 2"); + env.assert_is_new_request(Id::new(2), payload.get_slice(), bob_completed_read_2); + + // We now need to bypass the local protocol checks to inject a malicious one. + + let local_err_result = env.inject_and_send_request(Alice, payload.get()); + + env.assert_is_error_message( + ErrorKind::RequestLimitExceeded, + Id::new(3), + local_err_result, + ); + } + } + + #[test] + fn env_req_exceed_req_size_limit() { + let payload = VaryingPayload::TooLarge; + + let mut env = TestingSetup::new(); + let bob_result = env.inject_and_send_request(Alice, payload.get()); + + env.assert_is_error_message(ErrorKind::RequestTooLarge, Id::new(1), bob_result); + } + + #[test] + fn env_req_duplicate_request() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + + let bob_completed_read_1 = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request 1"); + env.assert_is_new_request(Id::new(1), payload.get_slice(), bob_completed_read_1); + + // Send a second request with the same ID. For this, we manipulate Alice's internal + // counter and state. + let alice_channel = env + .alice + .lookup_channel_mut(env.common_channel) + .expect("should have channel"); + alice_channel.prev_request_id -= 1; + alice_channel.outgoing_requests.clear(); + + let second_send_result = env.inject_and_send_request(Alice, payload.get()); + env.assert_is_error_message( + ErrorKind::DuplicateRequest, + Id::new(1), + second_send_result, + ); + } + } + + #[test] + fn env_req_response_for_ficticious_request() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + + let bob_completed_read_1 = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request 1"); + env.assert_is_new_request(Id::new(1), payload.get_slice(), bob_completed_read_1); + + // Send a response with a wrong ID. + let second_send_result = env.inject_and_send_response(Bob, Id::new(123), payload.get()); + env.assert_is_error_message( + ErrorKind::FictitiousRequest, + Id::new(123), + second_send_result, + ); + } + } + + #[test] + fn env_req_cancellation_for_ficticious_request() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + + let bob_completed_read_1 = env + .create_and_send_request(Alice, payload.get()) + .expect("bob should accept request 1"); + env.assert_is_new_request(Id::new(1), payload.get_slice(), bob_completed_read_1); + + // Have bob send a response for a request that was never made. + let alice_result = env.inject_and_send_response(Bob, Id::new(123), payload.get()); + env.assert_is_error_message(ErrorKind::FictitiousRequest, Id::new(123), alice_result); + } + } + + #[test] + fn env_req_size_limit_exceeded() { + let mut env = TestingSetup::new(); + + let payload = VaryingPayload::TooLarge; + + // Alice should not allow too-large requests to be sent. + let violation = env + .alice + .create_request(env.common_channel, payload.get()) + .expect_err("should not be able to create too large request"); + + assert_matches!(violation, LocalProtocolViolation::PayloadExceedsLimit); + + // If we force the issue, Bob must refuse it instead. + let bob_result = env.inject_and_send_request(Alice, payload.get()); + env.assert_is_error_message(ErrorKind::RequestTooLarge, Id::new(1), bob_result); + } + + #[test] + fn env_response_size_limit_exceeded() { + let (mut env, id) = env_with_initial_areq(VaryingPayload::None); + let payload = VaryingPayload::TooLarge; + + // Bob should not allow too-large responses to be sent. + let violation = env + .bob + .create_request(env.common_channel, payload.get()) + .expect_err("should not be able to create too large response"); + assert_matches!(violation, LocalProtocolViolation::PayloadExceedsLimit); + + // If we force the issue, Alice must refuse it. + let alice_result = env.inject_and_send_response(Bob, id, payload.get()); + env.assert_is_error_message(ErrorKind::ResponseTooLarge, Id::new(1), alice_result); + } + + #[test] + fn env_req_response_cancellation_limit_exceeded() { + for payload in VaryingPayload::all_valid() { + for num_requests in 0..=2 { + let mut env = TestingSetup::new(); + + // Have Alice make requests in order to fill-up the in-flights. + for i in 0..num_requests { + let expected_id = Id::new(i + 1); + let bobs_read = env + .create_and_send_request(Alice, payload.get()) + .expect("should accept request"); + env.assert_is_new_request(expected_id, payload.get_slice(), bobs_read); + } + + // Now send the corresponding amount of cancellations. + for i in 0..num_requests { + let id = Id::new(i + 1); + + let msg = create_unchecked_request_cancellation(env.common_channel, id); + + let bobs_read = env.recv_on(Bob, msg).expect("cancellation should not fail"); + env.assert_is_request_cancellation(id, bobs_read); + } + + let id = Id::new(num_requests + 1); + // Finally another cancellation should trigger an error. + let msg = create_unchecked_request_cancellation(env.common_channel, id); + + let bobs_result = env.recv_on(Bob, msg); + env.assert_is_error_message(ErrorKind::CancellationLimitExceeded, id, bobs_result); + } + } + } + + #[test] + fn env_max_frame_size_exceeded() { + // Note: An actual `MaxFrameSizeExceeded` can never occur due to how this library is + // implemented. This is the closest situation that can occur. + + let mut env = TestingSetup::new(); + + let payload = VaryingPayload::TooLarge; + let id = Id::new(1); + + // We have to craft the message by hand to exceed the frame size. + let msg = OutgoingMessage::new( + Header::new(Kind::RequestPl, env.common_channel, id), + payload.get(), + ); + let mut encoded = BytesMut::from( + msg.to_bytes(MaxFrameSize::new( + 2 * payload + .get() + .expect("TooLarge payload should have body") + .len() as u32, + )) + .as_ref(), + ); + let violation = env.bob.process_incoming(&mut encoded).to_result(); + + env.assert_is_error_message(ErrorKind::RequestTooLarge, id, violation); + } + + #[test] + fn env_invalid_header() { + for payload in VaryingPayload::all_valid() { + let mut env = TestingSetup::new(); + + let id = Id::new(123); + + // We have to craft the message by hand to exceed the frame size. + let msg = OutgoingMessage::new( + Header::new(Kind::RequestPl, env.common_channel, id), + payload.get(), + ); + let mut encoded = BytesMut::from(msg.to_bytes(env.max_frame_size).as_ref()); + + // Patch the header so that it is broken. + encoded[0] = 0b0000_1111; // Kind: Normal, all data bits set. + + let violation = env + .bob + .process_incoming(&mut encoded) + .to_result() + .expect_err("expected invalid header to produce an error"); + + // We have to manually assert the error, since invalid header errors are sent with an ID + // of 0 and on channel 0. + + let header = violation.header(); + assert_eq!(header.error_kind(), ErrorKind::InvalidHeader); + assert_eq!(header.id(), Id::new(0)); + assert_eq!(header.channel(), ChannelId::new(0)); + } + } + + #[test] + fn env_bad_varint() { + let payload = VaryingPayload::MultiFrame; + let mut env = TestingSetup::new(); + + let id = Id::new(1); + + // We have to craft the message by hand to exceed the frame size. + let msg = OutgoingMessage::new( + Header::new(Kind::RequestPl, env.common_channel, id), + payload.get(), + ); + let mut encoded = BytesMut::from(msg.to_bytes(env.max_frame_size).as_ref()); + + // Invalidate the varint. + encoded[4] = 0xFF; + encoded[5] = 0xFF; + encoded[6] = 0xFF; + encoded[7] = 0xFF; + encoded[8] = 0xFF; + + let violation = env.bob.process_incoming(&mut encoded).to_result(); + + env.assert_is_error_message(ErrorKind::BadVarInt, id, violation); + } + + #[test] + fn response_with_no_payload_is_cleared_from_buffer() { + // This test is fairly specific from a concrete bug. In general, buffer advancement is + // tested in other tests as one of many condition checks. + + let mut protocol: JulietProtocol<16> = ProtocolBuilder::with_default_channel_config( + ChannelConfiguration::new() + .with_max_request_payload_size(4096) + .with_max_response_payload_size(4096), + ) + .build(); + + let channel = ChannelId::new(6); + let id = Id::new(1); + + // Create the request to prime the protocol state machine for the incoming response. + let msg = protocol + .create_request(channel, Some(Bytes::from(&b"foobar"[..]))) + .expect("can create request"); + + assert_eq!(msg.header().channel(), channel); + assert_eq!(msg.header().id(), id); + + let mut response_raw = + BytesMut::from(&Header::new(Kind::Response, channel, id).as_ref()[..]); + + assert_eq!(response_raw.remaining(), 4); + + let outcome = protocol + .process_incoming(&mut response_raw) + .expect("should complete outcome"); + assert_eq!( + outcome, + CompletedRead::ReceivedResponse { + channel, + /// The ID of the request received. + id, + /// The response payload. + payload: None, + } + ); + + assert_eq!(response_raw.remaining(), 0); + } + + #[test] + fn one_respone_or_cancellation_per_request() { + for payload in VaryingPayload::all_valid() { + // Case 1: Response, response. + let (mut env, id) = env_with_initial_areq(payload); + let completed_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("should send response") + .expect("should accept response"); + env.assert_is_received_response(id, payload.get_slice(), completed_read); + + let alice_result = env.inject_and_send_response(Bob, id, payload.get()); + env.assert_is_error_message(ErrorKind::FictitiousRequest, id, alice_result); + + // Case 2: Response, cancel. + let (mut env, id) = env_with_initial_areq(payload); + let completed_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("should send response") + .expect("should accept response"); + env.assert_is_received_response(id, payload.get_slice(), completed_read); + + let alice_result = env.inject_and_send_response_cancellation(Bob, id); + env.assert_is_error_message(ErrorKind::FictitiousCancel, id, alice_result); + + // Case 3: Cancel, response. + let (mut env, id) = env_with_initial_areq(payload); + let completed_read = env + .cancel_response_and_send(Bob, id) + .expect("should send response cancellation") + .expect("should accept response cancellation"); + env.assert_is_response_cancellation(id, completed_read); + + let alice_result = env.inject_and_send_response(Bob, id, payload.get()); + env.assert_is_error_message(ErrorKind::FictitiousRequest, id, alice_result); + + // Case4: Cancel, cancel. + let (mut env, id) = env_with_initial_areq(payload); + let completed_read = env + .create_and_send_response(Bob, id, payload.get()) + .expect("should send response") + .expect("should accept response"); + env.assert_is_received_response(id, payload.get_slice(), completed_read); + + let alice_result = env.inject_and_send_response(Bob, id, payload.get()); + env.assert_is_error_message(ErrorKind::FictitiousRequest, id, alice_result); + } + } + + #[test] + fn multiframe_messages_cancelled_correctly_after_partial_reception() { + // We send a single frame of a multi-frame payload. + let payload = VaryingPayload::MultiFrame; + + let mut env = TestingSetup::new(); + + let expected_id = Id::new(1); + let channel = env.common_channel; + + // Alice sends a multi-frame request. + let alices_multiframe_request = env + .get_peer_mut(Alice) + .create_request(channel, payload.get()) + .expect("should be able to create request"); + let req_header = alices_multiframe_request.header(); + + assert!(alices_multiframe_request.is_multi_frame(env.max_frame_size)); + + let frames = alices_multiframe_request.frames(); + let (frame, _additional_frames) = frames.next_owned(env.max_frame_size); + let mut buffer = BytesMut::from(frame.to_bytes().as_ref()); + + // The outcome of receiving a single frame should be a begun multi-frame read and 4 bytes + // incompletion asking for the next header. + let outcome = env.get_peer_mut(Bob).process_incoming(&mut buffer); + assert_eq!(outcome, Outcome::incomplete(4)); + + let bobs_channel = &env.get_peer_mut(Bob).channels[channel.get() as usize]; + let mut expected = HashSet::new(); + expected.insert(expected_id); + assert_eq!(bobs_channel.incoming_requests, expected); + assert!(matches!( + bobs_channel.current_multiframe_receiver, + MultiframeReceiver::InProgress { + header, + .. + } if header == req_header + )); + + // Now send the cancellation. + let cancellation_frames = env + .get_peer_mut(Alice) + .cancel_request(channel, expected_id) + .expect("alice should be able to create the cancellation") + .expect("should required to send cancellation") + .frames(); + let (cancellation_frame, _additional_frames) = + cancellation_frames.next_owned(env.max_frame_size); + let mut buffer = BytesMut::from(cancellation_frame.to_bytes().as_ref()); + + let bobs_outcome = env.get_peer_mut(Bob).process_incoming(&mut buffer); + + // Processing the cancellation should have no external effect. + assert_eq!(bobs_outcome, Outcome::incomplete(4)); + + // Finally, check if the state is as expected. Since it is an incomplete multi-channel + // message, we must cancel the transfer early. + let bobs_channel = &env.get_peer_mut(Bob).channels[channel.get() as usize]; + + assert!(bobs_channel.incoming_requests.is_empty()); + assert!(matches!( + bobs_channel.current_multiframe_receiver, + MultiframeReceiver::Ready + )); + } +} diff --git a/juliet/src/protocol/multiframe.rs b/juliet/src/protocol/multiframe.rs new file mode 100644 index 0000000000..bf26da1baf --- /dev/null +++ b/juliet/src/protocol/multiframe.rs @@ -0,0 +1,682 @@ +//! Multiframe reading support. +//! +//! The juliet protocol supports multi-frame messages, which are subject to additional rules and +//! checks. The resulting state machine is encoded in the [`MultiframeReceiver`] type. + +use std::mem; + +use bytes::{Buf, BytesMut}; + +use crate::{ + header::{ErrorKind, Header}, + protocol::{ + err_msg, + Outcome::{self, Success}, + }, + try_outcome, + util::Index, + varint::decode_varint32, +}; + +use super::{outgoing_message::OutgoingMessage, MaxFrameSize}; + +/// The multi-frame message receival state of a single channel, as specified in the RFC. +/// +/// The receiver is not channel-aware, that is it will treat a new multi-frame message on a channel +/// that is different from the one where a multi-frame transfer is already in progress as an error +/// in the same way it would if they were on the same channel. The caller thus must ensure to create +/// an instance of `MultiframeReceiver` for every active channel. +#[derive(Debug, Default)] +#[cfg_attr(test, derive(Clone))] +pub(super) enum MultiframeReceiver { + /// The channel is ready to start receiving a new multi-frame message. + #[default] + Ready, + /// A multi-frame message transfer is currently in progress. + InProgress { + /// The header that initiated the multi-frame transfer. + header: Header, + /// Payload data received so far. + payload: BytesMut, + /// The total size of the payload to be received. + total_payload_size: u32, + }, +} + +impl MultiframeReceiver { + /// Attempt to process a single multi-frame message frame. + /// + /// The caller MUST only call this method if it has determined that the frame in `buffer` is one + /// that includes a payload. If this is the case, the entire receive `buffer` should be passed + /// to this function. + /// + /// If a message payload matching the given header has been successfully completed, both header + /// and payload are consumed from the `buffer`, the payload being returned. If a starting or + /// intermediate segment was processed without completing the message, both are still consumed, + /// but `None` is returned instead. This method will never consume more than one frame. + /// + /// On any error, [`Outcome::Fatal`] with a suitable message to return to the sender is + /// returned. + /// + /// `max_payload_size` is the maximum size of a payload across multiple frames. If it is + /// exceeded, the `payload_exceeded_error_kind` function is used to construct an error `Header` + /// to return. + pub(super) fn accept( + &mut self, + header: Header, + buffer: &mut BytesMut, + max_frame_size: MaxFrameSize, + max_payload_size: u32, + payload_exceeded_error_kind: ErrorKind, + ) -> Outcome, OutgoingMessage> { + // TODO: Use tracing to log frames here. + + match self { + MultiframeReceiver::Ready => { + // We know there has to be a starting segment. + let frame_data = try_outcome!(detect_starting_segment( + header, + buffer, + max_frame_size, + max_payload_size, + payload_exceeded_error_kind, + )); + + // At this point we are sure to complete a frame, so drop the preamble. + buffer.advance(frame_data.preamble_len); + + // Consume the segment. + let segment = buffer.split_to(frame_data.segment_len); + + if frame_data.is_complete() { + // No need to alter the state, we stay `Ready`. + Success(Some(segment)) + } else { + // Length exceeds the frame boundary, split to maximum and store that. + *self = MultiframeReceiver::InProgress { + header, + payload: segment, + total_payload_size: frame_data.payload_size, + }; + + // We have successfully consumed a frame, but are not finished yet. + Success(None) + } + } + MultiframeReceiver::InProgress { + header: active_header, + payload, + total_payload_size, + } => { + if header != *active_header { + // The newly supplied header does not match the one active. Let's see if we have + // a valid start frame. + let frame_data = try_outcome!(detect_starting_segment( + header, + buffer, + max_frame_size, + max_payload_size, + payload_exceeded_error_kind, + )); + + if frame_data.is_complete() { + // An interspersed complete frame is fine, consume and return it. + buffer.advance(frame_data.preamble_len); + let segment = buffer.split_to(frame_data.segment_len); + return Success(Some(segment)); + } else { + // Otherwise, `InProgress`, we cannot start a second multiframe transfer. + return err_msg(header, ErrorKind::InProgress); + } + } + + // Determine whether we expect an intermediate or end segment. + let bytes_remaining = *total_payload_size as usize - payload.remaining(); + let max_data_in_frame = max_frame_size.without_header(); + + if bytes_remaining > max_data_in_frame { + // Intermediate segment. + if buffer.remaining() < max_frame_size.get_usize() { + return Outcome::incomplete( + max_frame_size.get_usize() - buffer.remaining(), + ); + } + + // Discard header. + buffer.advance(Header::SIZE); + + // Copy data over to internal buffer. + payload.extend_from_slice(&buffer[0..max_data_in_frame]); + buffer.advance(max_data_in_frame); + + // We're done with this frame (but not the payload). + Success(None) + } else { + // End segment + let frame_end = Index::new(buffer, bytes_remaining + Header::SIZE); + + // If we don't have the entire frame read yet, return. + if *frame_end > buffer.remaining() { + return Outcome::incomplete(*frame_end - buffer.remaining()); + } + + // Discard header. + buffer.advance(Header::SIZE); + + // Copy data over to internal buffer. + payload.extend_from_slice(&buffer[0..bytes_remaining]); + buffer.advance(bytes_remaining); + + let finished_payload = mem::take(payload); + *self = MultiframeReceiver::Ready; + + Success(Some(finished_payload)) + } + } + } + } + + /// Determines whether given `new_header` would be a new transfer if accepted. + /// + /// If `false`, `new_header` would indicate a continuation of an already in-progress transfer. + #[inline] + pub(super) fn is_new_transfer(&self, new_header: Header) -> bool { + match self { + MultiframeReceiver::Ready => true, + MultiframeReceiver::InProgress { header, .. } => *header != new_header, + } + } + + /// Returns the ID of the in-progress transfer. + #[inline] + pub(super) fn in_progress_header(&self) -> Option
{ + match self { + MultiframeReceiver::Ready => None, + MultiframeReceiver::InProgress { header, .. } => Some(*header), + } + } +} + +/// Information about an initial frame in a given buffer. +#[derive(Copy, Clone, Debug)] +struct InitialFrameData { + /// The length of the preamble. + preamble_len: usize, + /// The length of the segment. + segment_len: usize, + /// The total payload size described in the frame preamble. + payload_size: u32, +} + +impl InitialFrameData { + /// Returns whether or not the initial frame data describes a complete initial frame. + #[inline(always)] + fn is_complete(self) -> bool { + self.segment_len >= self.payload_size as usize + } +} + +/// Detects a complete start frame in the given buffer. +/// +/// Assumes that buffer still contains the frames header. Returns (`preamble_size`, `payload_len`). +#[inline(always)] +fn detect_starting_segment( + header: Header, + buffer: &BytesMut, + max_frame_size: MaxFrameSize, + max_payload_size: u32, + payload_exceeded_error_kind: ErrorKind, +) -> Outcome { + // The `segment_buf` is the frame's data without the header. + let segment_buf = &buffer[Header::SIZE..]; + + // Try to decode a payload size. + let payload_size = try_outcome!(decode_varint32(segment_buf).map_err(|_overflow| { + OutgoingMessage::new(header.with_err(ErrorKind::BadVarInt), None) + })); + + if payload_size.value > max_payload_size { + return err_msg(header, payload_exceeded_error_kind); + } + + // We have a valid varint32. + let preamble_len = Header::SIZE + payload_size.offset.get() as usize; + let max_data_in_frame = max_frame_size.get() - preamble_len as u32; + + // Determine how many additional bytes are needed for frame completion. + let segment_len = (max_data_in_frame as usize).min(payload_size.value as usize); + let frame_end = preamble_len + segment_len; + if buffer.remaining() < frame_end { + return Outcome::incomplete(frame_end - buffer.remaining()); + } + + Success(InitialFrameData { + preamble_len, + segment_len, + payload_size: payload_size.value, + }) +} + +#[cfg(test)] +mod tests { + use bytes::{BufMut, Bytes, BytesMut}; + use proptest::{arbitrary::any, collection, proptest}; + use proptest_derive::Arbitrary; + + use crate::{ + header::{ErrorKind, Header, Kind}, + protocol::{FrameIter, MaxFrameSize, OutgoingMessage}, + ChannelId, Id, Outcome, + }; + + use super::MultiframeReceiver; + + /// Frame size used for multiframe tests. + const MAX_FRAME_SIZE: MaxFrameSize = MaxFrameSize::new(16); + + /// Maximum size of a payload of a single frame message. + /// + /// One byte is required to encode the length, which is <= 16. + const MAX_SINGLE_FRAME_PAYLOAD_SIZE: u32 = MAX_FRAME_SIZE.get() - Header::SIZE as u32 - 1; + + /// Maximum payload size used in testing. + const MAX_PAYLOAD_SIZE: u32 = 4096; + + #[test] + fn single_message_frame_by_frame() { + // We single-feed a message frame-by-frame into the multi-frame receiver: + let mut receiver = MultiframeReceiver::default(); + + let payload = gen_payload(64); + let header = Header::new(Kind::RequestPl, ChannelId(1), Id(1)); + + let msg = OutgoingMessage::new(header, Some(Bytes::from(payload.clone()))); + + let mut buffer = BytesMut::new(); + let mut frames_left = msg.num_frames(MAX_FRAME_SIZE); + + for frame in msg.frame_iter(MAX_FRAME_SIZE) { + assert!(frames_left > 0); + frames_left -= 1; + + buffer.put(frame); + + match receiver.accept( + header, + &mut buffer, + MAX_FRAME_SIZE, + MAX_PAYLOAD_SIZE, + ErrorKind::RequestLimitExceeded, + ) { + Outcome::Incomplete(n) => { + assert_eq!(n.get(), 4, "expected multi-frame to ask for header next"); + } + Outcome::Fatal(_) => { + panic!("did not expect fatal error on multi-frame parse") + } + Outcome::Success(Some(output)) => { + assert_eq!(frames_left, 0, "should have consumed all frames"); + assert_eq!(output, payload); + } + Outcome::Success(None) => { + // all good, we will read another frame + } + } + assert!( + buffer.is_empty(), + "multi frame receiver should consume entire frame" + ); + } + } + + /// A testing model action . + #[derive(Arbitrary, derive_more::Debug)] + enum Action { + /// Sends a single frame not subject to multi-frame (due to its payload fitting the size). + #[proptest(weight = 30)] + SendSingleFrame { + /// Header for the single frame. + /// + /// Subject to checking for conflicts with ongoing multi-frame messages. + header: Header, + /// The payload to include. + #[proptest( + strategy = "collection::vec(any::(), 0..=MAX_SINGLE_FRAME_PAYLOAD_SIZE as usize)" + )] + #[debug("{} bytes", payload.len())] + payload: Vec, + }, + /// Creates a new multi-frame message, does nothing if there is already one in progress. + #[proptest(weight = 5)] + BeginMultiFrameMessage { + /// Header for the new multi-frame message. + header: Header, + /// Payload to include. + #[proptest( + strategy = "collection::vec(any::(), (MAX_SINGLE_FRAME_PAYLOAD_SIZE as usize+1)..=MAX_PAYLOAD_SIZE as usize)" + )] + #[debug("{} bytes", payload.len())] + payload: Vec, + }, + /// Continue sending the current multi-frame message; does nothing if no multi-frame send + /// is in progress. + #[proptest(weight = 63)] + Continue, + /// Creates a multi-frame message that conflicts with one already in progress. If there is + /// no transfer in progress, does nothing. + #[proptest(weight = 1)] + SendConflictingMultiFrameMessage { + /// Channel for the conflicting multi-frame message. + /// + /// Will be adjusted if NOT conflicting. + channel: ChannelId, + /// Channel for the conflicting multi-frame message. + /// + /// Will be adjusted if NOT conflicting. + id: Id, + /// Size of the payload to include. + #[proptest( + strategy = "collection::vec(any::(), (MAX_SINGLE_FRAME_PAYLOAD_SIZE as usize+1)..=MAX_PAYLOAD_SIZE as usize)" + )] + #[debug("{} bytes", payload.len())] + payload: Vec, + }, + /// Sends another frame with data. + /// + /// Will be ignored if hitting the last frame of the payload. + #[proptest(weight = 1)] + ContinueWithoutTooSmallFrame, + /// Exceeds the size limit. + #[proptest(weight = 1)] + ExceedPayloadSizeLimit { + /// The header for the new message. + header: Header, + /// How much to reduce the maximum payload size by. + #[proptest(strategy = "collection::vec(any::(), + (MAX_SINGLE_FRAME_PAYLOAD_SIZE as usize + 1) + ..=(2+2*MAX_SINGLE_FRAME_PAYLOAD_SIZE as usize))")] + #[debug("{} bytes", payload.len())] + payload: Vec, + }, + } + + proptest! { + #[test] + #[ignore] // TODO: Adjust parameters so that this does not OOM (or fix leakage bug). + fn model_sequence_test_multi_frame_receiver( + actions in collection::vec(any::(), 0..1000) + ) { + let (input, expected) = generate_model_sequence(actions); + check_model_sequence(input, expected) + } + } + + /// Creates a new header guaranteed to be different from the given header. + fn twiddle_header(header: Header) -> Header { + let new_id = Id::new(header.id().get().wrapping_add(1)); + if header.is_error() { + Header::new_error(header.error_kind(), header.channel(), new_id) + } else { + Header::new(header.kind(), header.channel(), new_id) + } + } + + /// Generates a model sequence and encodes it as input. + /// + /// Returns a [`BytesMut`] buffer filled with a syntactically valid sequence of bytes that + /// decode to multiple frames, along with vector of expected outcomes of the + /// [`MultiframeReceiver::accept`] method. + fn generate_model_sequence( + actions: Vec, + ) -> (BytesMut, Vec, OutgoingMessage>>) { + let mut expected = Vec::new(); + + let mut active_transfer: Option = None; + let mut active_payload = Vec::new(); + let mut input = BytesMut::new(); + + for action in actions { + match action { + Action::SendSingleFrame { + mut header, + payload, + } => { + // Ensure the new message does not clash with an ongoing transfer. + if let Some(ref active_transfer) = active_transfer { + if active_transfer.header() == header { + header = twiddle_header(header); + } + } + + // Sending a standalone frame should yield a message instantly. + let pl = BytesMut::from(payload.as_slice()); + expected.push(Outcome::Success(Some(pl))); + input.put( + OutgoingMessage::new(header, Some(payload.into())) + .iter_bytes(MAX_FRAME_SIZE), + ); + } + Action::BeginMultiFrameMessage { header, payload } => { + if active_transfer.is_some() { + // Do not create conflicts, just ignore. + continue; + } + + // Construct iterator over multi-frame message. + let frames = + OutgoingMessage::new(header, Some(payload.clone().into())).frames(); + active_payload = payload; + + // The first read will be a `None` read. + expected.push(Outcome::Success(None)); + let (frame, more) = frames.next_owned(MAX_FRAME_SIZE); + input.put(frame); + + active_transfer = Some( + more.expect("test generated multi-frame message that only has one frame"), + ); + } + Action::Continue => { + if let Some(frames) = active_transfer.take() { + let (frame, more) = frames.next_owned(MAX_FRAME_SIZE); + + if more.is_some() { + // More frames to come. + expected.push(Outcome::Success(None)); + } else { + let pl = BytesMut::from(active_payload.as_slice()); + expected.push(Outcome::Success(Some(pl))); + } + + input.put(frame); + active_transfer = more; + } + // Otherwise nothing to do - there is no transfer to continue. + } + Action::SendConflictingMultiFrameMessage { + channel, + id, + payload, + } => { + // We need to manually construct a header here, since it must not be an error. + let mut header = Header::new(Kind::Request, channel, id); + if let Some(ref active_transfer) = active_transfer { + // Ensure we don't accidentally hit the same header. + if active_transfer.header() == header { + header = twiddle_header(header); + } + + // We were asked to produce an error, since the protocol was violated. + let msg = OutgoingMessage::new(header, Some(payload.into())); + let (frame, _) = msg.frames().next_owned(MAX_FRAME_SIZE); + input.put(frame); + expected.push(Outcome::Fatal(OutgoingMessage::new( + header.with_err(ErrorKind::InProgress), + None, + ))); + break; // Stop after error. + } else { + // Nothing to do - we cannot conflict with a transfer if there is none. + } + } + Action::ContinueWithoutTooSmallFrame => { + if let Some(ref active_transfer) = active_transfer { + let header = active_transfer.header(); + + // The only guarantee we have is that there is at least one more byte of + // payload, so we send a zero-sized payload. + let msg = OutgoingMessage::new(header, Some(Bytes::new())); + let (frame, _) = msg.frames().next_owned(MAX_FRAME_SIZE); + input.put(frame); + expected.push(Outcome::Fatal(OutgoingMessage::new( + header.with_err(ErrorKind::SegmentViolation), + None, + ))); + break; // Stop after error. + } else { + // Nothing to do, we cannot send a too-small frame if there is no transfer. + } + } + Action::ExceedPayloadSizeLimit { header, payload } => { + if active_transfer.is_some() { + // Only do this if there is no active transfer. + continue; + } + + let msg = OutgoingMessage::new(header, Some(payload.into())); + let (frame, _) = msg.frames().next_owned(MAX_FRAME_SIZE); + input.put(frame); + expected.push(Outcome::Fatal(OutgoingMessage::new( + header.with_err(ErrorKind::RequestTooLarge), + None, + ))); + break; + } + } + } + + (input, expected) + } + + /// Extracts a header from a slice. + /// + /// # Panics + /// + /// Panics if there is no syntactically well-formed header in the first four bytes of `data`. + #[track_caller] + fn expect_header_from_slice(data: &[u8]) -> Header { + let raw_header: [u8; Header::SIZE] = + <[u8; Header::SIZE] as TryFrom<&[u8]>>::try_from(&data[..Header::SIZE]) + .expect("did not expect header to be missing"); + Header::parse(raw_header).expect("did not expect header parsing to fail") + } + + /// Process a given input and compare it against predetermined expected outcomes. + fn check_model_sequence( + mut input: BytesMut, + expected: Vec, OutgoingMessage>>, + ) { + let mut receiver = MultiframeReceiver::default(); + + let mut actual = Vec::new(); + while !input.is_empty() { + // We need to perform the work usually done by the IO system and protocol layer before + // we can pass it on to the multi-frame handler. + let header = expect_header_from_slice(&input); + + let outcome = receiver.accept( + header, + &mut input, + MAX_FRAME_SIZE, + MAX_PAYLOAD_SIZE, + ErrorKind::RequestTooLarge, + ); + actual.push(outcome); + + // On error, we exit. + if matches!(actual.last().unwrap(), Outcome::Fatal(_)) { + break; + } + } + + assert_eq!(actual, expected); + + // Note that `input` may contain residual data here if there was an error, since `accept` + // only consumes the frame if it was valid. + } + + /// Generates a payload. + fn gen_payload(size: usize) -> Vec { + let mut payload = Vec::with_capacity(size); + for i in 0..size { + payload.push((i % 256) as u8); + } + payload + } + + #[test] + fn mutltiframe_allows_interspersed_frames() { + let sf_payload = gen_payload(10); + + let actions = vec![ + Action::BeginMultiFrameMessage { + header: Header::new(Kind::Request, ChannelId(0), Id(0)), + payload: gen_payload(1361), + }, + Action::SendSingleFrame { + header: Header::new_error(ErrorKind::Other, ChannelId(1), Id(42188)), + payload: sf_payload.clone(), + }, + ]; + + // Failed sequence was generated by a proptest, check that it matches. + assert_eq!(format!("{:?}", actions), "[BeginMultiFrameMessage { header: [Request chan: 0 id: 0], payload: 1361 bytes }, SendSingleFrame { header: [err:Other chan: 1 id: 42188], payload: 10 bytes }]"); + + let (input, expected) = generate_model_sequence(actions); + + // We expect the single frame message to come through. + assert_eq!( + expected, + vec![ + Outcome::Success(None), + Outcome::Success(Some(sf_payload.as_slice().into())) + ] + ); + + check_model_sequence(input, expected); + } + + #[test] + fn mutltiframe_does_not_allow_multiple_multiframe_transfers() { + let actions = vec![ + Action::BeginMultiFrameMessage { + header: Header::new(Kind::Request, ChannelId(0), Id(0)), + payload: gen_payload(12), + }, + Action::SendConflictingMultiFrameMessage { + channel: ChannelId(0), + id: Id(1), + payload: gen_payload(106), + }, + ]; + + // Failed sequence was generated by a proptest, check that it matches. + assert_eq!(format!("{:?}", actions), "[BeginMultiFrameMessage { header: [Request chan: 0 id: 0], payload: 12 bytes }, SendConflictingMultiFrameMessage { channel: ChannelId(0), id: Id(1), payload: 106 bytes }]"); + + let (input, expected) = generate_model_sequence(actions); + + // We expect the single frame message to come through. + assert_eq!( + expected, + vec![ + Outcome::Success(None), + Outcome::Fatal(OutgoingMessage::new( + Header::new_error(ErrorKind::InProgress, ChannelId(0), Id(1)), + None + )) + ] + ); + + check_model_sequence(input, expected); + } +} diff --git a/juliet/src/protocol/outgoing_message.rs b/juliet/src/protocol/outgoing_message.rs new file mode 100644 index 0000000000..2804da8795 --- /dev/null +++ b/juliet/src/protocol/outgoing_message.rs @@ -0,0 +1,710 @@ +//! Outgoing message data. +//! +//! The [`protocol`](crate::protocol) module exposes a pure, non-IO state machine for handling the +//! juliet networking protocol, this module contains the necessary output types like +//! [`OutgoingMessage`]. + +use std::{ + fmt::{self, Debug, Display, Formatter, Write}, + io::Cursor, + iter, +}; + +use bytemuck::{Pod, Zeroable}; +use bytes::{buf::Chain, Buf, Bytes}; + +use crate::{header::Header, varint::Varint32}; + +use super::{payload_is_multi_frame, MaxFrameSize}; + +/// A message to be sent to the peer. +/// +/// [`OutgoingMessage`]s are generated when the protocol requires data to be sent to the peer. +/// Unless the connection is terminated, they should not be dropped, but can be sent in any order. +/// +/// A message that spans one or more frames must have its internal frame order preserved. In +/// general, the [`OutgoingMessage::frames()`] iterator should be used, even for single-frame +/// messages. +#[must_use] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OutgoingMessage { + /// The common header for all outgoing messages. + header: Header, + /// The payload, potentially split across multiple messages. + payload: Option, +} + +impl OutgoingMessage { + /// Constructs a new outgoing message. + // Note: Do not make this function available to users of the library, to avoid them constructing + // messages by accident that may violate the protocol. + #[inline(always)] + pub(super) const fn new(header: Header, payload: Option) -> Self { + Self { header, payload } + } + + /// Returns whether or not a message will span multiple frames. + #[inline(always)] + pub const fn is_multi_frame(&self, max_frame_size: MaxFrameSize) -> bool { + if let Some(ref payload) = self.payload { + payload_is_multi_frame(max_frame_size, payload.len()) + } else { + false + } + } + + /// Creates an iterator over all frames in the message. + #[inline(always)] + pub const fn frames(self) -> FrameIter { + FrameIter { + msg: self, + bytes_processed: 0, + } + } + + /// Creates an iterator over all frames in the message with a fixed maximum frame size. + /// + /// A slightly more convenient `frames` method, with a fixed `max_frame_size`. The resulting + /// iterator will use slightly more memory than the equivalent `FrameIter`. + pub fn frame_iter(self, max_frame_size: MaxFrameSize) -> impl Iterator { + let mut frames = Some(self.frames()); + + iter::from_fn(move || { + let iter = frames.take()?; + let (frame, more) = iter.next_owned(max_frame_size); + frames = more; + Some(frame) + }) + } + + /// Returns the outgoing message's header. + #[inline(always)] + pub const fn header(&self) -> Header { + self.header + } + + /// Calculates the total number of bytes that are not header data that will be transmitted with + /// this message (the payload + its variable length encoded length prefix). + #[inline] + pub const fn non_header_len(&self) -> usize { + match self.payload { + Some(ref pl) => Varint32::length_of(pl.len() as u32) + pl.len(), + None => 0, + } + } + + /// Calculates the number of frames this message will produce. + #[inline] + pub const fn num_frames(&self, max_frame_size: MaxFrameSize) -> usize { + let usable_size = max_frame_size.without_header(); + + let num_frames = (self.non_header_len() + usable_size - 1) / usable_size; + if num_frames == 0 { + 1 // `Ord::max` is not `const fn`. + } else { + num_frames + } + } + + /// Calculates the total length in bytes of all frames produced by this message. + #[inline] + pub const fn total_len(&self, max_frame_size: MaxFrameSize) -> usize { + self.num_frames(max_frame_size) * Header::SIZE + self.non_header_len() + } + + /// Creates an byte-iterator over all frames in the message. + /// + /// The returned `ByteIter` will return all frames in sequence using the [`bytes::Buf`] trait, + /// with no regard for frame boundaries, thus it is only suitable to send all frames of the + /// message with no interleaved data. + #[inline] + pub fn iter_bytes(self, max_frame_size: MaxFrameSize) -> ByteIter { + let length_prefix = self + .payload + .as_ref() + .map(|pl| Varint32::encode(pl.len() as u32)) + .unwrap_or(Varint32::SENTINEL); + ByteIter { + msg: self, + length_prefix, + consumed: 0, + max_frame_size, + } + } + + /// Writes out all frames as they should be sent out on the wire into a [`Bytes`] struct. + /// + /// Consider using the `frames()` or `bytes()` methods instead to avoid additional copies. This + /// method is not zero-copy, but still consumes `self` to avoid a conversion of a potentially + /// unshared payload buffer. + #[inline] + pub fn to_bytes(self, max_frame_size: MaxFrameSize) -> Bytes { + let mut everything = self.iter_bytes(max_frame_size); + everything.copy_to_bytes(everything.remaining()) + } +} + +/// Combination of header and potential message payload length. +/// +/// A message with a payload always starts with an initial frame that has a header and a varint +/// encoded payload length. This type combines the two, and allows for the payload length to +/// effectively be omitted (through [`Varint32::SENTINEL`]). It has a compact, constant size memory +/// representation regardless of whether a variably sized integer is present or not. +/// +/// This type implements [`AsRef`], which will return the correctly encoded bytes suitable for +/// sending header and potential varint encoded length. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +struct Preamble { + /// The header, which is always sent. + header: Header, + /// The payload length. If [`Varint32::SENTINEL`], it will always be omitted from output. + payload_length: Varint32, +} + +impl Display for Preamble { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.header, f)?; + if !self.payload_length.is_sentinel() { + write!(f, " [len={}]", self.payload_length.decode())?; + } + Ok(()) + } +} + +impl Preamble { + /// Creates a new preamble. + /// + /// Passing [`Varint32::SENTINEL`] as the length will cause it to be omitted. + #[inline(always)] + const fn new(header: Header, payload_length: Varint32) -> Self { + Self { + header, + payload_length, + } + } + + /// Returns the length of the preamble when encoded as as a bytestring. + #[inline(always)] + const fn len(self) -> usize { + Header::SIZE + self.payload_length.len() + } + + #[inline(always)] + const fn header(self) -> Header { + self.header + } +} + +impl AsRef<[u8]> for Preamble { + #[inline] + fn as_ref(&self) -> &[u8] { + let bytes = bytemuck::bytes_of(self); + &bytes[0..(self.len())] + } +} + +/// Iterator over frames of a message. +// Note: This type can be written just borrowing `msg`, by making it owned, we prevent accidental +// duplicate message sending. Furthermore we allow methods like `into_iter` to be added. +#[derive(Debug)] +#[must_use] +pub struct FrameIter { + /// The outgoing message in its entirety. + msg: OutgoingMessage, + /// Number of bytes output using `OutgoingFrame`s so far. + bytes_processed: usize, +} + +impl FrameIter { + /// Returns the next frame to send. + /// + /// Will return the next frame, and `Some(self)` if there are additional frames to send to + /// complete the message, `None` otherwise. + /// + /// # Note + /// + /// While different [`OutgoingMessage`]s can have their send order mixed or interspersed, a + /// caller MUST NOT send [`OutgoingFrame`]s of a single message in any order but the one + /// produced by this method. In other words, reorder messages, but not frames within a message. + pub fn next_owned(mut self, max_frame_size: MaxFrameSize) -> (OutgoingFrame, Option) { + if let Some(ref payload) = self.msg.payload { + let mut payload_remaining = payload.len() - self.bytes_processed; + + // If this is the first frame, include the message payload length. + let length_prefix = if self.bytes_processed == 0 { + Varint32::encode(payload_remaining as u32) + } else { + Varint32::SENTINEL + }; + + let preamble = Preamble::new(self.msg.header, length_prefix); + + let frame_capacity = max_frame_size.get_usize() - preamble.len(); + let frame_payload_len = frame_capacity.min(payload_remaining); + + let range = self.bytes_processed..(self.bytes_processed + frame_payload_len); + let frame_payload = payload.slice(range); + self.bytes_processed += frame_payload_len; + + // Update payload remaining, now that an additional frame has been produced. + payload_remaining = payload.len() - self.bytes_processed; + + let frame = OutgoingFrame::new_with_payload(preamble, frame_payload); + if payload_remaining > 0 { + (frame, Some(self)) + } else { + (frame, None) + } + } else { + ( + OutgoingFrame::new(Preamble::new(self.msg.header, Varint32::SENTINEL)), + None, + ) + } + } + + /// Returns the outgoing message's header. + #[inline(always)] + pub const fn header(&self) -> Header { + self.msg.header() + } +} + +/// Byte-wise message iterator. +#[derive(Debug)] +pub struct ByteIter { + /// The outgoing message. + msg: OutgoingMessage, + /// A written-out copy of the length prefixed. + /// + /// Handed out by reference. + length_prefix: Varint32, + /// Number of bytes already written/sent. + // Note: The `ByteIter` uses `usize`s, since its primary use is to allow using the `Buf` + // interface, which can only deal with usize arguments anyway. + consumed: usize, + /// Maximum frame size at construction. + max_frame_size: MaxFrameSize, +} + +impl ByteIter { + /// Returns the total number of bytes to be emitted by this [`ByteIter`]. + #[inline(always)] + const fn total(&self) -> usize { + self.msg.total_len(self.max_frame_size) + } +} + +impl Buf for ByteIter { + #[inline(always)] + fn remaining(&self) -> usize { + self.total() - self.consumed + } + + #[inline] + fn chunk(&self) -> &[u8] { + if self.remaining() == 0 { + return &[]; + } + + // Determine where we are. + let frames_completed = self.consumed / self.max_frame_size.get_usize(); + let frame_progress = self.consumed % self.max_frame_size.get_usize(); + let in_first_frame = frames_completed == 0; + + if frame_progress < Header::SIZE { + // Currently sending the header. + return &self.msg.header.as_ref()[frame_progress..]; + } + + debug_assert!(!self.length_prefix.is_sentinel()); + if in_first_frame && frame_progress < (Header::SIZE + self.length_prefix.len()) { + // Currently sending the payload length prefix. + let varint_progress = frame_progress - Header::SIZE; + return &self.length_prefix.as_ref()[varint_progress..]; + } + + // Currently sending a payload chunk. + let space_in_frame = self.max_frame_size.without_header(); + let first_preamble = Header::SIZE + self.length_prefix.len(); + let (frame_payload_start, frame_payload_progress, frame_payload_end) = if in_first_frame { + ( + 0, + frame_progress - first_preamble, + self.max_frame_size.get_usize() - first_preamble, + ) + } else { + let start = frames_completed * space_in_frame - self.length_prefix.len(); + (start, frame_progress - Header::SIZE, start + space_in_frame) + }; + + let current_frame_chunk = self + .msg + .payload + .as_ref() + .map(|pl| &pl[frame_payload_start..frame_payload_end.min(pl.remaining())]) + .unwrap_or_default(); + + ¤t_frame_chunk[frame_payload_progress..] + } + + #[inline(always)] + fn advance(&mut self, cnt: usize) { + self.consumed = (self.consumed + cnt).min(self.total()); + } +} + +/// A single frame to be sent. +/// +/// Implements [`bytes::Buf`], which will yield the bytes to send it across the wire to a peer. +#[derive(Debug)] +#[repr(transparent)] +#[must_use] +pub struct OutgoingFrame(Chain, Bytes>); + +impl Display for OutgoingFrame { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "<{}", self.0.first_ref().get_ref(),)?; + + let payload = self.0.last_ref(); + + if !payload.as_ref().is_empty() { + f.write_char(' ')?; + Display::fmt(&crate::util::PayloadFormat(self.0.last_ref()), f)?; + } + + f.write_str(">") + } +} + +impl OutgoingFrame { + /// Creates a new [`OutgoingFrame`] with no payload. + /// + /// # Panics + /// + /// Panics in debug mode if the [`Preamble`] contains a payload length. + #[inline(always)] + fn new(preamble: Preamble) -> Self { + debug_assert!( + preamble.payload_length.is_sentinel(), + "frame without payload should not have a payload length" + ); + Self::new_with_payload(preamble, Bytes::new()) + } + + /// Creates a new [`OutgoingFrame`] with a payload. + /// + /// # Panics + /// + /// Panics in debug mode if [`Preamble`] does not have a correct payload length, or if the + /// payload exceeds `u32::MAX` in size. + #[inline(always)] + fn new_with_payload(preamble: Preamble, payload: Bytes) -> Self { + debug_assert!( + payload.len() <= u32::MAX as usize, + "payload exceeds maximum allowed payload" + ); + + OutgoingFrame(Cursor::new(preamble).chain(payload)) + } + + /// Returns the outgoing frame's header. + #[inline] + pub fn header(&self) -> Header { + self.0.first_ref().get_ref().header() + } + + /// Writes out the frame. + /// + /// Equivalent to `self.copy_to_bytes(self.remaining)`. + #[inline] + pub fn to_bytes(mut self) -> Bytes { + self.copy_to_bytes(self.remaining()) + } +} + +impl Buf for OutgoingFrame { + #[inline(always)] + fn remaining(&self) -> usize { + self.0.remaining() + } + + #[inline(always)] + fn chunk(&self) -> &[u8] { + self.0.chunk() + } + + #[inline(always)] + fn advance(&mut self, cnt: usize) { + self.0.advance(cnt) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use bytes::{Buf, Bytes}; + + use crate::{ + header::{Header, Kind}, + protocol::MaxFrameSize, + varint::Varint32, + ChannelId, Id, + }; + + use super::{FrameIter, OutgoingMessage, Preamble}; + + /// Maximum frame size used across tests. + const MAX_FRAME_SIZE: MaxFrameSize = MaxFrameSize::new(16); + + /// A reusable sample payload. + const PAYLOAD: &[u8] = &[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99, + ]; + + /// Collects all frames from a single frame iter. + fn collect_frames(mut iter: FrameIter) -> Vec> { + let mut frames = Vec::new(); + loop { + let (mut frame, more) = iter.next_owned(MAX_FRAME_SIZE); + let expanded = frame.copy_to_bytes(frame.remaining()); + frames.push(expanded.into()); + if let Some(more) = more { + iter = more; + } else { + break frames; + } + } + } + + /// Constructs a message with the given length, turns it into frames and compares if the + /// resulting frames are equal to the expected frame sequence. + #[track_caller] + fn check_payload(length: Option, expected: &[&[u8]]) { + assert!( + !expected.is_empty(), + "impossible to have message with no frames" + ); + + let payload = length.map(|l| Bytes::from(&PAYLOAD[..l])); + + let header = Header::new(Kind::RequestPl, ChannelId(0xAB), Id(0xEFCD)); + let msg = OutgoingMessage::new(header, payload); + + assert_eq!(msg.header(), header); + assert_eq!(msg.clone().frames().header(), header); + assert_eq!(expected.len() > 1, msg.is_multi_frame(MAX_FRAME_SIZE)); + assert_eq!(expected.len(), msg.num_frames(MAX_FRAME_SIZE)); + + // Payload data check. + if let Some(length) = length { + assert_eq!( + length + Varint32::length_of(length as u32), + msg.non_header_len() + ); + } else { + assert_eq!(msg.non_header_len(), 0); + } + + // A zero-byte payload is still expected to produce a single byte for the 0-length. + let frames = collect_frames(msg.clone().frames()); + + // Addtional test: Ensure `frame_iter` yields the same result. + let mut from_frame_iter: Vec = Vec::new(); + for frame in msg.clone().frame_iter(MAX_FRAME_SIZE) { + from_frame_iter.extend(frame.to_bytes()); + } + + // We could compare without creating a new vec, but this gives nicer error messages. + let comparable: Vec<_> = frames.iter().map(|v| v.as_slice()).collect(); + assert_eq!(&comparable, expected); + + // Ensure that the written out version is the same as expected. + let expected_bytestring: Vec = + expected.iter().flat_map(Deref::deref).copied().collect(); + assert_eq!(expected_bytestring.len(), msg.total_len(MAX_FRAME_SIZE)); + assert_eq!(from_frame_iter, expected_bytestring); + + let mut bytes_iter = msg.clone().iter_bytes(MAX_FRAME_SIZE); + let written_out = bytes_iter.copy_to_bytes(bytes_iter.remaining()).to_vec(); + assert_eq!(written_out, expected_bytestring); + let converted_to_bytes = msg.clone().to_bytes(MAX_FRAME_SIZE); + assert_eq!(converted_to_bytes, expected_bytestring); + + // Finally, we do a trickle-test with various step sizes. + for step_size in 1..=(MAX_FRAME_SIZE.get_usize() * 2) { + let mut buf: Vec = Vec::new(); + + let mut bytes_iter = msg.clone().iter_bytes(MAX_FRAME_SIZE); + + while bytes_iter.remaining() > 0 { + let chunk = bytes_iter.chunk(); + let next_step = chunk.len().min(step_size); + buf.extend(&chunk[..next_step]); + bytes_iter.advance(next_step); + } + + assert_eq!(buf, expected_bytestring); + } + } + + #[test] + fn message_is_fragmentized_correctly() { + check_payload(None, &[&[0x02, 0xAB, 0xCD, 0xEF]]); + check_payload(Some(0), &[&[0x02, 0xAB, 0xCD, 0xEF, 0]]); + check_payload(Some(1), &[&[0x02, 0xAB, 0xCD, 0xEF, 1, 0]]); + check_payload(Some(5), &[&[0x02, 0xAB, 0xCD, 0xEF, 5, 0, 1, 2, 3, 4]]); + check_payload( + Some(11), + &[&[0x02, 0xAB, 0xCD, 0xEF, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], + ); + check_payload( + Some(12), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[0x02, 0xAB, 0xCD, 0xEF, 11], + ], + ); + check_payload( + Some(13), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[0x02, 0xAB, 0xCD, 0xEF, 11, 12], + ], + ); + check_payload( + Some(23), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 23, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + ], + ], + ); + check_payload( + Some(24), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 24, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + ], + &[0x02, 0xAB, 0xCD, 0xEF, 23], + ], + ); + check_payload( + Some(35), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 35, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + ], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + ], + ], + ); + check_payload( + Some(36), + &[ + &[0x02, 0xAB, 0xCD, 0xEF, 36, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + ], + &[ + 0x02, 0xAB, 0xCD, 0xEF, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + ], + &[0x02, 0xAB, 0xCD, 0xEF, 35], + ], + ); + } + + #[test] + fn bytes_iterator_smoke_test() { + let payload = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11][..]; + + // Expected output: + // &[0x02, 0xAB, 0xCD, 0xEF, 12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + // &[0x02, 0xAB, 0xCD, 0xEF, 11], + + let msg = OutgoingMessage::new( + Header::new(Kind::RequestPl, ChannelId(0xAB), Id(0xEFCD)), + Some(Bytes::from(payload)), + ); + + let mut byte_iter = msg.iter_bytes(MAX_FRAME_SIZE); + + // First header. + assert_eq!(byte_iter.remaining(), 21); + assert_eq!(byte_iter.chunk(), &[0x02, 0xAB, 0xCD, 0xEF]); + assert_eq!(byte_iter.chunk(), &[0x02, 0xAB, 0xCD, 0xEF]); + byte_iter.advance(2); + assert_eq!(byte_iter.remaining(), 19); + assert_eq!(byte_iter.chunk(), &[0xCD, 0xEF]); + byte_iter.advance(2); + assert_eq!(byte_iter.remaining(), 17); + + // Varint encoding length. + assert_eq!(byte_iter.chunk(), &[12]); + byte_iter.advance(1); + assert_eq!(byte_iter.remaining(), 16); + + // Payload of first frame (MAX_FRAME_SIZE - 5 = 11 bytes). + assert_eq!(byte_iter.chunk(), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + byte_iter.advance(1); + assert_eq!(byte_iter.chunk(), &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + byte_iter.advance(5); + assert_eq!(byte_iter.chunk(), &[6, 7, 8, 9, 10]); + byte_iter.advance(5); + + // Second frame. + assert_eq!(byte_iter.remaining(), 5); + assert_eq!(byte_iter.chunk(), &[0x02, 0xAB, 0xCD, 0xEF]); + byte_iter.advance(3); + assert_eq!(byte_iter.chunk(), &[0xEF]); + byte_iter.advance(1); + assert_eq!(byte_iter.remaining(), 1); + assert_eq!(byte_iter.chunk(), &[11]); + byte_iter.advance(1); + assert_eq!(byte_iter.remaining(), 0); + assert_eq!(byte_iter.chunk(), &[0u8; 0]); + assert_eq!(byte_iter.chunk(), &[0u8; 0]); + assert_eq!(byte_iter.chunk(), &[0u8; 0]); + assert_eq!(byte_iter.chunk(), &[0u8; 0]); + assert_eq!(byte_iter.chunk(), &[0u8; 0]); + assert_eq!(byte_iter.remaining(), 0); + assert_eq!(byte_iter.remaining(), 0); + assert_eq!(byte_iter.remaining(), 0); + assert_eq!(byte_iter.remaining(), 0); + } + + #[test] + fn display_works() { + let header = Header::new(Kind::RequestPl, ChannelId(1), Id(2)); + let preamble = Preamble::new(header, Varint32::encode(678)); + + assert_eq!(preamble.to_string(), "[RequestPl chan: 1 id: 2] [len=678]"); + + let preamble_no_payload = Preamble::new(header, Varint32::SENTINEL); + + assert_eq!(preamble_no_payload.to_string(), "[RequestPl chan: 1 id: 2]"); + + let msg = OutgoingMessage::new(header, Some(Bytes::from(&b"asdf"[..]))); + let (frame, _) = msg.frames().next_owned(Default::default()); + + assert_eq!( + frame.to_string(), + "<[RequestPl chan: 1 id: 2] [len=4] 61 73 64 66 (4 bytes)>" + ); + + let msg_no_payload = OutgoingMessage::new(header, None); + let (frame, _) = msg_no_payload.frames().next_owned(Default::default()); + + assert_eq!(frame.to_string(), "<[RequestPl chan: 1 id: 2]>"); + } +} diff --git a/juliet/src/rpc.rs b/juliet/src/rpc.rs new file mode 100644 index 0000000000..4c77dc2348 --- /dev/null +++ b/juliet/src/rpc.rs @@ -0,0 +1,1113 @@ +//! RPC layer. +//! +//! The outermost layer of the `juliet` stack, combines the underlying [`io`](crate::io) and +//! [`protocol`](crate::protocol) layers into a convenient RPC system. +//! +//! The term RPC is used somewhat inaccurately here, as the crate does _not_ deal with the actual +//! method calls or serializing arguments, but only provides the underlying request/response system. +//! +//! ## Usage +//! +//! The RPC system is configured by setting up an [`RpcBuilder`], which in turn requires an +//! [`IoCoreBuilder`] and [`ProtocolBuilder`](crate::protocol::ProtocolBuilder) (see the +//! [`io`](crate::io) and [`protocol`](crate::protocol) module documentation for details), with `N` +//! denoting the number of preconfigured channels. +//! +//! Once a connection has been established, [`RpcBuilder::build`] is used to construct a +//! [`JulietRpcClient`] and [`JulietRpcServer`] pair, the former being used use to make remote +//! procedure calls, while latter is used to answer them. Note that +//! [`JulietRpcServer::next_request`] must continuously be called regardless of whether requests are +//! handled locally, since the function is also responsible for performing the underlying IO. + +use std::{ + cmp::Reverse, + collections::{BinaryHeap, HashMap}, + fmt::{self, Display, Formatter}, + sync::Arc, + time::Duration, +}; + +use bytes::Bytes; + +use once_cell::sync::OnceCell; +use thiserror::Error; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::{ + mpsc::{self, UnboundedReceiver, UnboundedSender}, + Notify, + }, + time::Instant, +}; + +use crate::{ + io::{ + CoreError, EnqueueError, Handle, IoCore, IoCoreBuilder, IoEvent, IoId, RequestHandle, + RequestTicket, ReservationError, + }, + protocol::LocalProtocolViolation, + util::PayloadFormat, + ChannelId, Id, +}; + +/// Builder for a new RPC interface. +pub struct RpcBuilder { + /// The IO core builder used. + core: IoCoreBuilder, +} + +impl RpcBuilder { + /// Constructs a new RPC builder. + /// + /// The builder can be reused to create instances for multiple connections. + pub fn new(core: IoCoreBuilder) -> Self { + RpcBuilder { core } + } + + /// Creates new RPC client and server instances. + pub fn build( + &self, + reader: R, + writer: W, + ) -> (JulietRpcClient, JulietRpcServer) { + let (core, core_handle) = self.core.build(reader, writer); + + let (new_request_sender, new_requests_receiver) = mpsc::unbounded_channel(); + + let client = JulietRpcClient { + new_request_sender, + request_handle: core_handle.clone(), + }; + let server = JulietRpcServer { + core, + handle: core_handle.downgrade(), + pending: Default::default(), + new_requests_receiver, + timeouts: BinaryHeap::new(), + }; + + (client, server) + } +} + +/// Juliet RPC client. +/// +/// The client is used to create new RPC calls through [`JulietRpcClient::create_request`]. +#[derive(Clone, Debug)] +pub struct JulietRpcClient { + new_request_sender: UnboundedSender, + request_handle: RequestHandle, +} + +/// Builder for an outgoing RPC request. +/// +/// Once configured, it can be sent using either +/// [`queue_for_sending`](JulietRpcRequestBuilder::queue_for_sending) or +/// [`try_queue_for_sending`](JulietRpcRequestBuilder::try_queue_for_sending), returning a +/// [`RequestGuard`], which can be used to await the results of the request. +#[derive(Debug)] +pub struct JulietRpcRequestBuilder<'a, const N: usize> { + client: &'a JulietRpcClient, + channel: ChannelId, + payload: Option, + timeout: Option, +} + +/// Juliet RPC Server. +/// +/// The server's purpose is to produce incoming RPC calls and run the underlying IO layer. For this +/// reason it is important to repeatedly call [`next_request`](Self::next_request), see the method +/// documentation for details. +/// +/// ## Shutdown +/// +/// The server will automatically be shutdown if the last [`JulietRpcClient`] is dropped. +#[derive(Debug)] +pub struct JulietRpcServer { + /// The `io` module core used by this server. + core: IoCore, + /// Handle to the `IoCore`, cloned for clients. + handle: Handle, + /// Map of requests that are still pending. + pending: HashMap>, + /// Receiver for request scheduled by `JulietRpcClient`s. + new_requests_receiver: UnboundedReceiver, + /// Heap of pending timeouts. + timeouts: BinaryHeap>, +} + +/// Internal structure representing a new outgoing request. +#[derive(Debug)] +struct NewOutgoingRequest { + /// The already reserved ticket. + ticket: RequestTicket, + /// Request guard to store results. + guard: Arc, + /// Payload of the request. + payload: Option, + /// When the request is supposed to time out. + expires: Option, +} + +impl Display for NewOutgoingRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "NewOutgoingRequest {{ ticket: {}", self.ticket,)?; + if let Some(ref expires) = self.expires { + write!(f, ", expires: {:?}", expires)?; + } + if let Some(ref payload) = self.payload { + write!(f, ", payload: {}", PayloadFormat(payload))?; + } + f.write_str(" }}") + } +} + +#[derive(Debug)] +struct RequestGuardInner { + /// The returned response of the request. + outcome: OnceCell, RequestError>>, + /// A notifier for when the result arrives. + ready: Option, +} + +impl RequestGuardInner { + fn new() -> Self { + RequestGuardInner { + outcome: OnceCell::new(), + ready: Some(Notify::new()), + } + } + + fn set_and_notify(&self, value: Result, RequestError>) { + if self.outcome.set(value).is_ok() { + // If this is the first time the outcome is changed, notify exactly once. + if let Some(ref ready) = self.ready { + ready.notify_one() + } + }; + } +} + +impl JulietRpcClient { + /// Creates a new RPC request builder. + /// + /// The returned builder can be used to create a single request on the given channel. + pub fn create_request(&self, channel: ChannelId) -> JulietRpcRequestBuilder { + JulietRpcRequestBuilder { + client: self, + channel, + payload: None, + timeout: None, + } + } +} + +/// An error produced by the RPC error. +#[derive(Debug, Error)] +pub enum RpcServerError { + /// An [`IoCore`] error. + #[error(transparent)] + CoreError(#[from] CoreError), +} + +impl JulietRpcServer +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + /// Produce the next request from the peer. + /// + /// Runs the underlying IO until another [`IncomingRequest`] has been produced by the remote + /// peer. On success, this function should be called again immediately. + /// + /// On a regular shutdown (`None` returned) or an error ([`RpcServerError`] returned), a caller + /// must stop calling [`next_request`](Self::next_request) and should drop the entire + /// [`JulietRpcServer`]. + /// + /// **Important**: Even if the local peer is not intending to handle any requests, this function + /// must still be called, since it drives the underlying IO system. It is also highly recommend + /// to offload the actual handling of requests to a separate task and return to calling + /// `next_request` as soon as possible. + pub async fn next_request(&mut self) -> Result, RpcServerError> { + loop { + let now = Instant::now(); + + // Process all the timeouts. + let deadline = self.process_timeouts(now); + let timeout_check = tokio::time::sleep_until(deadline); + + tokio::select! { + biased; + + _ = timeout_check => { + // Enough time has elapsed that we need to check for timeouts, which we will + // do the next time we loop. + #[cfg(feature = "tracing")] + tracing::trace!("timeout check"); + } + + opt_new_request = self.new_requests_receiver.recv() => { + #[cfg(feature = "tracing")] + { + if let Some(ref new_request) = opt_new_request { + tracing::debug!(%new_request, "trying to enqueue"); + } + } + if let Some(NewOutgoingRequest { ticket, guard, payload, expires }) = opt_new_request { + match self.handle.enqueue_request(ticket, payload) { + Ok(io_id) => { + // The request will be sent out, store it in our pending map. + self.pending.insert(io_id, guard); + + // If a timeout has been configured, add it to the timeouts map. + if let Some(expires) = expires { + self.timeouts.push(Reverse((expires, io_id))); + } + }, + Err(payload) => { + // Failed to send -- time to shut down. + guard.set_and_notify(Err(RequestError::RemoteClosed(payload))) + } + } + } else { + // The client has been dropped, time for us to shut down as well. + #[cfg(feature = "tracing")] + tracing::info!("last client dropped locally, shutting down"); + + return Ok(None); + } + } + + event_result = self.core.next_event() => { + #[cfg(feature = "tracing")] + { + match event_result { + Err(ref err) => { + if matches!(err, CoreError::LocalProtocolViolation(_)) { + tracing::warn!(%err, "error"); + } else { + tracing::info!(%err, "error"); + } + } + Ok(None) => { + tracing::info!("received remote close"); + } + Ok(Some(ref event)) => { + tracing::debug!(%event, "received"); + } + } + } + if let Some(event) = event_result? { + match event { + IoEvent::NewRequest { + channel, + id, + payload, + } => return Ok(Some(IncomingRequest { + channel, + id, + payload, + handle: Some(self.handle.clone()), + })), + IoEvent::RequestCancelled { .. } => { + // Request cancellation is currently not implemented; there is no + // harm in sending the reply. + }, + IoEvent::ReceivedResponse { io_id, payload } => { + match self.pending.remove(&io_id) { + None => { + // The request has been cancelled on our end, no big deal. + } + Some(guard) => { + guard.set_and_notify(Ok(payload)) + } + } + }, + IoEvent::ReceivedCancellationResponse { io_id } => { + match self.pending.remove(&io_id) { + None => { + // The request has been cancelled on our end, no big deal. + } + Some(guard) => { + guard.set_and_notify(Err(RequestError::RemoteCancelled)) + } + } + }, + } + } else { + return Ok(None) + } + } + }; + } + } + + /// Process all pending timeouts, setting and notifying `RequestError::TimedOut` on timeout. + /// + /// Returns the duration until the next timeout check needs to take place if timeouts are not + /// modified in the interim. + fn process_timeouts(&mut self, now: Instant) -> Instant { + let is_expired = |t: &Reverse<(Instant, IoId)>| t.0 .0 <= now; + + for item in drain_heap_while(&mut self.timeouts, is_expired) { + let (_, io_id) = item.0; + + // If not removed already through other means, set and notify about timeout. + if let Some(guard_ref) = self.pending.remove(&io_id) { + #[cfg(feature = "tracing")] + tracing::debug!(%io_id, "timeout due to response not received in time"); + guard_ref.set_and_notify(Err(RequestError::TimedOut)); + + // We also need to send a cancellation. + if self.handle.enqueue_request_cancellation(io_id).is_err() { + #[cfg(feature = "tracing")] + tracing::debug!(%io_id, "dropping timeout cancellation, remote already closed"); + } + } + } + + // Calculate new delay for timeouts. + if let Some(Reverse((when, _))) = self.timeouts.peek() { + *when + } else { + // 1 hour dummy sleep, since we cannot have a conditional future. + now + Duration::from_secs(3600) + } + } +} + +impl Drop for JulietRpcServer { + fn drop(&mut self) { + // When the server is dropped, ensure all waiting requests are informed. + self.new_requests_receiver.close(); + + for (_io_id, guard) in self.pending.drain() { + guard.set_and_notify(Err(RequestError::Shutdown)); + } + + while let Ok(NewOutgoingRequest { + ticket: _, + guard, + payload, + expires: _, + }) = self.new_requests_receiver.try_recv() + { + guard.set_and_notify(Err(RequestError::RemoteClosed(payload))) + } + } +} + +impl<'a, const N: usize> JulietRpcRequestBuilder<'a, N> { + /// Recovers a payload from the request builder. + pub fn into_payload(self) -> Option { + self.payload + } + + /// Sets the payload for the request. + /// + /// By default, no payload is included. + pub fn with_payload(mut self, payload: Bytes) -> Self { + self.payload = Some(payload); + self + } + + /// Sets the timeout for the request. + /// + /// By default, there is an infinite timeout. + pub const fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Schedules a new request on an outgoing channel. + /// + /// If there is no buffer space available for the request, blocks until there is. + pub async fn queue_for_sending(self) -> RequestGuard { + let ticket = match self + .client + .request_handle + .reserve_request(self.channel) + .await + { + Some(ticket) => ticket, + None => { + // We cannot queue the request, since the connection was closed. + return RequestGuard::new_error(RequestError::RemoteClosed(self.payload)); + } + }; + + self.do_enqueue_request(ticket) + } + + /// Schedules a new request on an outgoing channel if space is available. + /// + /// If no space is available, returns the [`JulietRpcRequestBuilder`] as an `Err` value, so it + /// can be retried later. + pub fn try_queue_for_sending(self) -> Result { + let ticket = match self.client.request_handle.try_reserve_request(self.channel) { + Ok(ticket) => ticket, + Err(ReservationError::Closed) => { + return Ok(RequestGuard::new_error(RequestError::RemoteClosed( + self.payload, + ))); + } + Err(ReservationError::NoBufferSpaceAvailable) => { + return Err(self); + } + }; + + Ok(self.do_enqueue_request(ticket)) + } + + #[inline(always)] + fn do_enqueue_request(self, ticket: RequestTicket) -> RequestGuard { + let inner = Arc::new(RequestGuardInner::new()); + + // If a timeout is set, calculate expiration time. + let expires = if let Some(timeout) = self.timeout { + match Instant::now().checked_add(timeout) { + Some(expires) => Some(expires), + None => { + // The timeout is so high that the resulting `Instant` would overflow. + return RequestGuard::new_error(RequestError::TimeoutOverflow(timeout)); + } + } + } else { + None + }; + + match self.client.new_request_sender.send(NewOutgoingRequest { + ticket, + guard: inner.clone(), + payload: self.payload, + expires, + }) { + Ok(()) => RequestGuard { inner }, + Err(send_err) => { + RequestGuard::new_error(RequestError::RemoteClosed(send_err.0.payload)) + } + } + } +} + +/// An RPC request error. +/// +/// Describes the reason a request did not yield a response. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum RequestError { + /// Remote closed, could not send. + /// + /// The request was never sent out, since the underlying [`IoCore`] was already shut down when + /// it was made. + #[error("remote closed connection before request could be sent")] + RemoteClosed(Option), + /// Sent, but never received a reply. + /// + /// Request was sent, but we never received anything back before the [`IoCore`] was shut down. + #[error("never received reply before remote closed connection")] + Shutdown, + /// Local timeout. + /// + /// The request was cancelled on our end due to a timeout. + #[error("request timed out")] + TimedOut, + /// Local timeout overflow. + /// + /// The given timeout would cause a clock overflow. + #[error("requested timeout ({0:?}) would cause clock overflow")] + TimeoutOverflow(Duration), + /// Remote responded with cancellation. + /// + /// Instead of sending a response, the remote sent a cancellation. + #[error("remote cancelled our request")] + RemoteCancelled, + /// Cancelled locally. + /// + /// Request was cancelled on our end. + #[error("request cancelled locally")] + Cancelled, + /// API misuse. + /// + /// Either the API was misused, or a bug in this crate appeared. + #[error("API misused or other internal error")] + Error(LocalProtocolViolation), +} + +/// Handle to an in-flight outgoing request. +/// +/// The existence of a [`RequestGuard`] indicates that a request has been made or is ongoing. It +/// can also be used to attempt to [`cancel`](RequestGuard::cancel) the request, or retrieve its +/// values using [`wait_for_response`](RequestGuard::wait_for_response) or +/// [`try_get_response`](RequestGuard::try_get_response). +#[derive(Debug)] +#[must_use = "dropping the request guard will immediately cancel the request"] +pub struct RequestGuard { + /// Shared reference to outcome data. + inner: Arc, +} + +impl RequestGuard { + /// Creates a new request guard with no shared data that is already resolved to an error. + fn new_error(error: RequestError) -> Self { + let outcome = OnceCell::new(); + outcome + .set(Err(error)) + .expect("newly constructed cell should always be empty"); + RequestGuard { + inner: Arc::new(RequestGuardInner { + outcome, + ready: None, + }), + } + } + + /// Cancels the request. + /// + /// May cause the request to not be sent if it is still in the queue, or a cancellation to be + /// sent if it already left the local machine. + pub fn cancel(mut self) { + self.do_cancel(); + + self.forget() + } + + fn do_cancel(&mut self) { + // TODO: Implement eager cancellation locally, potentially removing this request from the + // outbound queue. + // TODO: Implement actual sending of the cancellation. + } + + /// Forgets the request was made. + /// + /// Similar to [`cancel`](Self::cancel), except that it will not cause an actual cancellation, + /// so the peer will likely perform all the work. The response will be discarded. + pub fn forget(self) { + // Just do nothing. + } + + /// Waits for a response to come back. + /// + /// Blocks until a response, cancellation or error has been received for this particular + /// request. + /// + /// If a response has been received, the optional [`Bytes`] of the payload will be returned. + /// + /// On an error, including a cancellation by the remote, returns a [`RequestError`]. + pub async fn wait_for_response(self) -> Result, RequestError> { + // Wait for notification. + if let Some(ref ready) = self.inner.ready { + ready.notified().await; + } + + self.take_inner() + } + + /// Waits for the response, non-blockingly. + /// + /// Like [`wait_for_response`](Self::wait_for_response), except that instead of waiting, it will + /// return `Err(self)` if the peer was not ready yet. + pub fn try_get_response(self) -> Result, RequestError>, Self> { + if self.inner.outcome.get().is_some() { + Ok(self.take_inner()) + } else { + Err(self) + } + } + + fn take_inner(self) -> Result, RequestError> { + // TODO: Best to move `Notified` + `OnceCell` into a separate struct for testing and + // upholding these invariants, avoiding the extra clones. + + self.inner + .outcome + .get() + .expect("should not have called notified without setting cell contents") + .clone() + } +} + +impl Drop for RequestGuard { + fn drop(&mut self) { + self.do_cancel(); + } +} + +/// An incoming request from a peer. +/// +/// Every request should be answered using either the [`IncomingRequest::cancel()`] or +/// [`IncomingRequest::respond()`] methods. +/// +/// ## Automatic cleanup +/// +/// If dropped, [`IncomingRequest::cancel()`] is called automatically, which will cause a +/// cancellation to be sent. +#[derive(Debug)] +#[must_use] +pub struct IncomingRequest { + /// Channel the request was sent on. + channel: ChannelId, + /// Id chosen by peer for the request. + id: Id, + /// Payload attached to request. + payload: Option, + /// Handle to [`IoCore`] to send a reply. + handle: Option, +} + +impl Display for IncomingRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "IncomingRequest {{ channel: {}, id: {}, payload: ", + self.channel, self.id + )?; + + if let Some(ref payload) = self.payload { + write!(f, "{} bytes }}", payload.len()) + } else { + f.write_str("none>") + } + } +} + +impl IncomingRequest { + /// Returns the [`ChannelId`] of the channel the request arrived on. + #[inline(always)] + pub const fn channel(&self) -> ChannelId { + self.channel + } + + /// Returns the [`Id`] of the request. + #[inline(always)] + pub const fn id(&self) -> Id { + self.id + } + + /// Returns a reference to the payload, if any. + #[inline(always)] + pub const fn payload(&self) -> &Option { + &self.payload + } + + /// Returns a mutable reference to the payload, if any. + /// + /// Typically used in conjunction with [`Option::take()`]. + #[inline(always)] + pub fn payload_mut(&mut self) -> &mut Option { + &mut self.payload + } + + /// Enqueue a response to be sent out. + /// + /// The response will contain the specified `payload`, sent on a best effort basis. Responses + /// will never be rejected on a basis of memory. + #[inline] + pub fn respond(mut self, payload: Option) { + if let Some(handle) = self.handle.take() { + if let Err(err) = handle.enqueue_response(self.channel, self.id, payload) { + match err { + EnqueueError::Closed(_) => { + // Do nothing, just discard the response. + } + EnqueueError::BufferLimitHit(_) => { + // TODO: Add seperate type to avoid this. + unreachable!("cannot hit request limit when responding") + } + } + } + } + } + + /// Cancel the request. + /// + /// This will cause a cancellation to be sent back. + #[inline(always)] + pub fn cancel(mut self) { + self.do_cancel(); + } + + fn do_cancel(&mut self) { + if let Some(handle) = self.handle.take() { + if let Err(err) = handle.enqueue_response_cancellation(self.channel, self.id) { + match err { + EnqueueError::Closed(_) => { + // Do nothing, just discard the response. + } + EnqueueError::BufferLimitHit(_) => { + unreachable!("cannot hit request limit when responding") + } + } + } + } + } +} + +impl Drop for IncomingRequest { + #[inline(always)] + fn drop(&mut self) { + self.do_cancel(); + } +} + +/// An iterator draining items out of a heap based on a predicate. +/// +/// See [`drain_heap_while`] for details. +struct DrainConditional<'a, T, F> { + /// Heap to be drained. + heap: &'a mut BinaryHeap, + /// Predicate function to determine whether or not to drain a specific element. + predicate: F, +} + +/// Removes items from the top of a heap while a given predicate is true. +fn drain_heap_while bool>( + heap: &mut BinaryHeap, + predicate: F, +) -> DrainConditional<'_, T, F> { + DrainConditional { heap, predicate } +} + +impl<'a, T, F> Iterator for DrainConditional<'a, T, F> +where + F: FnMut(&T) -> bool, + T: Ord + PartialOrd + 'static, +{ + type Item = T; + + #[inline] + fn next(&mut self) -> Option { + let candidate = self.heap.peek()?; + if (self.predicate)(candidate) { + Some( + self.heap + .pop() + .expect("did not expect heap top to disappear"), + ) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use std::{collections::BinaryHeap, sync::Arc, time::Duration}; + + use bytes::Bytes; + use futures::FutureExt; + use tokio::io::{DuplexStream, ReadHalf, WriteHalf}; + use tracing::{span, Instrument, Level}; + + use crate::{ + io::IoCoreBuilder, + protocol::ProtocolBuilder, + rpc::{RequestError, RpcBuilder}, + ChannelConfiguration, ChannelId, + }; + + use super::{ + drain_heap_while, JulietRpcClient, JulietRpcServer, RequestGuard, RequestGuardInner, + }; + + #[allow(clippy::type_complexity)] // We'll allow it in testing. + fn setup_peers( + builder: RpcBuilder, + ) -> ( + ( + JulietRpcClient, + JulietRpcServer, WriteHalf>, + ), + ( + JulietRpcClient, + JulietRpcServer, WriteHalf>, + ), + ) { + let (peer_a_pipe, peer_b_pipe) = tokio::io::duplex(64); + let peer_a = { + let (reader, writer) = tokio::io::split(peer_a_pipe); + builder.build(reader, writer) + }; + let peer_b = { + let (reader, writer) = tokio::io::split(peer_b_pipe); + builder.build(reader, writer) + }; + (peer_a, peer_b) + } + + // It takes about 12 ms one-way for sound from the base of the Matterhorn to reach the summit, + // so we expect a single yodel to echo within ~ 24 ms, which is use as a reference here. + const ECHO_DELAY: Duration = Duration::from_millis(2 * 12); + + /// Runs an echo server in the background. + /// + /// The server keeps running as long as the future is polled. + async fn run_echo_server( + server: ( + JulietRpcClient, + JulietRpcServer, WriteHalf>, + ), + ) { + let (rpc_client, mut rpc_server) = server; + + while let Some(req) = rpc_server + .next_request() + .await + .expect("error receiving request") + { + let payload = req.payload().clone(); + + tokio::time::sleep(ECHO_DELAY).await; + req.respond(payload); + } + + drop(rpc_client); + } + + /// Runs the necessary server functionality for the RPC client. + async fn run_echo_client( + mut rpc_server: JulietRpcServer, WriteHalf>, + ) { + while let Some(inc) = rpc_server + .next_request() + .await + .expect("client rpc_server error") + { + panic!("did not expect to receive {:?} on client", inc); + } + } + + /// Creates a channel configuration with test defaults. + fn create_config() -> ChannelConfiguration { + ChannelConfiguration::new() + .with_max_request_payload_size(1024) + .with_max_response_payload_size(1024) + .with_request_limit(1) + } + + /// Completely sets up an environment with a running echo server, returning a client. + fn create_rpc_echo_server_env(channel_config: ChannelConfiguration) -> JulietRpcClient<2> { + // Setup logging if not already set up. + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init() + .ok(); // If setting up logging fails, another testing thread already initialized it. + + let builder = RpcBuilder::new(IoCoreBuilder::new( + ProtocolBuilder::<2>::with_default_channel_config(channel_config), + )); + + let (client, server) = setup_peers(builder); + + // Spawn the server. + tokio::spawn(run_echo_server(server).instrument(span!(Level::ERROR, "server"))); + + let (rpc_client, rpc_server) = client; + + // Run the background process for the client. + tokio::spawn(run_echo_client(rpc_server).instrument(span!(Level::ERROR, "client"))); + + rpc_client + } + + #[tokio::test] + async fn basic_smoke_test() { + let rpc_client = create_rpc_echo_server_env(create_config()); + + let payload = Bytes::from(&b"foobar"[..]); + + let response = rpc_client + .create_request(ChannelId::new(0)) + .with_payload(payload.clone()) + .queue_for_sending() + .await + .wait_for_response() + .await + .expect("request failed"); + + assert_eq!(response, Some(payload.clone())); + + // Create a second request with a timeout. + let response_err = rpc_client + .create_request(ChannelId::new(0)) + .with_payload(payload.clone()) + .with_timeout(ECHO_DELAY / 2) + .queue_for_sending() + .await + .wait_for_response() + .await; + assert_eq!(response_err, Err(crate::rpc::RequestError::TimedOut)); + } + + #[tokio::test] + async fn timeout_processed_in_correct_order() { + // It's important to set a request limit higher than 1, so that both requests can be sent at + // the same time. + let rpc_client = create_rpc_echo_server_env(create_config().with_request_limit(3)); + + let payload_short = Bytes::from(&b"timeout check short"[..]); + let payload_long = Bytes::from(&b"timeout check long"[..]); + + // Sending two requests with different timeouts will result in both being added to the heap + // of timeouts to check. If the internal heap is in the wrong order, the bigger timeout will + // prevent the smaller one from being processed. + + let req_short = rpc_client + .create_request(ChannelId::new(0)) + .with_payload(payload_short) + .with_timeout(ECHO_DELAY / 2) + .queue_for_sending() + .await; + + let req_long = rpc_client + .create_request(ChannelId::new(0)) + .with_payload(payload_long.clone()) + .with_timeout(ECHO_DELAY * 100) + .queue_for_sending() + .await; + + let result_short = req_short.wait_for_response().await; + let result_long = req_long.wait_for_response().await; + + assert_eq!(result_short, Err(RequestError::TimedOut)); + assert_eq!(result_long, Ok(Some(payload_long))); + + // TODO: Ensure cancellation was sent. Right now, we can verify this in the logs, but it + // would be nice to have a test tailored to ensure this. + } + + #[test] + fn request_guard_polls_waiting_with_no_response() { + let inner = Arc::new(RequestGuardInner::new()); + let guard = RequestGuard { inner }; + + // Initially, the guard should not have a response. + let guard = guard + .try_get_response() + .expect_err("should not have a result"); + + // Polling it should also result in a wait. + let waiting = guard.wait_for_response(); + + assert!(waiting.now_or_never().is_none()); + } + + #[test] + fn request_guard_polled_early_returns_response_when_available() { + let inner = Arc::new(RequestGuardInner::new()); + let guard = RequestGuard { + inner: inner.clone(), + }; + + // Waiter created before response sent. + let waiting = guard.wait_for_response(); + inner.set_and_notify(Ok(None)); + + assert_eq!(waiting.now_or_never().expect("should poll ready"), Ok(None)); + } + + #[test] + fn request_guard_polled_late_returns_response_when_available() { + let inner = Arc::new(RequestGuardInner::new()); + let guard = RequestGuard { + inner: inner.clone(), + }; + + inner.set_and_notify(Ok(None)); + + // Waiter created after response sent. + let waiting = guard.wait_for_response(); + + assert_eq!(waiting.now_or_never().expect("should poll ready"), Ok(None)); + } + + #[test] + fn request_guard_get_returns_correct_value_when_available() { + let inner = Arc::new(RequestGuardInner::new()); + let guard = RequestGuard { + inner: inner.clone(), + }; + + // Waiter created and polled before notification. + let guard = guard + .try_get_response() + .expect_err("should not have a result"); + + let payload_str = b"hello, world"; + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str)))); + + assert_eq!( + guard.try_get_response().expect("should be ready"), + Ok(Some(Bytes::from_static(payload_str))) + ); + } + + #[test] + fn request_guard_harmless_to_set_multiple_times() { + // We want first write wins semantics here. + let inner = Arc::new(RequestGuardInner::new()); + let guard = RequestGuard { + inner: inner.clone(), + }; + + let payload_str = b"hello, world"; + let payload_str2 = b"goodbye, world"; + + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + + assert_eq!( + guard.try_get_response().expect("should be ready"), + Ok(Some(Bytes::from_static(payload_str))) + ); + + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + inner.set_and_notify(Ok(Some(Bytes::from_static(payload_str2)))); + } + + #[test] + fn drain_works() { + let mut heap = BinaryHeap::new(); + + heap.push(5); + heap.push(3); + heap.push(2); + heap.push(7); + heap.push(11); + heap.push(13); + + assert!(drain_heap_while(&mut heap, |_| false).next().is_none()); + assert!(drain_heap_while(&mut heap, |&v| v > 14).next().is_none()); + + assert_eq!( + drain_heap_while(&mut heap, |&v| v > 10).collect::>(), + vec![13, 11] + ); + + assert_eq!( + drain_heap_while(&mut heap, |&v| v > 10).collect::>(), + Vec::::new() + ); + + assert_eq!( + drain_heap_while(&mut heap, |&v| v > 2).collect::>(), + vec![7, 5, 3] + ); + + assert_eq!( + drain_heap_while(&mut heap, |_| true).collect::>(), + vec![2] + ); + } + + #[test] + fn drain_on_empty_works() { + let mut empty_heap = BinaryHeap::::new(); + + assert!(drain_heap_while(&mut empty_heap, |_| true).next().is_none()); + } +} diff --git a/juliet/src/util.rs b/juliet/src/util.rs new file mode 100644 index 0000000000..4665f1140f --- /dev/null +++ b/juliet/src/util.rs @@ -0,0 +1,96 @@ +//! Miscellaneous utilities used across multiple modules. + +use std::{ + fmt::{self, Display, Formatter}, + marker::PhantomData, + ops::Deref, +}; + +use bytes::{Bytes, BytesMut}; + +/// Bytes offset with a lifetime. +/// +/// Helper type that ensures that offsets that are depending on a buffer are not being invalidated +/// through accidental modification. +pub(crate) struct Index<'a> { + /// The byte offset this `Index` represents. + index: usize, + /// Buffer it is tied to. + buffer: PhantomData<&'a BytesMut>, +} + +impl<'a> Deref for Index<'a> { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.index + } +} + +impl<'a> Index<'a> { + /// Creates a new `Index` with offset value `index`, borrowing `buffer`. + pub(crate) const fn new(buffer: &'a BytesMut, index: usize) -> Self { + let _ = buffer; + Index { + index, + buffer: PhantomData, + } + } +} + +/// Pretty prints a single payload. +pub(crate) struct PayloadFormat<'a>(pub &'a Bytes); + +impl<'a> Display for PayloadFormat<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let raw = self.0.as_ref(); + + for &byte in &raw[0..raw.len().min(16)] { + write!(f, "{:02x} ", byte)?; + } + + if raw.len() > 16 { + f.write_str("... ")?; + } + + write!(f, "({} bytes)", raw.len())?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use bytes::{Bytes, BytesMut}; + use proptest_attr_macro::proptest; + + use crate::util::PayloadFormat; + + use super::Index; + + #[proptest] + fn index_derefs_correctly(idx: usize) { + let buffer = BytesMut::new(); + let index = Index::new(&buffer, idx); + + assert_eq!(*index, idx); + } + + #[test] + fn payload_formatting_works() { + let payload_small = Bytes::from_static(b"hello"); + assert_eq!( + PayloadFormat(&payload_small).to_string(), + "68 65 6c 6c 6f (5 bytes)" + ); + + let payload_large = Bytes::from_static(b"goodbye, cruel world"); + assert_eq!( + PayloadFormat(&payload_large).to_string(), + "67 6f 6f 64 62 79 65 2c 20 63 72 75 65 6c 20 77 ... (20 bytes)" + ); + + let payload_empty = Bytes::from_static(b""); + assert_eq!(PayloadFormat(&payload_empty).to_string(), "(0 bytes)"); + } +} diff --git a/juliet/src/varint.rs b/juliet/src/varint.rs new file mode 100644 index 0000000000..8832d70f14 --- /dev/null +++ b/juliet/src/varint.rs @@ -0,0 +1,315 @@ +//! Variable length integer encoding. +//! +//! This module implements the variable length encoding of 32 bit integers, as described in the +//! juliet RFC, which is 1-5 bytes in length for any `u32`. + +use std::{ + fmt::Debug, + num::{NonZeroU32, NonZeroU8}, +}; + +use bytemuck::{Pod, Zeroable}; + +use crate::Outcome::{self, Fatal, Incomplete, Success}; + +/// The bitmask to separate the data-follows bit from actual value bits. +const VARINT_MASK: u8 = 0b0111_1111; + +/// The only possible error for a varint32 parsing, value overflow. +#[derive(Clone, Copy, Debug)] +pub struct Overflow; + +/// A successful parse of a varint32. +/// +/// Contains both the decoded value and the bytes consumed. +pub struct ParsedU32 { + /// The number of bytes consumed by the varint32. + // Note: The `NonZeroU8` allows for niche optimization of compound types containing this type. + pub offset: NonZeroU8, + /// The actual parsed value. + pub value: u32, +} + +/// Decodes a varint32 from the given input. +pub const fn decode_varint32(input: &[u8]) -> Outcome { + let mut value = 0u32; + + // `for` is not stable in `const fn` yet. + let mut idx = 0; + while idx < input.len() { + let c = input[idx]; + if idx >= 4 && c & 0b1111_0000 != 0 { + return Fatal(Overflow); + } + + value |= ((c & VARINT_MASK) as u32) << (idx * 7); + + if c & !VARINT_MASK == 0 { + return Success(ParsedU32 { + value, + offset: unsafe { NonZeroU8::new_unchecked((idx + 1) as u8) }, + }); + } + + idx += 1; + } + + // We found no stop bit, so our integer is incomplete. + Incomplete(unsafe { NonZeroU32::new_unchecked(1) }) +} + +/// An encoded varint32. +/// +/// Internally these are stored as six byte arrays to make passing around convenient. Since the +/// maximum length a 32 bit varint can posses is 5 bytes, the 6th byte is used to record the +/// length. +#[repr(transparent)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct Varint32([u8; 6]); + +impl Debug for Varint32 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + v if v.is_sentinel() => f.write_str("Varint32::SENTINEL"), + _ => f.debug_tuple("Varint32").field(&self.0).finish(), + } + } +} + +impl Varint32 { + /// `Varint32` sentinel. + /// + /// This value will never be parsed or generated by any encoded `u32`. It allows using a + /// `Varint32` as an inlined `Option`. The return value of `Varint32::len()` of the + /// `SENTINEL` is guaranteed to be `0`. + pub const SENTINEL: Varint32 = Varint32([0u8; 6]); + + /// The maximum encoded length of a [`Varint32`]. + pub const MAX_LEN: usize = 5; + + /// Encodes a 32-bit integer to variable length. + #[inline] + pub const fn encode(mut value: u32) -> Self { + let mut output = [0u8; 6]; + let mut count = 0; + + while value > 0 { + output[count] = value as u8 & VARINT_MASK; + value >>= 7; + if value > 0 { + output[count] |= !VARINT_MASK; + count += 1; + } + } + + output[5] = count as u8 + 1; + Varint32(output) + } + + /// Returns the number of bytes in the encoded varint. + #[inline] + #[allow(clippy::len_without_is_empty)] + pub const fn len(self) -> usize { + self.0[5] as usize + } + + /// Returns whether or not the given value is the sentinel value. + #[inline] + pub const fn is_sentinel(self) -> bool { + self.len() == 0 + } + + /// Decodes the contained `Varint32`. + /// + /// Should only be used in debug assertions, as `Varint32`s not meant to encoded/decoded cheaply + /// throughout their lifecycle. The sentinel value is decoded as 0. + pub(crate) const fn decode(self) -> u32 { + // Note: It is not possible to decorate this function with `#[cfg(debug_assertions)]`, since + // `debug_assert!` will not remove the assertion from the code, but put it behind an + // `if false { .. }` instead. Furthermore we also don't panic at runtime, as adding + // a panic that only occurs in `--release` builds is arguably worse than this function + // being called. + + if self.is_sentinel() { + return 0; + } + + match decode_varint32(self.0.as_slice()) { + Incomplete(_) | Fatal(_) => 0, // actually unreachable. + Success(v) => v.value, + } + } + + /// Returns the length of the given value encoded as a `Varint32`. + #[inline] + pub const fn length_of(value: u32) -> usize { + if value < (1 << 7) { + return 1; + } + + if value < 1 << 14 { + return 2; + } + + if value < 1 << 21 { + return 3; + } + + if value < 1 << 28 { + return 4; + } + + 5 + } +} + +impl AsRef<[u8]> for Varint32 { + fn as_ref(&self) -> &[u8] { + &self.0[0..self.len()] + } +} + +#[cfg(test)] +mod tests { + use bytemuck::Zeroable; + use proptest::prelude::{any, prop::collection}; + use proptest_attr_macro::proptest; + + use crate::{ + varint::{decode_varint32, Overflow}, + Outcome, + }; + + use super::{ParsedU32, Varint32}; + + #[test] + fn encode_known_values() { + assert_eq!(Varint32::encode(0x00000000).as_ref(), &[0x00]); + assert_eq!(Varint32::encode(0x00000040).as_ref(), &[0x40]); + assert_eq!(Varint32::encode(0x0000007f).as_ref(), &[0x7f]); + assert_eq!(Varint32::encode(0x00000080).as_ref(), &[0x80, 0x01]); + assert_eq!(Varint32::encode(0x000000ff).as_ref(), &[0xff, 0x01]); + assert_eq!(Varint32::encode(0x0000ffff).as_ref(), &[0xff, 0xff, 0x03]); + assert_eq!( + Varint32::encode(u32::MAX).as_ref(), + &[0xff, 0xff, 0xff, 0xff, 0x0f] + ); + + // 0x12345678 = 0b0001 0010001 1010001 0101100 1111000 + // 0001 10010001 11010001 10101100 11111000 + // 0x 01 91 d1 ac f8 + + assert_eq!( + Varint32::encode(0x12345678).as_ref(), + &[0xf8, 0xac, 0xd1, 0x91, 0x01] + ); + } + + #[track_caller] + fn check_decode(expected: u32, input: &[u8]) { + let ParsedU32 { offset, value } = + decode_varint32(input).expect("expected decoding to succeed"); + assert_eq!(expected, value); + assert_eq!(offset.get() as usize, input.len()); + + // Also ensure that all partial outputs yield `Incomplete`. + let mut l = input.len(); + + while l > 1 { + l -= 1; + + let partial = &input[0..l]; + assert!(matches!(decode_varint32(partial), Outcome::Incomplete(n) if n.get() == 1)); + } + } + + #[test] + fn decode_known_values_and_crossover_points() { + check_decode(0x00000000, &[0x00]); + check_decode(0x00000040, &[0x40]); + check_decode(0x0000007f, &[0x7f]); + + check_decode(0x00000080, &[0x80, 0x01]); + check_decode(0x00000081, &[0x81, 0x01]); + check_decode(0x000000ff, &[0xff, 0x01]); + check_decode(0x00003fff, &[0xff, 0x7f]); + + check_decode(0x00004000, &[0x80, 0x80, 0x01]); + check_decode(0x00004001, &[0x81, 0x80, 0x01]); + check_decode(0x0000ffff, &[0xff, 0xff, 0x03]); + check_decode(0x001fffff, &[0xff, 0xff, 0x7f]); + + check_decode(0x00200000, &[0x80, 0x80, 0x80, 0x01]); + check_decode(0x00200001, &[0x81, 0x80, 0x80, 0x01]); + check_decode(0x0fffffff, &[0xff, 0xff, 0xff, 0x7f]); + + check_decode(0x10000000, &[0x80, 0x80, 0x80, 0x80, 0x01]); + check_decode(0x10000001, &[0x81, 0x80, 0x80, 0x80, 0x01]); + check_decode(0xf0000000, &[0x80, 0x80, 0x80, 0x80, 0x0f]); + check_decode(0x12345678, &[0xf8, 0xac, 0xd1, 0x91, 0x01]); + check_decode(0xffffffff, &[0xff, 0xFF, 0xFF, 0xFF, 0x0F]); + check_decode(u32::MAX, &[0xff, 0xff, 0xff, 0xff, 0x0f]); + } + + #[proptest] + fn roundtrip_value(value: u32) { + let encoded = Varint32::encode(value); + assert_eq!(encoded.len(), encoded.as_ref().len()); + assert!(!encoded.is_sentinel()); + check_decode(value, encoded.as_ref()); + + assert_eq!(encoded.decode(), value); + } + + #[test] + fn check_error_conditions() { + // Value is too long (no more than 5 bytes allowed). + assert!(matches!( + decode_varint32(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x01]), + Outcome::Fatal(Overflow) + )); + + // This behavior should already trigger on the fifth byte. + assert!(matches!( + decode_varint32(&[0x80, 0x80, 0x80, 0x80, 0x80]), + Outcome::Fatal(Overflow) + )); + + // Value is too big to be held by a `u32`. + assert!(matches!( + decode_varint32(&[0x80, 0x80, 0x80, 0x80, 0x10]), + Outcome::Fatal(Overflow) + )); + } + + proptest::proptest! { + #[test] + fn fuzz_varint(data in collection::vec(any::(), 0..256)) { + if let Outcome::Success(ParsedU32{ offset, value }) = decode_varint32(&data) { + let valid_substring = &data[0..(offset.get() as usize)]; + check_decode(value, valid_substring); + } + }} + + #[test] + fn ensure_is_zeroable() { + assert_eq!(Varint32::zeroed().as_ref(), Varint32::SENTINEL.as_ref()); + } + + #[test] + fn sentinel_has_length_zero() { + assert_eq!(Varint32::SENTINEL.len(), 0); + assert!(Varint32::SENTINEL.is_sentinel()); + } + + #[test] + fn working_sentinel_formatting_and_decoding() { + assert_eq!(format!("{:?}", Varint32::SENTINEL), "Varint32::SENTINEL"); + assert_eq!(Varint32::SENTINEL.decode(), 0); + } + + #[proptest] + fn working_debug_impl(value: u32) { + format!("{:?}", Varint32::encode(value)); + } +} diff --git a/juliet/test.sh b/juliet/test.sh new file mode 100755 index 0000000000..066d85562e --- /dev/null +++ b/juliet/test.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +#: Shorthand script to run test with logging setup correctly. + +RUST_LOG=${RUST_LOG:-juliet=trace} +export RUST_LOG + +# Run one thread at a time to not get interleaved output. +exec cargo test --features tracing -- --test-threads=1 --nocapture $@ diff --git a/node/CHANGELOG.md b/node/CHANGELOG.md index 4f21ecba8f..94cd5080f4 100644 --- a/node/CHANGELOG.md +++ b/node/CHANGELOG.md @@ -14,6 +14,11 @@ All notable changes to this project will be documented in this file. The format ## Unreleased ### Added +* The network handshake now contains the hash of the chainspec used and will be successful only if they match. +* Add an `identity` option to load existing network identity certificates signed by a CA. +* TLS connection keys can now be logged using the `network.keylog_location` setting (similar to `SSLKEYLOGFILE` envvar found in other applications). +* Add a `lock_status` field to the JSON representation of the `ContractPackage` values. +* Unit tests can be run with JSON log output by setting a `NODE_TEST_LOG=json` environment variable. * New environment variable `CL_EVENT_QUEUE_DUMP_THRESHOLD` to enable dumping of queue event counts to log when a certain threshold is exceeded. ### Fixed @@ -21,8 +26,11 @@ All notable changes to this project will be documented in this file. The format ### Changed * The `state_identifier` parameter of the `query_global_state` JSON-RPC method is now optional. If no `state_identifier` is specified, the highest complete block known to the node will be used to fulfill the request. +* The underlying network protocol has been changed, now supports multiplexing for better latency and proper backpressuring across nodes. +* Any metrics containing queue names "network_low_priority" and "network_incoming" have had said portion renamed to "message_low_priority" and "message_incoming". - +### Removed +* There is no more weighted rate limiting on incoming traffic, instead the nodes dynamically adjusts allowed rates from peers based on available resources. This resulted in the removal of the `estimator_weights` configuration option and the `accumulated_incoming_limiter_delay` metric. ## 1.5.2 diff --git a/node/Cargo.toml b/node/Cargo.toml index 05c7a5a833..e90f90a405 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -16,6 +16,7 @@ exclude = ["proptest-regressions"] ansi_term = "0.12.1" anyhow = "1" aquamarine = "0.1.12" +array-init = "2.0.1" async-trait = "0.1.50" backtrace = "0.3.50" base16 = "0.2.1" @@ -26,14 +27,13 @@ casper-execution-engine = { version = "5.0.0", path = "../execution_engine" } casper-hashing = { version = "2.0.0", path = "../hashing" } casper-json-rpc = { version = "1.1.0", path = "../json_rpc" } casper-types = { version = "3.0.0", path = "../types", features = ["datasize", "json-schema", "std"] } -datasize = { version = "0.2.11", features = ["detailed", "fake_clock-types", "futures-types", "smallvec-types"] } +datasize = { version = "0.2.15", features = ["detailed", "fake_clock-types", "futures-types", "smallvec-types"] } derive_more = "0.99.7" either = { version = "1", features = ["serde"] } enum-iterator = "0.6.0" erased-serde = "0.3.18" fs2 = "0.4.3" -futures = "0.3.5" -futures-io = "0.3.5" +futures = "0.3.21" hex-buffer-serde = "0.3.0" hex_fmt = "0.3.0" hostname = "0.3.0" @@ -41,6 +41,7 @@ http = "0.2.1" humantime = "2.1.0" hyper = "0.14.26" itertools = "0.10.0" +juliet = { path = "../juliet" } libc = "0.2.66" linked-hash-map = "0.5.3" lmdb-rkv = "0.14" @@ -78,9 +79,8 @@ tempfile = "3.4.0" thiserror = "1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "sync", "time"] } tokio-openssl = "0.6.1" -tokio-serde = { version = "0.8.0", features = ["bincode"] } tokio-stream = { version = "0.1.4", features = ["sync"] } -tokio-util = { version = "0.6.4", features = ["codec"] } +tokio-util = { version = "0.6.4", features = ["codec", "compat"] } toml = "0.5.6" tower = { version = "0.4.6", features = ["limit"] } tracing = "0.1.18" diff --git a/node/src/components.rs b/node/src/components.rs index af24afea45..3a19d32fc0 100644 --- a/node/src/components.rs +++ b/node/src/components.rs @@ -182,7 +182,7 @@ pub(crate) trait PortBoundComponent: InitializedComponent { } match self.listen(effect_builder) { - Ok(effects) => (effects, ComponentState::Initialized), + Ok(effects) => (effects, ComponentState::Initializing), Err(error) => (Effects::new(), ComponentState::Fatal(format!("{}", error))), } } diff --git a/node/src/components/block_accumulator/metrics.rs b/node/src/components/block_accumulator/metrics.rs index 5e44639b02..e0e3661bc0 100644 --- a/node/src/components/block_accumulator/metrics.rs +++ b/node/src/components/block_accumulator/metrics.rs @@ -1,44 +1,32 @@ use prometheus::{IntGauge, Registry}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Metrics for the block accumulator component. #[derive(Debug)] pub(super) struct Metrics { /// Total number of BlockAcceptors contained in the BlockAccumulator. - pub(super) block_acceptors: IntGauge, + pub(super) block_acceptors: RegisteredMetric, /// Number of child block hashes that we know of and that will be used in order to request next /// blocks. - pub(super) known_child_blocks: IntGauge, - registry: Registry, + pub(super) known_child_blocks: RegisteredMetric, } impl Metrics { /// Creates a new instance of the block accumulator metrics, using the given prefix. pub fn new(registry: &Registry) -> Result { - let block_acceptors = IntGauge::new( + let block_acceptors = registry.new_int_gauge( "block_accumulator_block_acceptors".to_string(), "number of block acceptors in the Block Accumulator".to_string(), )?; - let known_child_blocks = IntGauge::new( + let known_child_blocks = registry.new_int_gauge( "block_accumulator_known_child_blocks".to_string(), "number of blocks received by the Block Accumulator for which we know the hash of the child block".to_string(), )?; - registry.register(Box::new(block_acceptors.clone()))?; - registry.register(Box::new(known_child_blocks.clone()))?; - Ok(Metrics { block_acceptors, known_child_blocks, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.block_acceptors); - unregister_metric!(self.registry, self.known_child_blocks); - } -} diff --git a/node/src/components/block_accumulator/tests.rs b/node/src/components/block_accumulator/tests.rs index cc6d872a98..58d4066af0 100644 --- a/node/src/components/block_accumulator/tests.rs +++ b/node/src/components/block_accumulator/tests.rs @@ -618,10 +618,7 @@ fn acceptor_should_store_block() { let mut acceptor = BlockAcceptor::new(*block.hash(), vec![]); // Create 4 pairs of keys so we can later create 4 signatures. - let keys: Vec<(SecretKey, PublicKey)> = (0..4) - .into_iter() - .map(|_| generate_ed25519_keypair()) - .collect(); + let keys: Vec<(SecretKey, PublicKey)> = (0..4).map(|_| generate_ed25519_keypair()).collect(); // Register the keys into the era validator weights, front loaded on the // first 2 with 80% weight. let era_validator_weights = EraValidatorWeights::new( diff --git a/node/src/components/block_synchronizer/deploy_acquisition/tests.rs b/node/src/components/block_synchronizer/deploy_acquisition/tests.rs index a14665517c..af76e86125 100644 --- a/node/src/components/block_synchronizer/deploy_acquisition/tests.rs +++ b/node/src/components/block_synchronizer/deploy_acquisition/tests.rs @@ -11,7 +11,6 @@ use super::*; fn gen_test_deploys(rng: &mut TestRng) -> BTreeMap { let num_deploys = rng.gen_range(2..15); (0..num_deploys) - .into_iter() .map(|_| { let deploy = Deploy::random(rng); (*deploy.hash(), deploy) diff --git a/node/src/components/block_synchronizer/execution_results_acquisition/tests.rs b/node/src/components/block_synchronizer/execution_results_acquisition/tests.rs index b205d5df24..729ddd30e8 100644 --- a/node/src/components/block_synchronizer/execution_results_acquisition/tests.rs +++ b/node/src/components/block_synchronizer/execution_results_acquisition/tests.rs @@ -15,10 +15,8 @@ fn execution_results_chunks_apply_correctly() { let block = Block::random(&mut rng); // Create chunkable execution results - let exec_results: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() - .map(|_| rng.gen()) - .collect(); + let exec_results: Vec = + (0..NUM_TEST_EXECUTION_RESULTS).map(|_| rng.gen()).collect(); let test_chunks = chunks_with_proof_from_data(&exec_results.to_bytes().unwrap()); assert!(test_chunks.len() >= 3); @@ -166,10 +164,8 @@ fn cant_apply_chunk_from_different_exec_results_or_invalid_checksum() { let block = Block::random(&mut rng); // Create valid execution results - let valid_exec_results: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() - .map(|_| rng.gen()) - .collect(); + let valid_exec_results: Vec = + (0..NUM_TEST_EXECUTION_RESULTS).map(|_| rng.gen()).collect(); let valid_test_chunks = chunks_with_proof_from_data(&valid_exec_results.to_bytes().unwrap()); assert!(valid_test_chunks.len() >= 3); @@ -351,10 +347,8 @@ fn acquisition_pending_state_has_correct_transitions() { ); // Acquisition can transition from `Pending` to `Acquiring` if a single chunk is applied - let exec_results: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() - .map(|_| rng.gen()) - .collect(); + let exec_results: Vec = + (0..NUM_TEST_EXECUTION_RESULTS).map(|_| rng.gen()).collect(); let test_chunks = chunks_with_proof_from_data(&exec_results.to_bytes().unwrap()); assert!(test_chunks.len() >= 3); @@ -362,7 +356,6 @@ fn acquisition_pending_state_has_correct_transitions() { let exec_result = BlockExecutionResultsOrChunkId::new(*block.hash()) .response(ValueOrChunk::ChunkWithProof(first_chunk.clone())); let deploy_hashes: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() .map(|index| DeployHash::new(Digest::hash(index.to_bytes().unwrap()))) .collect(); assert_matches!( @@ -380,10 +373,8 @@ fn acquisition_acquiring_state_has_correct_transitions() { let block = Block::random(&mut rng); // Generate valid execution results that are chunkable - let exec_results: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() - .map(|_| rng.gen()) - .collect(); + let exec_results: Vec = + (0..NUM_TEST_EXECUTION_RESULTS).map(|_| rng.gen()).collect(); let test_chunks = chunks_with_proof_from_data(&exec_results.to_bytes().unwrap()); assert!(test_chunks.len() >= 3); @@ -417,7 +408,6 @@ fn acquisition_acquiring_state_has_correct_transitions() { let exec_result = BlockExecutionResultsOrChunkId::new(*block.hash()) .response(ValueOrChunk::ChunkWithProof(last_chunk.clone())); let deploy_hashes: Vec = (0..NUM_TEST_EXECUTION_RESULTS) - .into_iter() .map(|index| DeployHash::new(Digest::hash(index.to_bytes().unwrap()))) .collect(); acquisition = assert_matches!( diff --git a/node/src/components/block_synchronizer/global_state_synchronizer/tests.rs b/node/src/components/block_synchronizer/global_state_synchronizer/tests.rs index 9a2b2563de..a45f3f7492 100644 --- a/node/src/components/block_synchronizer/global_state_synchronizer/tests.rs +++ b/node/src/components/block_synchronizer/global_state_synchronizer/tests.rs @@ -78,7 +78,7 @@ impl MockReactor { } fn random_test_trie(rng: &mut TestRng) -> TrieRaw { - let data: Vec = (0..64).into_iter().map(|_| rng.gen()).collect(); + let data: Vec = (0..64).map(|_| rng.gen()).collect(); TrieRaw::new(Bytes::from(data)) } @@ -210,7 +210,6 @@ async fn sync_global_state_request_starts_maximum_trie_fetches() { // root node would have some children that we haven't yet downloaded Err(engine_state::Error::MissingTrieNodeChildren( (0u8..255) - .into_iter() // TODO: generate random hashes when `rng.gen` works .map(|i| Digest::hash([i; 32])) .collect(), @@ -497,7 +496,6 @@ async fn missing_trie_node_children_triggers_fetch() { // We generate more than the parallel_fetch_limit. let num_missing_trie_nodes = rng.gen_range(12..20); let missing_tries: Vec = (0..num_missing_trie_nodes) - .into_iter() .map(|_| random_test_trie(&mut rng)) .collect(); let missing_trie_nodes_hashes: Vec = missing_tries diff --git a/node/src/components/block_synchronizer/metrics.rs b/node/src/components/block_synchronizer/metrics.rs index 541fa5f09c..786e731c8a 100644 --- a/node/src/components/block_synchronizer/metrics.rs +++ b/node/src/components/block_synchronizer/metrics.rs @@ -1,6 +1,6 @@ use prometheus::{Histogram, Registry}; -use crate::{unregister_metric, utils}; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; const HIST_SYNC_DURATION_NAME: &str = "historical_block_sync_duration_seconds"; const HIST_SYNC_DURATION_HELP: &str = "duration (in sec) to synchronize a historical block"; @@ -17,10 +17,9 @@ const EXPONENTIAL_BUCKET_COUNT: usize = 10; #[derive(Debug)] pub(super) struct Metrics { /// Time duration for the historical synchronizer to get a block. - pub(super) historical_block_sync_duration: Histogram, + pub(super) historical_block_sync_duration: RegisteredMetric, /// Time duration for the forward synchronizer to get a block. - pub(super) forward_block_sync_duration: Histogram, - registry: Registry, + pub(super) forward_block_sync_duration: RegisteredMetric, } impl Metrics { @@ -33,26 +32,16 @@ impl Metrics { )?; Ok(Metrics { - historical_block_sync_duration: utils::register_histogram_metric( - registry, + historical_block_sync_duration: registry.new_histogram( HIST_SYNC_DURATION_NAME, HIST_SYNC_DURATION_HELP, buckets.clone(), )?, - forward_block_sync_duration: utils::register_histogram_metric( - registry, + forward_block_sync_duration: registry.new_histogram( FWD_SYNC_DURATION_NAME, FWD_SYNC_DURATION_HELP, buckets, )?, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.historical_block_sync_duration); - unregister_metric!(self.registry, self.forward_block_sync_duration); - } -} diff --git a/node/src/components/block_synchronizer/peer_list/tests.rs b/node/src/components/block_synchronizer/peer_list/tests.rs index 7302738e20..24035aa7a5 100644 --- a/node/src/components/block_synchronizer/peer_list/tests.rs +++ b/node/src/components/block_synchronizer/peer_list/tests.rs @@ -19,10 +19,7 @@ impl PeerList { // Create multiple random peers fn random_peers(rng: &mut TestRng, num_random_peers: usize) -> HashSet { - (0..num_random_peers) - .into_iter() - .map(|_| NodeId::random(rng)) - .collect() + (0..num_random_peers).map(|_| NodeId::random(rng)).collect() } #[test] diff --git a/node/src/components/block_synchronizer/tests.rs b/node/src/components/block_synchronizer/tests.rs index e9e81f9e04..4b9f5b44e3 100644 --- a/node/src/components/block_synchronizer/tests.rs +++ b/node/src/components/block_synchronizer/tests.rs @@ -95,7 +95,7 @@ impl MockReactor { ) -> Vec { let mut events = Vec::new(); for effect in effects { - tokio::spawn(async move { effect.await }); + tokio::spawn(effect); let event = self.crank().await; events.push(event); } @@ -661,7 +661,7 @@ async fn should_not_stall_after_registering_new_era_validator_weights() { // bleed off the event q, checking the expected event kind for effect in effects { - tokio::spawn(async move { effect.await }); + tokio::spawn(effect); let event = mock_reactor.crank().await; match event { MockReactorEvent::SyncLeapFetcherRequest(_) => (), diff --git a/node/src/components/block_synchronizer/tests/test_utils.rs b/node/src/components/block_synchronizer/tests/test_utils.rs index 2079fb0276..27f71d21c3 100644 --- a/node/src/components/block_synchronizer/tests/test_utils.rs +++ b/node/src/components/block_synchronizer/tests/test_utils.rs @@ -7,7 +7,6 @@ use rand::Rng; pub(crate) fn chunks_with_proof_from_data(data: &[u8]) -> BTreeMap { (0..data.chunks(ChunkWithProof::CHUNK_SIZE_BYTES).count()) - .into_iter() .map(|index| { ( index as u64, @@ -22,7 +21,6 @@ pub(crate) fn test_chunks_with_proof( ) -> (Vec, Vec, Vec) { let mut rng = rand::thread_rng(); let data: Vec = (0..ChunkWithProof::CHUNK_SIZE_BYTES * num_chunks as usize) - .into_iter() .map(|_| rng.gen()) .collect(); diff --git a/node/src/components/block_synchronizer/trie_accumulator/tests.rs b/node/src/components/block_synchronizer/trie_accumulator/tests.rs index ce30f032ef..05d13d80ef 100644 --- a/node/src/components/block_synchronizer/trie_accumulator/tests.rs +++ b/node/src/components/block_synchronizer/trie_accumulator/tests.rs @@ -131,10 +131,7 @@ async fn failed_fetch_retriggers_download_with_different_peer() { let (_, chunk_ids, _) = test_chunks_with_proof(1); // Create multiple peers - let peers: Vec = (0..2) - .into_iter() - .map(|_| NodeId::random(&mut rng)) - .collect(); + let peers: Vec = (0..2).map(|_| NodeId::random(&mut rng)).collect(); let chunks = PartialChunks { peers: peers.clone(), diff --git a/node/src/components/consensus.rs b/node/src/components/consensus.rs index d9aecaa31f..c038e3160b 100644 --- a/node/src/components/consensus.rs +++ b/node/src/components/consensus.rs @@ -233,7 +233,11 @@ impl Display for ConsensusRequestMessage { impl Display for Event { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Event::Incoming(ConsensusMessageIncoming { sender, message }) => { + Event::Incoming(ConsensusMessageIncoming { + sender, + message, + ticket: _, + }) => { write!(f, "message from {:?}: {}", sender, message) } Event::DemandIncoming(demand) => { @@ -426,8 +430,14 @@ where Event::Action { era_id, action_id } => { self.handle_action(effect_builder, rng, era_id, action_id) } - Event::Incoming(ConsensusMessageIncoming { sender, message }) => { - self.handle_message(effect_builder, rng, sender, *message) + Event::Incoming(ConsensusMessageIncoming { + sender, + message, + ticket, + }) => { + let rv = self.handle_message(effect_builder, rng, sender, *message); + drop(ticket); + rv } Event::DemandIncoming(ConsensusDemand { sender, diff --git a/node/src/components/consensus/era_supervisor.rs b/node/src/components/consensus/era_supervisor.rs index ef9f2cd77d..3d260f84d1 100644 --- a/node/src/components/consensus/era_supervisor.rs +++ b/node/src/components/consensus/era_supervisor.rs @@ -980,7 +980,7 @@ impl EraSupervisor { } ProtocolOutcome::CreatedTargetedMessage(payload, to) => { let message = ConsensusMessage::Protocol { era_id, payload }; - effect_builder.enqueue_message(to, message.into()).ignore() + effect_builder.try_send_message(to, message.into()).ignore() } ProtocolOutcome::CreatedMessageToRandomPeer(payload) => { let message = ConsensusMessage::Protocol { era_id, payload }; @@ -988,7 +988,7 @@ impl EraSupervisor { async move { let peers = effect_builder.get_fully_connected_peers(1).await; if let Some(to) = peers.into_iter().next() { - effect_builder.enqueue_message(to, message.into()).await; + effect_builder.try_send_message(to, message.into()).await; } } .ignore() @@ -999,7 +999,7 @@ impl EraSupervisor { async move { let peers = effect_builder.get_fully_connected_peers(1).await; if let Some(to) = peers.into_iter().next() { - effect_builder.enqueue_message(to, message.into()).await; + effect_builder.try_send_message(to, message.into()).await; } } .ignore() diff --git a/node/src/components/consensus/highway_core/active_validator.rs b/node/src/components/consensus/highway_core/active_validator.rs index 588b928b5c..ebddb64986 100644 --- a/node/src/components/consensus/highway_core/active_validator.rs +++ b/node/src/components/consensus/highway_core/active_validator.rs @@ -1,3 +1,4 @@ +#![allow(clippy::arithmetic_side_effects)] use std::{ fmt::{self, Debug}, fs::{self, File}, diff --git a/node/src/components/consensus/highway_core/finality_detector.rs b/node/src/components/consensus/highway_core/finality_detector.rs index 32e4563a29..8fd553061b 100644 --- a/node/src/components/consensus/highway_core/finality_detector.rs +++ b/node/src/components/consensus/highway_core/finality_detector.rs @@ -1,4 +1,5 @@ //! Functions for detecting finality of proposed blocks and calculating rewards. +#![allow(clippy::arithmetic_side_effects)] mod horizon; mod rewards; diff --git a/node/src/components/consensus/highway_core/highway.rs b/node/src/components/consensus/highway_core/highway.rs index 682485463d..9b8665d6c9 100644 --- a/node/src/components/consensus/highway_core/highway.rs +++ b/node/src/components/consensus/highway_core/highway.rs @@ -1,4 +1,5 @@ //! The implementation of the Highway consensus protocol. +#![allow(clippy::arithmetic_side_effects)] mod vertex; diff --git a/node/src/components/consensus/highway_core/state.rs b/node/src/components/consensus/highway_core/state.rs index a250140209..b7d1943b53 100644 --- a/node/src/components/consensus/highway_core/state.rs +++ b/node/src/components/consensus/highway_core/state.rs @@ -1,3 +1,4 @@ +#![allow(clippy::arithmetic_side_effects)] mod block; mod index_panorama; mod panorama; diff --git a/node/src/components/consensus/highway_core/state/tallies.rs b/node/src/components/consensus/highway_core/state/tallies.rs index 2c8aba60ca..732bf63454 100644 --- a/node/src/components/consensus/highway_core/state/tallies.rs +++ b/node/src/components/consensus/highway_core/state/tallies.rs @@ -1,3 +1,5 @@ +#![allow(clippy::arithmetic_side_effects)] + use std::{ collections::BTreeMap, iter::{self, Extend, FromIterator}, diff --git a/node/src/components/consensus/highway_core/state/tests.rs b/node/src/components/consensus/highway_core/state/tests.rs index a04b0ace94..a4589a0a7d 100644 --- a/node/src/components/consensus/highway_core/state/tests.rs +++ b/node/src/components/consensus/highway_core/state/tests.rs @@ -500,6 +500,8 @@ fn validate_lnc_mixed_citations() -> Result<(), AddUnitError> { if !ENABLE_ENDORSEMENTS { return Ok(()); } + + #[rustfmt::skip] // Eric's vote should not require an endorsement as his unit e0 cites equivocator Carol before // the fork. // @@ -545,6 +547,8 @@ fn validate_lnc_transitive_endorsement() -> Result<(), AddUnitError if !ENABLE_ENDORSEMENTS { return Ok(()); } + + #[rustfmt::skip] // Endorsements should be transitive to descendants. // c1 doesn't have to be endorsed, it is enough that c0 is. // @@ -582,6 +586,8 @@ fn validate_lnc_cite_descendant_of_equivocation() -> Result<(), AddUnitError, /// Amount of finalized blocks. - finalized_block_count: IntGauge, + finalized_block_count: RegisteredMetric, /// Timestamp of the most recently accepted block payload. - time_of_last_proposed_block: IntGauge, + time_of_last_proposed_block: RegisteredMetric, /// Timestamp of the most recently finalized block. - time_of_last_finalized_block: IntGauge, + time_of_last_finalized_block: RegisteredMetric, /// The current era. - pub(super) consensus_current_era: IntGauge, - /// Registry component. - registry: Registry, + pub(super) consensus_current_era: RegisteredMetric, } impl Metrics { pub(super) fn new(registry: &Registry) -> Result { - let finalization_time = Gauge::new( + let finalization_time = registry.new_gauge( "finalization_time", "the amount of time, in milliseconds, between proposal and finalization of the latest finalized block", )?; let finalized_block_count = - IntGauge::new("amount_of_blocks", "the number of blocks finalized so far")?; - let time_of_last_proposed_block = IntGauge::new( + registry.new_int_gauge("amount_of_blocks", "the number of blocks finalized so far")?; + let time_of_last_proposed_block = registry.new_int_gauge( "time_of_last_block_payload", "timestamp of the most recently accepted block payload", )?; - let time_of_last_finalized_block = IntGauge::new( + let time_of_last_finalized_block = registry.new_int_gauge( "time_of_last_finalized_block", "timestamp of the most recently finalized block", )?; let consensus_current_era = - IntGauge::new("consensus_current_era", "the current era in consensus")?; - registry.register(Box::new(finalization_time.clone()))?; - registry.register(Box::new(finalized_block_count.clone()))?; - registry.register(Box::new(consensus_current_era.clone()))?; - registry.register(Box::new(time_of_last_proposed_block.clone()))?; - registry.register(Box::new(time_of_last_finalized_block.clone()))?; + registry.new_int_gauge("consensus_current_era", "the current era in consensus")?; + Ok(Metrics { finalization_time, finalized_block_count, time_of_last_proposed_block, time_of_last_finalized_block, consensus_current_era, - registry: registry.clone(), }) } @@ -70,13 +66,3 @@ impl Metrics { .set(Timestamp::now().millis() as i64); } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.finalization_time); - unregister_metric!(self.registry, self.finalized_block_count); - unregister_metric!(self.registry, self.consensus_current_era); - unregister_metric!(self.registry, self.time_of_last_finalized_block); - unregister_metric!(self.registry, self.time_of_last_proposed_block); - } -} diff --git a/node/src/components/consensus/protocols/common.rs b/node/src/components/consensus/protocols/common.rs index 95f7f13e97..fda8e77392 100644 --- a/node/src/components/consensus/protocols/common.rs +++ b/node/src/components/consensus/protocols/common.rs @@ -1,4 +1,5 @@ //! Utilities common to different consensus algorithms. +#![allow(clippy::arithmetic_side_effects)] use itertools::Itertools; use num_rational::Ratio; diff --git a/node/src/components/consensus/protocols/highway.rs b/node/src/components/consensus/protocols/highway.rs index 8626c9d41f..a81d498973 100644 --- a/node/src/components/consensus/protocols/highway.rs +++ b/node/src/components/consensus/protocols/highway.rs @@ -1,3 +1,5 @@ +#![allow(clippy::arithmetic_side_effects)] + pub(crate) mod config; mod participation; mod round_success_meter; @@ -609,7 +611,6 @@ impl HighwayProtocol { unit_seq_number, } }) - .into_iter() .collect() } else { // We're ahead. @@ -649,7 +650,6 @@ impl HighwayProtocol { .wire_unit(unit, *self.highway.instance_id()) .map(|swu| HighwayMessage::NewVertex(Vertex::Unit(swu))) }) - .into_iter() .collect(), }, } diff --git a/node/src/components/consensus/protocols/zug.rs b/node/src/components/consensus/protocols/zug.rs index e10a1c0fd9..4b691fd6b8 100644 --- a/node/src/components/consensus/protocols/zug.rs +++ b/node/src/components/consensus/protocols/zug.rs @@ -1,3 +1,4 @@ +#![allow(clippy::arithmetic_side_effects)] //! # The Zug consensus protocol. //! //! This protocol requires that at most _f_ out of _n > 3 f_ validators (by weight) are faulty. It diff --git a/node/src/components/consensus/utils/validators.rs b/node/src/components/consensus/utils/validators.rs index 50b4175fcf..a651c770b0 100644 --- a/node/src/components/consensus/utils/validators.rs +++ b/node/src/components/consensus/utils/validators.rs @@ -1,3 +1,4 @@ +#![allow(clippy::arithmetic_side_effects)] use std::{ collections::HashMap, fmt, diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index e052b47d83..643e5757dd 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -270,11 +270,16 @@ impl ContractRuntime { fn handle_trie_request( &self, effect_builder: EffectBuilder, - TrieRequestIncoming { sender, message }: TrieRequestIncoming, + TrieRequestIncoming { + sender, + message, + ticket, + }: TrieRequestIncoming, ) -> Effects where REv: From> + Send, { + drop(ticket); // TODO: Properly handle ticket. let TrieRequest(ref serialized_id) = *message; let fetch_response = match self.get_trie(serialized_id) { Ok(fetch_response) => fetch_response, diff --git a/node/src/components/contract_runtime/metrics.rs b/node/src/components/contract_runtime/metrics.rs index a7833e72fd..7160125b75 100644 --- a/node/src/components/contract_runtime/metrics.rs +++ b/node/src/components/contract_runtime/metrics.rs @@ -1,6 +1,6 @@ use prometheus::{self, Gauge, Histogram, IntGauge, Registry}; -use crate::{unregister_metric, utils}; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Value of upper bound of histogram. const EXPONENTIAL_BUCKET_START: f64 = 0.01; @@ -58,20 +58,19 @@ const EXEC_QUEUE_SIZE_HELP: &str = /// Metrics for the contract runtime component. #[derive(Debug)] pub struct Metrics { - pub(super) run_execute: Histogram, - pub(super) apply_effect: Histogram, - pub(super) commit_upgrade: Histogram, - pub(super) run_query: Histogram, - pub(super) commit_step: Histogram, - pub(super) get_balance: Histogram, - pub(super) get_era_validators: Histogram, - pub(super) get_bids: Histogram, - pub(super) put_trie: Histogram, - pub(super) get_trie: Histogram, - pub(super) exec_block: Histogram, - pub(super) latest_commit_step: Gauge, - pub(super) exec_queue_size: IntGauge, - registry: Registry, + pub(super) run_execute: RegisteredMetric, + pub(super) apply_effect: RegisteredMetric, + pub(super) commit_upgrade: RegisteredMetric, + pub(super) run_query: RegisteredMetric, + pub(super) commit_step: RegisteredMetric, + pub(super) get_balance: RegisteredMetric, + pub(super) get_era_validators: RegisteredMetric, + pub(super) get_bids: RegisteredMetric, + pub(super) put_trie: RegisteredMetric, + pub(super) get_trie: RegisteredMetric, + pub(super) exec_block: RegisteredMetric, + pub(super) latest_commit_step: RegisteredMetric, + pub(super) exec_queue_size: RegisteredMetric, } impl Metrics { @@ -89,100 +88,57 @@ impl Metrics { // Anything above that should be a warning signal. let tiny_buckets = prometheus::exponential_buckets(0.001, 2.0, 10)?; - let latest_commit_step = Gauge::new(LATEST_COMMIT_STEP_NAME, LATEST_COMMIT_STEP_HELP)?; - registry.register(Box::new(latest_commit_step.clone()))?; + let latest_commit_step = + registry.new_gauge(LATEST_COMMIT_STEP_NAME, LATEST_COMMIT_STEP_HELP)?; - let exec_queue_size = IntGauge::new(EXEC_QUEUE_SIZE_NAME, EXEC_QUEUE_SIZE_HELP)?; - registry.register(Box::new(exec_queue_size.clone()))?; + let exec_queue_size = registry.new_int_gauge(EXEC_QUEUE_SIZE_NAME, EXEC_QUEUE_SIZE_HELP)?; Ok(Metrics { - run_execute: utils::register_histogram_metric( - registry, + run_execute: registry.new_histogram( RUN_EXECUTE_NAME, RUN_EXECUTE_HELP, common_buckets.clone(), )?, - apply_effect: utils::register_histogram_metric( - registry, + apply_effect: registry.new_histogram( APPLY_EFFECT_NAME, APPLY_EFFECT_HELP, common_buckets.clone(), )?, - run_query: utils::register_histogram_metric( - registry, + run_query: registry.new_histogram( RUN_QUERY_NAME, RUN_QUERY_HELP, common_buckets.clone(), )?, - commit_step: utils::register_histogram_metric( - registry, + commit_step: registry.new_histogram( COMMIT_STEP_NAME, COMMIT_STEP_HELP, common_buckets.clone(), )?, - commit_upgrade: utils::register_histogram_metric( - registry, + commit_upgrade: registry.new_histogram( COMMIT_UPGRADE_NAME, COMMIT_UPGRADE_HELP, common_buckets.clone(), )?, - get_balance: utils::register_histogram_metric( - registry, + get_balance: registry.new_histogram( GET_BALANCE_NAME, GET_BALANCE_HELP, common_buckets.clone(), )?, - get_era_validators: utils::register_histogram_metric( - registry, + get_era_validators: registry.new_histogram( GET_ERA_VALIDATORS_NAME, GET_ERA_VALIDATORS_HELP, common_buckets.clone(), )?, - get_bids: utils::register_histogram_metric( - registry, + get_bids: registry.new_histogram( GET_BIDS_NAME, GET_BIDS_HELP, common_buckets.clone(), )?, - get_trie: utils::register_histogram_metric( - registry, - GET_TRIE_NAME, - GET_TRIE_HELP, - tiny_buckets.clone(), - )?, - put_trie: utils::register_histogram_metric( - registry, - PUT_TRIE_NAME, - PUT_TRIE_HELP, - tiny_buckets, - )?, - exec_block: utils::register_histogram_metric( - registry, - EXEC_BLOCK_NAME, - EXEC_BLOCK_HELP, - common_buckets, - )?, + get_trie: registry.new_histogram(GET_TRIE_NAME, GET_TRIE_HELP, tiny_buckets.clone())?, + put_trie: registry.new_histogram(PUT_TRIE_NAME, PUT_TRIE_HELP, tiny_buckets)?, + exec_block: registry.new_histogram(EXEC_BLOCK_NAME, EXEC_BLOCK_HELP, common_buckets)?, latest_commit_step, exec_queue_size, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.run_execute); - unregister_metric!(self.registry, self.apply_effect); - unregister_metric!(self.registry, self.commit_upgrade); - unregister_metric!(self.registry, self.run_query); - unregister_metric!(self.registry, self.commit_step); - unregister_metric!(self.registry, self.get_balance); - unregister_metric!(self.registry, self.get_era_validators); - unregister_metric!(self.registry, self.get_bids); - unregister_metric!(self.registry, self.put_trie); - unregister_metric!(self.registry, self.get_trie); - unregister_metric!(self.registry, self.exec_block); - unregister_metric!(self.registry, self.latest_commit_step); - unregister_metric!(self.registry, self.exec_queue_size); - } -} diff --git a/node/src/components/deploy_acceptor/metrics.rs b/node/src/components/deploy_acceptor/metrics.rs index 444bd41ee3..d48b5f685b 100644 --- a/node/src/components/deploy_acceptor/metrics.rs +++ b/node/src/components/deploy_acceptor/metrics.rs @@ -2,7 +2,7 @@ use prometheus::{Histogram, Registry}; use casper_types::Timestamp; -use crate::{unregister_metric, utils}; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; const DEPLOY_ACCEPTED_NAME: &str = "deploy_acceptor_accepted_deploy"; const DEPLOY_ACCEPTED_HELP: &str = "time in seconds to accept a deploy in the deploy acceptor"; @@ -20,9 +20,8 @@ const EXPONENTIAL_BUCKET_COUNT: usize = 10; #[derive(Debug)] pub(super) struct Metrics { - deploy_accepted: Histogram, - deploy_rejected: Histogram, - registry: Registry, + deploy_accepted: RegisteredMetric, + deploy_rejected: RegisteredMetric, } impl Metrics { @@ -34,19 +33,16 @@ impl Metrics { )?; Ok(Self { - deploy_accepted: utils::register_histogram_metric( - registry, + deploy_accepted: registry.new_histogram( DEPLOY_ACCEPTED_NAME, DEPLOY_ACCEPTED_HELP, common_buckets.clone(), )?, - deploy_rejected: utils::register_histogram_metric( - registry, + deploy_rejected: registry.new_histogram( DEPLOY_REJECTED_NAME, DEPLOY_REJECTED_HELP, common_buckets, )?, - registry: registry.clone(), }) } @@ -60,10 +56,3 @@ impl Metrics { .observe(start.elapsed().millis() as f64); } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.deploy_accepted); - unregister_metric!(self.registry, self.deploy_rejected); - } -} diff --git a/node/src/components/deploy_buffer/metrics.rs b/node/src/components/deploy_buffer/metrics.rs index df2e292b01..811324ba9b 100644 --- a/node/src/components/deploy_buffer/metrics.rs +++ b/node/src/components/deploy_buffer/metrics.rs @@ -1,52 +1,38 @@ use prometheus::{IntGauge, Registry}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Metrics for the deploy_buffer component. #[derive(Debug)] pub(super) struct Metrics { /// Total number of deploys contained in the deploy buffer. - pub(super) total_deploys: IntGauge, + pub(super) total_deploys: RegisteredMetric, /// Number of deploys contained in in-flight proposed blocks. - pub(super) held_deploys: IntGauge, + pub(super) held_deploys: RegisteredMetric, /// Number of deploys that should not be included in future proposals ever again. - pub(super) dead_deploys: IntGauge, - registry: Registry, + pub(super) dead_deploys: RegisteredMetric, } impl Metrics { /// Creates a new instance of the block accumulator metrics, using the given prefix. pub fn new(registry: &Registry) -> Result { - let total_deploys = IntGauge::new( + let total_deploys = registry.new_int_gauge( "deploy_buffer_total_deploys".to_string(), "total number of deploys contained in the deploy buffer.".to_string(), )?; - let held_deploys = IntGauge::new( + let held_deploys = registry.new_int_gauge( "deploy_buffer_held_deploys".to_string(), "number of deploys included in in-flight proposed blocks.".to_string(), )?; - let dead_deploys = IntGauge::new( + let dead_deploys = registry.new_int_gauge( "deploy_buffer_dead_deploys".to_string(), "number of deploys that should not be included in future proposals.".to_string(), )?; - registry.register(Box::new(total_deploys.clone()))?; - registry.register(Box::new(held_deploys.clone()))?; - registry.register(Box::new(dead_deploys.clone()))?; - Ok(Metrics { total_deploys, held_deploys, dead_deploys, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.total_deploys); - unregister_metric!(self.registry, self.held_deploys); - unregister_metric!(self.registry, self.dead_deploys); - } -} diff --git a/node/src/components/diagnostics_port.rs b/node/src/components/diagnostics_port.rs index 682750c9b2..78d74b8ab9 100644 --- a/node/src/components/diagnostics_port.rs +++ b/node/src/components/diagnostics_port.rs @@ -17,7 +17,7 @@ use std::{ use datasize::DataSize; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio::{net::UnixListener, sync::watch}; +use tokio::net::UnixListener; use tracing::{debug, error, info, warn}; use crate::{ @@ -30,7 +30,7 @@ use crate::{ }, reactor::main_reactor::MainEvent, types::NodeRng, - utils::umask, + utils::{umask, DropSwitch, ObservableFuse}, WithDir, }; pub(crate) use stop_at::StopAtSpec; @@ -65,8 +65,8 @@ impl Default for Config { pub(crate) struct DiagnosticsPort { state: ComponentState, /// Sender which will cause server and client connections to exit when dropped. - #[data_size(skip)] - _shutdown_sender: Option>, // only used for its `Drop` impl + #[allow(dead_code)] + shutdown_fuse: DropSwitch, config: WithDir, } @@ -76,7 +76,7 @@ impl DiagnosticsPort { DiagnosticsPort { state: ComponentState::Uninitialized, config, - _shutdown_sender: None, + shutdown_fuse: DropSwitch::new(ObservableFuse::new()), } } } @@ -141,8 +141,16 @@ where if self.state != ComponentState::Initializing { return Effects::new(); } - let (effects, state) = self.bind(self.config.value().enabled, effect_builder); + let (effects, mut state) = + self.bind(self.config.value().enabled, effect_builder); + + if matches!(state, ComponentState::Initializing) { + // No port address to bind, jump to initialized immediately. + state = ComponentState::Initialized; + } + >::set_state(self, state); + effects } }, @@ -195,10 +203,6 @@ where &mut self, effect_builder: EffectBuilder, ) -> Result, Self::Error> { - let (shutdown_sender, shutdown_receiver) = watch::channel(()); - - self._shutdown_sender = Some(shutdown_sender); - let cfg = self.config.value(); let socket_path = self.config.with_dir(cfg.socket_path.clone()); @@ -208,7 +212,12 @@ where #[allow(clippy::useless_conversion)] cfg.socket_umask.into(), )?; - let server = tasks::server(effect_builder, socket_path, listener, shutdown_receiver); + let server = tasks::server( + effect_builder, + socket_path, + listener, + self.shutdown_fuse.inner().clone(), + ); Ok(server.ignore()) } } diff --git a/node/src/components/diagnostics_port/command.rs b/node/src/components/diagnostics_port/command.rs index 18e3477769..d7c48f59cb 100644 --- a/node/src/components/diagnostics_port/command.rs +++ b/node/src/components/diagnostics_port/command.rs @@ -23,11 +23,12 @@ pub(super) enum Error { } /// Output format information is sent back to the client it. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Default)] pub(super) enum OutputFormat { /// Human-readable interactive format. /// /// No string form, utilizes the `Display` implementation of types passed in. + #[default] Interactive, /// JSON, pretty-printed. Json, @@ -35,12 +36,6 @@ pub(super) enum OutputFormat { Bincode, } -impl Default for OutputFormat { - fn default() -> Self { - OutputFormat::Interactive - } -} - impl Display for OutputFormat { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { diff --git a/node/src/components/diagnostics_port/stop_at.rs b/node/src/components/diagnostics_port/stop_at.rs index b077f6e442..ac80142617 100644 --- a/node/src/components/diagnostics_port/stop_at.rs +++ b/node/src/components/diagnostics_port/stop_at.rs @@ -8,10 +8,11 @@ use datasize::DataSize; use serde::Serialize; /// A specification for a stopping point. -#[derive(Copy, Clone, DataSize, Debug, Eq, PartialEq, Serialize)] +#[derive(Copy, Clone, DataSize, Debug, Eq, PartialEq, Serialize, Default)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] pub(crate) enum StopAtSpec { /// Stop after completion of the current block. + #[default] NextBlock, /// Stop after the completion of the next switch block. EndOfCurrentEra, @@ -23,12 +24,6 @@ pub(crate) enum StopAtSpec { EraId(EraId), } -impl Default for StopAtSpec { - fn default() -> Self { - StopAtSpec::NextBlock - } -} - impl Display for StopAtSpec { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { diff --git a/node/src/components/diagnostics_port/tasks.rs b/node/src/components/diagnostics_port/tasks.rs index 312e2bea71..f19de23ec3 100644 --- a/node/src/components/diagnostics_port/tasks.rs +++ b/node/src/components/diagnostics_port/tasks.rs @@ -11,13 +11,15 @@ use bincode::{ DefaultOptions, Options, }; use erased_serde::Serializer as ErasedSerializer; -use futures::future::{self, Either}; +use futures::{ + future::{self, Either}, + pin_mut, +}; use serde::Serialize; use thiserror::Error; use tokio::{ io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader}, net::{unix::OwnedWriteHalf, UnixListener, UnixStream}, - sync::watch, }; use tracing::{debug, info, info_span, warn, Instrument}; @@ -37,7 +39,7 @@ use crate::{ EffectBuilder, }, logging, - utils::{display_error, opt_display::OptDisplay}, + utils::{display_error, opt_display::OptDisplay, ObservableFuse, Peel}, }; /// Success or failure response. @@ -471,7 +473,7 @@ fn set_log_filter(filter_str: &str) -> Result<(), SetLogFilterError> { async fn handler( effect_builder: EffectBuilder, stream: UnixStream, - mut shutdown_receiver: watch::Receiver<()>, + shutdown_fuse: ObservableFuse, ) -> io::Result<()> where REv: From @@ -488,14 +490,17 @@ where let mut keep_going = true; while keep_going { - let shutdown_messages = async { while shutdown_receiver.changed().await.is_ok() {} }; + let shutdown = shutdown_fuse.wait(); + pin_mut!(shutdown); + let next_line = lines.next_line(); + pin_mut!(next_line); - match future::select(Box::pin(shutdown_messages), Box::pin(lines.next_line())).await { + match future::select(shutdown, next_line).await.peel() { Either::Left(_) => { info!("shutting down diagnostics port connection to client"); return Ok(()); } - Either::Right((line_result, _)) => { + Either::Right(line_result) => { if let Some(line) = line_result? { keep_going = session .process_line(effect_builder, &mut writer, line.as_str()) @@ -516,7 +521,7 @@ pub(super) async fn server( effect_builder: EffectBuilder, socket_path: PathBuf, listener: UnixListener, - mut shutdown_receiver: watch::Receiver<()>, + shutdown_fuse: ObservableFuse, ) where REv: From + From @@ -524,8 +529,8 @@ pub(super) async fn server( + From + Send, { - let handling_shutdown_receiver = shutdown_receiver.clone(); let mut next_client_id: u64 = 0; + let acceptor_fuse = shutdown_fuse.clone(); let accept_connections = async move { loop { match listener.accept().await { @@ -541,8 +546,7 @@ pub(super) async fn server( next_client_id += 1; tokio::spawn( - handler(effect_builder, stream, handling_shutdown_receiver.clone()) - .instrument(span), + handler(effect_builder, stream, acceptor_fuse.clone()).instrument(span), ); } Err(err) => { @@ -552,11 +556,13 @@ pub(super) async fn server( } }; - let shutdown_messages = async move { while shutdown_receiver.changed().await.is_ok() {} }; + let shutdown = shutdown_fuse.wait(); + pin_mut!(shutdown); + pin_mut!(accept_connections); // Now we can wait for either the `shutdown` channel's remote end to do be dropped or the // infinite loop to terminate, which never happens. - match future::select(Box::pin(shutdown_messages), Box::pin(accept_connections)).await { + match future::select(shutdown, accept_connections).await { Either::Left(_) => info!("shutting down diagnostics port"), Either::Right(_) => unreachable!("server accept returns `!`"), } diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index ecd00ed7ea..f0bbaa4e38 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -27,11 +27,9 @@ mod tests; use std::{fmt::Debug, net::SocketAddr, path::PathBuf}; +use casper_json_rpc::{box_reply, CorsOrigin}; use datasize::DataSize; -use tokio::sync::{ - mpsc::{self, UnboundedSender}, - oneshot, -}; +use tokio::sync::mpsc::{self, UnboundedSender}; use tracing::{error, info, warn}; use warp::Filter; @@ -43,7 +41,7 @@ use crate::{ effect::{EffectBuilder, Effects}, reactor::main_reactor::MainEvent, types::JsonBlock, - utils::{self, ListeningError}, + utils::{self, ListeningError, ObservableFuse}, NodeRng, }; pub use config::Config; @@ -124,79 +122,35 @@ impl EventStreamServer { self.config.max_concurrent_subscribers, ); - let (server_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); + let shutdown_fuse = ObservableFuse::new(); let (sse_data_sender, sse_data_receiver) = mpsc::unbounded_channel(); - let listening_address = match self.config.cors_origin.as_str() { - "" => { - let (listening_address, server_with_shutdown) = warp::serve(sse_filter) - .try_bind_with_graceful_shutdown(required_address, async { - shutdown_receiver.await.ok(); - }) - .map_err(|error| ListeningError::Listen { - address: required_address, - error: Box::new(error), - })?; - - tokio::spawn(http_server::run( - self.config.clone(), - self.api_version, - server_with_shutdown, - server_shutdown_sender, - sse_data_receiver, - event_broadcaster, - new_subscriber_info_receiver, - )); - listening_address - } - "*" => { - let (listening_address, server_with_shutdown) = - warp::serve(sse_filter.with(warp::cors().allow_any_origin())) - .try_bind_with_graceful_shutdown(required_address, async { - shutdown_receiver.await.ok(); - }) - .map_err(|error| ListeningError::Listen { - address: required_address, - error: Box::new(error), - })?; - - tokio::spawn(http_server::run( - self.config.clone(), - self.api_version, - server_with_shutdown, - server_shutdown_sender, - sse_data_receiver, - event_broadcaster, - new_subscriber_info_receiver, - )); - listening_address - } - _ => { - let (listening_address, server_with_shutdown) = warp::serve( - sse_filter.with(warp::cors().allow_origin(self.config.cors_origin.as_str())), - ) - .try_bind_with_graceful_shutdown(required_address, async { - shutdown_receiver.await.ok(); - }) - .map_err(|error| ListeningError::Listen { - address: required_address, - error: Box::new(error), - })?; - - tokio::spawn(http_server::run( - self.config.clone(), - self.api_version, - server_with_shutdown, - server_shutdown_sender, - sse_data_receiver, - event_broadcaster, - new_subscriber_info_receiver, - )); - listening_address - } + let sse_filter = match CorsOrigin::parse_str(&self.config.cors_origin) { + Some(cors_origin) => sse_filter + .with(cors_origin.to_cors_builder().build()) + .map(box_reply) + .boxed(), + None => sse_filter.map(box_reply).boxed(), }; + let (listening_address, server_with_shutdown) = warp::serve(sse_filter) + .try_bind_with_graceful_shutdown(required_address, shutdown_fuse.clone().wait_owned()) + .map_err(|error| ListeningError::Listen { + address: required_address, + error: Box::new(error), + })?; + + tokio::spawn(http_server::run( + self.config.clone(), + self.api_version, + server_with_shutdown, + shutdown_fuse, + sse_data_receiver, + event_broadcaster, + new_subscriber_info_receiver, + )); + info!(address=%listening_address, "started event stream server"); let event_indexer = EventIndexer::new(self.storage_path.clone()); @@ -257,7 +211,18 @@ where } ComponentState::Initializing => match event { Event::Initialize => { - let (effects, state) = self.bind(self.config.enable_server, _effect_builder); + let (effects, mut state) = + self.bind(self.config.enable_server, _effect_builder); + + if matches!(state, ComponentState::Initializing) { + // Our current code does not support storing the bound port, so we skip the + // second step and go straight to `Initialized`. If new tests are written + // that rely on an initialized RPC server with a port being available, this + // needs to be refactored. Compare with the REST server on how this could be + // done. + state = ComponentState::Initialized; + } + >::set_state(self, state); effects } diff --git a/node/src/components/event_stream_server/http_server.rs b/node/src/components/event_stream_server/http_server.rs index 1712f50ff1..66098c5501 100644 --- a/node/src/components/event_stream_server/http_server.rs +++ b/node/src/components/event_stream_server/http_server.rs @@ -1,7 +1,7 @@ use futures::{future, Future, FutureExt}; use tokio::{ select, - sync::{broadcast, mpsc, oneshot}, + sync::{broadcast, mpsc}, task, }; use tracing::{info, trace}; @@ -9,6 +9,8 @@ use wheelbuf::WheelBuf; use casper_types::ProtocolVersion; +use crate::utils::{Fuse, ObservableFuse}; + use super::{ sse_server::{BroadcastChannelMessage, Id, NewSubscriberInfo, ServerSentEvent}, Config, EventIndex, SseData, @@ -17,7 +19,7 @@ use super::{ /// Run the HTTP server. /// /// * `server_with_shutdown` is the actual server as a future which can be gracefully shut down. -/// * `server_shutdown_sender` is the channel by which the server will be notified to shut down. +/// * `shutdown_fuse` is the fuse by which the server will be notified to shut down. /// * `data_receiver` will provide the server with local events which should then be sent to all /// subscribed clients. /// * `broadcaster` is used by the server to send events to each subscribed client after receiving @@ -29,7 +31,7 @@ pub(super) async fn run( config: Config, api_version: ProtocolVersion, server_with_shutdown: impl Future + Send + 'static, - server_shutdown_sender: oneshot::Sender<()>, + shutdown_fuse: ObservableFuse, mut data_receiver: mpsc::UnboundedReceiver<(EventIndex, SseData)>, broadcaster: broadcast::Sender, mut new_subscriber_info_receiver: mpsc::UnboundedReceiver, @@ -117,7 +119,7 @@ pub(super) async fn run( // Kill the event-stream handlers, and shut down the server. let _ = broadcaster.send(BroadcastChannelMessage::Shutdown); - let _ = server_shutdown_sender.send(()); + shutdown_fuse.set(); trace!("Event stream server stopped"); } diff --git a/node/src/components/fetcher/metrics.rs b/node/src/components/fetcher/metrics.rs index 35c403d633..755e901355 100644 --- a/node/src/components/fetcher/metrics.rs +++ b/node/src/components/fetcher/metrics.rs @@ -1,62 +1,46 @@ use prometheus::{IntCounter, Registry}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; #[derive(Debug)] pub(crate) struct Metrics { /// Number of fetch requests that found an item in the storage. - pub found_in_storage: IntCounter, + pub found_in_storage: RegisteredMetric, /// Number of fetch requests that fetched an item from peer. - pub found_on_peer: IntCounter, + pub found_on_peer: RegisteredMetric, /// Number of fetch requests that timed out. - pub timeouts: IntCounter, + pub timeouts: RegisteredMetric, /// Number of total fetch requests made. - pub fetch_total: IntCounter, - /// Reference to the registry for unregistering. - registry: Registry, + pub fetch_total: RegisteredMetric, } impl Metrics { pub(super) fn new(name: &str, registry: &Registry) -> Result { - let found_in_storage = IntCounter::new( + let found_in_storage = registry.new_int_counter( format!("{}_found_in_storage", name), format!( "number of fetch requests that found {} in local storage", name ), )?; - let found_on_peer = IntCounter::new( + let found_on_peer = registry.new_int_counter( format!("{}_found_on_peer", name), format!("number of fetch requests that fetched {} from peer", name), )?; - let timeouts = IntCounter::new( + let timeouts = registry.new_int_counter( format!("{}_timeouts", name), format!("number of {} fetch requests that timed out", name), )?; - let fetch_total = IntCounter::new( + let fetch_total = registry.new_int_counter( format!("{}_fetch_total", name), format!("number of {} all fetch requests made", name), )?; - registry.register(Box::new(found_in_storage.clone()))?; - registry.register(Box::new(found_on_peer.clone()))?; - registry.register(Box::new(timeouts.clone()))?; - registry.register(Box::new(fetch_total.clone()))?; Ok(Metrics { found_in_storage, found_on_peer, timeouts, fetch_total, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.found_in_storage); - unregister_metric!(self.registry, self.found_on_peer); - unregister_metric!(self.registry, self.timeouts); - unregister_metric!(self.registry, self.fetch_total); - } -} diff --git a/node/src/components/fetcher/tests.rs b/node/src/components/fetcher/tests.rs index 26fd08c3f0..e4f776054d 100644 --- a/node/src/components/fetcher/tests.rs +++ b/node/src/components/fetcher/tests.rs @@ -451,7 +451,7 @@ async fn assert_settled( rng: &mut TestRng, timeout: Duration, ) { - let has_responded = |_nodes: &HashMap>>| { + let has_responded = |_nodes: &HashMap>>>| { fetched.lock().unwrap().0 }; diff --git a/node/src/components/gossiper.rs b/node/src/components/gossiper.rs index 7fd0aaa486..8096e73027 100644 --- a/node/src/components/gossiper.rs +++ b/node/src/components/gossiper.rs @@ -597,7 +597,11 @@ where Event::CheckGetFromPeerTimeout { item_id, peer } => { self.check_get_from_peer_timeout(effect_builder, item_id, peer) } - Event::Incoming(GossiperIncoming:: { sender, message }) => match *message { + Event::Incoming(GossiperIncoming:: { + sender, + message, + ticket: _, // TODO: Sensibly process ticket. + }) => match *message { Message::Gossip(item_id) => { Self::is_stored(effect_builder, item_id.clone()).event(move |result| { Event::IsStoredResult { @@ -700,7 +704,11 @@ where error!(%item_id, %peer, "should not timeout getting small item from peer"); Effects::new() } - Event::Incoming(GossiperIncoming:: { sender, message }) => match *message { + Event::Incoming(GossiperIncoming:: { + sender, + message, + ticket: _, // TODO: Properly handle `ticket`. + }) => match *message { Message::Gossip(item_id) => { let target = ::id_as_item(&item_id).gossip_target(); let action = self.table.new_complete_data(&item_id, Some(sender), target); diff --git a/node/src/components/gossiper/metrics.rs b/node/src/components/gossiper/metrics.rs index 2bf9d2e900..90352a4cfb 100644 --- a/node/src/components/gossiper/metrics.rs +++ b/node/src/components/gossiper/metrics.rs @@ -1,50 +1,48 @@ use prometheus::{IntCounter, IntGauge, Registry}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Metrics for the gossiper component. #[derive(Debug)] pub(super) struct Metrics { /// Total number of items received by the gossiper. - pub(super) items_received: IntCounter, + pub(super) items_received: RegisteredMetric, /// Total number of gossip requests sent to peers. - pub(super) times_gossiped: IntCounter, + pub(super) times_gossiped: RegisteredMetric, /// Number of times the process had to pause due to running out of peers. - pub(super) times_ran_out_of_peers: IntCounter, + pub(super) times_ran_out_of_peers: RegisteredMetric, /// Number of items in the gossip table that are currently being gossiped. - pub(super) table_items_current: IntGauge, + pub(super) table_items_current: RegisteredMetric, /// Number of items in the gossip table that are finished. - pub(super) table_items_finished: IntGauge, - /// Reference to the registry for unregistering. - registry: Registry, + pub(super) table_items_finished: RegisteredMetric, } impl Metrics { /// Creates a new instance of gossiper metrics, using the given prefix. pub fn new(name: &str, registry: &Registry) -> Result { - let items_received = IntCounter::new( + let items_received = registry.new_int_counter( format!("{}_items_received", name), format!("number of items received by the {}", name), )?; - let times_gossiped = IntCounter::new( + let times_gossiped = registry.new_int_counter( format!("{}_times_gossiped", name), format!("number of times the {} sent gossip requests to peers", name), )?; - let times_ran_out_of_peers = IntCounter::new( + let times_ran_out_of_peers = registry.new_int_counter( format!("{}_times_ran_out_of_peers", name), format!( "number of times the {} ran out of peers and had to pause", name ), )?; - let table_items_current = IntGauge::new( + let table_items_current = registry.new_int_gauge( format!("{}_table_items_current", name), format!( "number of items in the gossip table of {} in state current", name ), )?; - let table_items_finished = IntGauge::new( + let table_items_finished = registry.new_int_gauge( format!("{}_table_items_finished", name), format!( "number of items in the gossip table of {} in state finished", @@ -52,29 +50,12 @@ impl Metrics { ), )?; - registry.register(Box::new(items_received.clone()))?; - registry.register(Box::new(times_gossiped.clone()))?; - registry.register(Box::new(times_ran_out_of_peers.clone()))?; - registry.register(Box::new(table_items_current.clone()))?; - registry.register(Box::new(table_items_finished.clone()))?; - Ok(Metrics { items_received, times_gossiped, times_ran_out_of_peers, table_items_current, table_items_finished, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.items_received); - unregister_metric!(self.registry, self.times_gossiped); - unregister_metric!(self.registry, self.times_ran_out_of_peers); - unregister_metric!(self.registry, self.table_items_current); - unregister_metric!(self.registry, self.table_items_finished); - } -} diff --git a/node/src/components/gossiper/tests.rs b/node/src/components/gossiper/tests.rs index e8b8a47468..f859cafd3f 100644 --- a/node/src/components/gossiper/tests.rs +++ b/node/src/components/gossiper/tests.rs @@ -24,7 +24,7 @@ use crate::{ components::{ deploy_acceptor, in_memory_network::{self, InMemoryNetwork, NetworkController}, - network::{GossipedAddress, Identity as NetworkIdentity}, + network::{GossipedAddress, Identity as NetworkIdentity, Ticket}, storage::{self, Storage}, }, effect::{ @@ -357,12 +357,13 @@ async fn run_gossip(rng: &mut TestRng, network_size: usize, deploy_count: usize) } // Check every node has every deploy stored locally. - let all_deploys_held = |nodes: &HashMap>>| { - nodes.values().all(|runner| { - let hashes = runner.reactor().inner().storage.get_all_deploy_hashes(); - all_deploy_hashes == hashes - }) - }; + let all_deploys_held = + |nodes: &HashMap>>>| { + nodes.values().all(|runner| { + let hashes = runner.reactor().inner().storage.get_all_deploy_hashes(); + all_deploy_hashes == hashes + }) + }; network.settle_on(rng, all_deploys_held, TIMEOUT).await; // Ensure all responders are called before dropping the network. @@ -445,7 +446,7 @@ async fn should_get_from_alternate_source() { testing::advance_time(duration_to_advance.into()).await; // Check node 0 has the deploy stored locally. - let deploy_held = |nodes: &HashMap>>| { + let deploy_held = |nodes: &HashMap>>>| { let runner = nodes.get(&node_ids[2]).unwrap(); runner .reactor() @@ -514,7 +515,7 @@ async fn should_timeout_gossip_response() { testing::advance_time(duration_to_advance.into()).await; // Check every node has every deploy stored locally. - let deploy_held = |nodes: &HashMap>>| { + let deploy_held = |nodes: &HashMap>>>| { nodes.values().all(|runner| { runner .reactor() @@ -631,6 +632,7 @@ async fn should_not_gossip_old_stored_item_again() { let event = Event::DeployGossiperIncoming(GossiperIncoming { sender: node_ids[1], message: Box::new(Message::Gossip(deploy.gossip_id())), + ticket: Arc::new(Ticket::create_dummy()), }); effect_builder .into_inner() @@ -703,6 +705,7 @@ async fn should_ignore_unexpected_message(message_type: Unexpected) { let event = Event::DeployGossiperIncoming(GossiperIncoming { sender: node_ids[1], message: Box::new(message), + ticket: Arc::new(Ticket::create_dummy()), }); effect_builder .into_inner() diff --git a/node/src/components/in_memory_network.rs b/node/src/components/in_memory_network.rs index 5f0d9b99a8..d1b3f02a07 100644 --- a/node/src/components/in_memory_network.rs +++ b/node/src/components/in_memory_network.rs @@ -284,15 +284,14 @@ use std::{ sync::{Arc, RwLock}, }; +use casper_types::testing::TestRng; use rand::seq::IteratorRandom; use serde::Serialize; use tokio::sync::mpsc::{self, error::SendError}; use tracing::{debug, error, info, warn}; -use casper_types::testing::TestRng; - use crate::{ - components::Component, + components::{network::Ticket, Component}, effect::{requests::NetworkRequest, EffectBuilder, EffectExt, Effects}, logging, reactor::{EventQueueHandle, QueueKind}, @@ -538,8 +537,7 @@ where NetworkRequest::SendMessage { dest, payload, - respond_after_queueing: _, - auto_closing_responder, + message_queued_responder, } => { if *dest == self.node_id { panic!("can't send message to self"); @@ -551,7 +549,11 @@ where error!("network lock has been poisoned") }; - auto_closing_responder.respond(()).ignore() + if let Some(responder) = message_queued_responder { + responder.respond(()).ignore() + } else { + Effects::new() + } } NetworkRequest::ValidatorBroadcast { payload, @@ -609,10 +611,11 @@ async fn receiver_task( P: 'static + Send, { while let Some((sender, payload)) = receiver.recv().await { - let announce: REv = REv::from_incoming(sender, payload); + // We do not use backpressure in the in-memory network, so provide a dummy ticket. + let announce: REv = REv::from_incoming(sender, payload, Ticket::create_dummy()); event_queue - .schedule(announce, QueueKind::NetworkIncoming) + .schedule(announce, QueueKind::MessageIncoming) .await; } diff --git a/node/src/components/metrics.rs b/node/src/components/metrics.rs index acd6ba0987..505d7b8e32 100644 --- a/node/src/components/metrics.rs +++ b/node/src/components/metrics.rs @@ -14,9 +14,9 @@ //! Creation and instantiation of this component happens inside the `reactor::Reactor::new` //! function, which is passed in a `prometheus::Registry` (see 2.). //! -//! 2. Instantiation of an `XYZMetrics` struct should always be combined with registering all of -//! the metrics on a registry. For this reason it is advisable to have the `XYZMetrics::new` -//! method take a `prometheus::Registry` and register it directly. +//! 2. Instantiation of an `XYZMetrics` struct should always be combined with registering all of the +//! metrics on a registry. For this reason it is advisable to have the `XYZMetrics::new` method +//! take a `prometheus::Registry` and register it directly. //! //! 3. Updating metrics is done inside the `handle_event` function by simply calling methods on the //! fields of `self.metrics` (`: XYZMetrics`). **Important**: Metrics should never be read to diff --git a/node/src/components/network.rs b/node/src/components/network.rs index 0ff785d6af..8ce0f32630 100644 --- a/node/src/components/network.rs +++ b/node/src/components/network.rs @@ -23,85 +23,84 @@ //! Nodes gossip their public listening addresses periodically, and will try to establish and //! maintain an outgoing connection to any new address learned. -mod bincode_format; pub(crate) mod blocklist; mod chain_info; mod config; -mod counting_format; +mod connection_id; mod error; mod event; mod gossiped_address; -mod health; +mod handshake; mod identity; mod insights; -mod limiter; mod message; -mod message_pack_format; mod metrics; mod outgoing; mod symmetry; pub(crate) mod tasks; #[cfg(test)] mod tests; +mod transport; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, fmt::{self, Debug, Display, Formatter}, - io, + fs::OpenOptions, + marker::PhantomData, net::{SocketAddr, TcpListener}, - sync::{Arc, Weak}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Weak, + }, time::{Duration, Instant}, }; +use bincode::Options; +use bytes::Bytes; use datasize::DataSize; use futures::{future::BoxFuture, FutureExt}; use itertools::Itertools; + +use juliet::rpc::{JulietRpcClient, JulietRpcServer, RequestGuard, RpcBuilder}; use prometheus::Registry; use rand::{ seq::{IteratorRandom, SliceRandom}, Rng, }; -use serde::{Deserialize, Serialize}; +use serde::Serialize; +use strum::EnumCount; use tokio::{ + io::{ReadHalf, WriteHalf}, net::TcpStream, - sync::{ - mpsc::{self, UnboundedSender}, - watch, - }, task::JoinHandle, }; use tokio_openssl::SslStream; -use tokio_util::codec::LengthDelimitedCodec; use tracing::{debug, error, info, trace, warn, Instrument, Span}; use casper_types::{EraId, PublicKey, SecretKey}; -pub(crate) use self::{ - bincode_format::BincodeFormat, - config::{Config, IdentityConfig}, - error::Error, - event::Event, - gossiped_address::GossipedAddress, - identity::Identity, - insights::NetworkInsights, - message::{ - generate_largest_serialized_message, EstimatorWeights, FromIncoming, Message, MessageKind, - Payload, - }, -}; use self::{ blocklist::BlocklistJustification, chain_info::ChainInfo, - counting_format::{ConnectionId, CountingFormat, Role}, - error::{ConnectionError, Result}, + error::{ConnectionError, MessageReceiverError}, event::{IncomingConnection, OutgoingConnection}, - health::{HealthConfig, TaggedTimestamp}, - limiter::Limiter, message::NodeKeyPair, metrics::Metrics, outgoing::{DialOutcome, DialRequest, OutgoingConfig, OutgoingManager}, symmetry::ConnectionSymmetry, - tasks::{MessageQueueItem, NetworkContext}, + tasks::NetworkContext, +}; +pub(crate) use self::{ + config::Config, + error::Error, + event::Event, + gossiped_address::GossipedAddress, + identity::Identity, + insights::NetworkInsights, + message::{ + generate_largest_serialized_message, Channel, FromIncoming, Message, MessageKind, Payload, + }, + transport::Ticket, }; use crate::{ components::{gossiper::GossipItem, Component, ComponentState, InitializedComponent}, @@ -113,10 +112,15 @@ use crate::{ reactor::{Finalize, ReactorEvent}, tls, types::{NodeId, ValidatorMatrix}, - utils::{self, display_error, Source}, + utils::{ + self, display_error, DropSwitch, Fuse, LockedLineWriter, ObservableFuse, Source, + TokenizedCount, + }, NodeRng, }; +use super::ValidatorBoundComponent; + const COMPONENT_NAME: &str = "network"; const MAX_METRICS_DROP_ATTEMPTS: usize = 25; @@ -134,28 +138,14 @@ const BASE_RECONNECTION_TIMEOUT: Duration = Duration::from_secs(1); /// Interval during which to perform outgoing manager housekeeping. const OUTGOING_MANAGER_SWEEP_INTERVAL: Duration = Duration::from_secs(1); -/// How often to send a ping down a healthy connection. -const PING_INTERVAL: Duration = Duration::from_secs(30); - -/// Maximum time for a ping until it connections are severed. -/// -/// If you are running a network under very extreme conditions, it may make sense to alter these -/// values, but usually these values should require no changing. -/// -/// `PING_TIMEOUT` should be less than `PING_INTERVAL` at all times. -const PING_TIMEOUT: Duration = Duration::from_secs(6); - -/// How many pings to send before giving up and dropping the connection. -const PING_RETRIES: u16 = 5; - #[derive(Clone, DataSize, Debug)] -pub(crate) struct OutgoingHandle

{ +pub(crate) struct OutgoingHandle { #[data_size(skip)] // Unfortunately, there is no way to inspect an `UnboundedSender`. - sender: UnboundedSender>, + rpc_client: JulietRpcClient<{ Channel::COUNT }>, peer_addr: SocketAddr, } -impl

Display for OutgoingHandle

{ +impl Display for OutgoingHandle { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "outgoing handle to {}", self.peer_addr) } @@ -171,64 +161,47 @@ where cfg: Config, /// Read-only networking information shared across tasks. context: Arc>, + /// A reference to the global validator matrix. + validator_matrix: ValidatorMatrix, /// Outgoing connections manager. - outgoing_manager: OutgoingManager, ConnectionError>, + outgoing_manager: OutgoingManager, + /// Incoming validator map. + /// + /// Tracks which incoming connections are from validators. The atomic bool is shared with the + /// receiver tasks to determine queue position. + incoming_validator_status: HashMap>, /// Tracks whether a connection is symmetric or not. connection_symmetries: HashMap, - /// Tracks nodes that have announced themselves as nodes that are syncing. - syncing_nodes: HashSet, + /// Fuse signaling a shutdown of the small network. + shutdown_fuse: DropSwitch, - channel_management: Option, - - /// Networking metrics. + /// Join handle for the server thread. #[data_size(skip)] - net_metrics: Arc, + server_join_handle: Option>, - /// The outgoing bandwidth limiter. + /// Builder for new node-to-node RPC instances. #[data_size(skip)] - outgoing_limiter: Limiter, + rpc_builder: RpcBuilder<{ Channel::COUNT }>, - /// The limiter for incoming resource usage. - /// - /// This is not incoming bandwidth but an independent resource estimate. + /// Networking metrics. #[data_size(skip)] - incoming_limiter: Limiter, + net_metrics: Arc, /// The era that is considered the active era by the network component. active_era: EraId, /// The state of this component. state: ComponentState, -} - -#[derive(DataSize)] -struct ChannelManagement { - /// Channel signaling a shutdown of the network. - // Note: This channel is closed when `Network` is dropped, signalling the receivers that - // they should cease operation. - #[data_size(skip)] - shutdown_sender: Option>, - /// Join handle for the server thread. - #[data_size(skip)] - server_join_handle: Option>, - /// Channel signaling a shutdown of the incoming connections. - // Note: This channel is closed when we finished syncing, so the `Network` can close all - // connections. When they are re-established, the proper value of the now updated `is_syncing` - // flag will be exchanged on handshake. - #[data_size(skip)] - close_incoming_sender: Option>, - /// Handle used by the `message_reader` task to receive a notification that incoming - /// connections should be closed. - #[data_size(skip)] - close_incoming_receiver: watch::Receiver<()>, + /// Marker for what kind of payload this small network instance supports. + _payload: PhantomData

, } impl Network where - P: Payload + 'static, + P: Payload, REv: ReactorEvent + From> + FromIncoming

@@ -246,64 +219,72 @@ where registry: &Registry, chain_info_source: C, validator_matrix: ValidatorMatrix, - ) -> Result> { + ) -> Result, Error> { let net_metrics = Arc::new(Metrics::new(registry)?); - let outgoing_limiter = Limiter::new( - cfg.max_outgoing_byte_rate_non_validators, - net_metrics.accumulated_outgoing_limiter_delay.clone(), - validator_matrix.clone(), - ); - - let incoming_limiter = Limiter::new( - cfg.max_incoming_message_rate_non_validators, - net_metrics.accumulated_incoming_limiter_delay.clone(), - validator_matrix, - ); - let outgoing_manager = OutgoingManager::with_metrics( OutgoingConfig { retry_attempts: RECONNECTION_ATTEMPTS, base_timeout: BASE_RECONNECTION_TIMEOUT, unblock_after: cfg.blocklist_retain_duration.into(), sweep_timeout: cfg.max_addr_pending_time.into(), - health: HealthConfig { - ping_interval: PING_INTERVAL, - ping_timeout: PING_TIMEOUT, - ping_retries: PING_RETRIES, - pong_limit: (1 + PING_RETRIES as u32) * 2, - }, }, net_metrics.create_outgoing_metrics(), ); + let keylog = match cfg.keylog_path { + Some(ref path) => { + let keylog = OpenOptions::new() + .append(true) + .create(true) + .write(true) + .open(path) + .map_err(Error::CannotAppendToKeylog)?; + warn!(%path, "keylog enabled, if you are not debugging turn this off in your configuration (`network.keylog_path`)"); + Some(LockedLineWriter::new(keylog)) + } + None => None, + }; + + let chain_info = chain_info_source.into(); + let rpc_builder = transport::create_rpc_builder( + chain_info.maximum_net_message_size, + cfg.max_in_flight_demands, + ); + let context = Arc::new(NetworkContext::new( cfg.clone(), our_identity, + keylog, node_key_pair.map(NodeKeyPair::new), - chain_info_source.into(), + chain_info, &net_metrics, )); let component = Network { cfg, context, + validator_matrix, outgoing_manager, + incoming_validator_status: Default::default(), connection_symmetries: HashMap::new(), - syncing_nodes: HashSet::new(), - channel_management: None, net_metrics, - outgoing_limiter, - incoming_limiter, // We start with an empty set of validators for era 0 and expect to be updated. active_era: EraId::new(0), state: ComponentState::Uninitialized, + shutdown_fuse: DropSwitch::new(ObservableFuse::new()), + server_join_handle: None, + rpc_builder, + _payload: PhantomData, }; Ok(component) } - fn initialize(&mut self, effect_builder: EffectBuilder) -> Result>> { + fn initialize( + &mut self, + effect_builder: EffectBuilder, + ) -> Result>, Error> { let mut known_addresses = HashSet::new(); for address in &self.cfg.known_addresses { match utils::resolve_address(address) { @@ -354,27 +335,15 @@ where // which we need to shutdown cleanly later on. info!(%local_addr, %public_addr, %protocol_version, "starting server background task"); - let (server_shutdown_sender, server_shutdown_receiver) = watch::channel(()); - let (close_incoming_sender, close_incoming_receiver) = watch::channel(()); - let context = self.context.clone(); - let server_join_handle = tokio::spawn( + self.server_join_handle = Some(tokio::spawn( tasks::server( context, tokio::net::TcpListener::from_std(listener).map_err(Error::ListenerConversion)?, - server_shutdown_receiver, + self.shutdown_fuse.inner().clone(), ) .in_current_span(), - ); - - let channel_management = ChannelManagement { - shutdown_sender: Some(server_shutdown_sender), - server_join_handle: Some(server_join_handle), - close_incoming_sender: Some(close_incoming_sender), - close_incoming_receiver, - }; - - self.channel_management = Some(channel_management); + )); // Learn all known addresses and mark them as unforgettable. let now = Instant::now(); @@ -403,13 +372,6 @@ where Ok(effects) } - /// Should only be called after component has been initialized. - fn channel_management(&self) -> &ChannelManagement { - self.channel_management - .as_ref() - .expect("component not initialized properly") - } - /// Queues a message to be sent to validator nodes in the given era. fn broadcast_message_to_validators(&self, msg: Arc>, era_id: EraId) { self.net_metrics.broadcast_requests.inc(); @@ -419,7 +381,8 @@ where for peer_id in self.outgoing_manager.connected_peers() { total_outgoing_manager_connected_peers += 1; - if self.outgoing_limiter.is_validator_in_era(era_id, &peer_id) { + + if true { total_connected_validators_in_era += 1; self.send_message(peer_id, msg.clone(), None) } @@ -439,12 +402,15 @@ where &self, rng: &mut NodeRng, msg: Arc>, - gossip_target: GossipTarget, + _gossip_target: GossipTarget, count: usize, exclude: HashSet, ) -> HashSet { - let is_validator_in_era = - |era: EraId, peer_id: &NodeId| self.outgoing_limiter.is_validator_in_era(era, peer_id); + // TODO: Restore sampling functionality. We currently override with `GossipTarget::All`. + // See #4247. + let is_validator_in_era = |_, _: &_| true; + let gossip_target = GossipTarget::All; + let peer_ids = choose_gossip_peers( rng, gossip_target, @@ -488,23 +454,96 @@ where &self, dest: NodeId, msg: Arc>, - opt_responder: Option>, + message_queued_responder: Option>, ) { // Try to send the message. if let Some(connection) = self.outgoing_manager.get_route(dest) { - if msg.payload_is_unsafe_for_syncing_nodes() && self.syncing_nodes.contains(&dest) { - // We should never attempt to send an unsafe message to a peer that we know is still - // syncing. Since "unsafe" does usually not mean immediately catastrophic, we - // attempt to carry on, but warn loudly. - error!(kind=%msg.classify(), node_id=%dest, "sending unsafe message to syncing node"); - } + let channel = msg.get_channel(); - if let Err(msg) = connection.sender.send((msg, opt_responder)) { - // We lost the connection, but that fact has not reached us yet. - warn!(our_id=%self.context.our_id(), %dest, ?msg, "dropped outgoing message, lost connection"); + let payload = if let Some(payload) = serialize_network_message(&msg) { + payload } else { - self.net_metrics.queued_messages.inc(); + // No need to log, `serialize_network_message` already logs the failure. + return; + }; + trace!(%msg, encoded_size=payload.len(), %channel, "enqueing message for sending"); + + /// Build the request. + /// + /// Internal helper function to ensure requests are always built the same way. + // Note: Ideally, this would be a closure, but lifetime inference does not + // work out here, and we cannot annotate lifetimes on closures. + #[inline(always)] + fn mk_request( + rpc_client: &JulietRpcClient<{ Channel::COUNT }>, + channel: Channel, + payload: Bytes, + ) -> juliet::rpc::JulietRpcRequestBuilder<'_, { Channel::COUNT }> { + rpc_client + .create_request(channel.into_channel_id()) + .with_payload(payload) + .with_timeout(Duration::from_secs(30)) + } + + let request = mk_request(&connection.rpc_client, channel, payload); + + // Attempt to enqueue it directly, regardless of what `message_queued_responder` is. + match request.try_queue_for_sending() { + Ok(guard) => process_request_guard(channel, guard), + Err(builder) => { + // Failed to queue immediately, our next step depends on whether we were asked + // to keep trying or to discard. + + // Reconstruct the payload. + let payload = match builder.into_payload() { + None => { + // This should never happen. + error!("payload unexpectedly disappeard"); + return; + } + Some(payload) => payload, + }; + + if let Some(responder) = message_queued_responder { + // Reconstruct the client. + let client = connection.rpc_client.clone(); + + // Technically, the queueing future should be spawned by the reactor, but + // since the networking component usually controls its own futures, we are + // allowed to spawn these as well. + tokio::spawn(async move { + let guard = mk_request(&client, channel, payload) + .queue_for_sending() + .await; + responder.respond(()).await; + + // We need to properly process the guard, so it does not cause a + // cancellation from being dropped. + process_request_guard(channel, guard) + }); + } else { + // We had to drop the message, since we hit the buffer limit. + debug!(%channel, "node is sending at too high a rate, message dropped"); + + match deserialize_network_message::

(&payload) { + Ok(reconstructed_message) => { + debug!(our_id=%self.context.our_id(), %dest, msg=%reconstructed_message, "dropped outgoing message, buffer exhausted"); + } + Err(err) => { + error!(our_id=%self.context.our_id(), + %dest, + reconstruction_error=%err, + ?payload, + "dropped outgoing message, buffer exhausted and also failed to reconstruct it" + ); + } + } + } + } } + + let _send_token = TokenizedCount::new(self.net_metrics.queued_messages.inner().clone()); + // TODO: How to update self.net_metrics.queued_messages? Or simply remove metric? } else { // We are not connected, so the reconnection is likely already in progress. debug!(our_id=%self.context.our_id(), %dest, ?msg, "dropped outgoing message, no connection"); @@ -513,7 +552,7 @@ where fn handle_incoming_connection( &mut self, - incoming: Box>, + incoming: Box, span: Span, ) -> Effects> { span.clone().in_scope(|| match *incoming { @@ -550,7 +589,7 @@ where public_addr, peer_id, peer_consensus_public_key, - stream, + transport, } => { if self.cfg.max_incoming_peer_connections != 0 { if let Some(symmetries) = self.connection_symmetries.get(&peer_id) { @@ -599,24 +638,71 @@ where // connection after a peer has closed the corresponding incoming connection. } + // If given a key, determine validator status. + let validator_status = peer_consensus_public_key + .as_ref() + .map(|public_key| { + let status = self + .validator_matrix + .is_active_or_upcoming_validator(public_key); + + // Find the shared `Arc` that holds validator status for this specific key. + match self.incoming_validator_status.entry((**public_key).clone()) { + // TODO: Use `Arc` for public key-key. + Entry::Occupied(mut occupied) => { + match occupied.get().upgrade() { + Some(arc) => { + arc.store(status, Ordering::Relaxed); + arc + } + None => { + // Failed to ugprade, the weak pointer is just a leftover + // that has not been cleaned up yet. We can replace it. + let arc = Arc::new(AtomicBool::new(status)); + occupied.insert(Arc::downgrade(&arc)); + arc + } + } + } + Entry::Vacant(vacant) => { + let arc = Arc::new(AtomicBool::new(status)); + vacant.insert(Arc::downgrade(&arc)); + arc + } + } + }) + .unwrap_or_else(|| Arc::new(AtomicBool::new(false))); + + let (read_half, write_half) = tokio::io::split(transport); + + let (rpc_client, rpc_server) = self.rpc_builder.build(read_half, write_half); + // Now we can start the message reader. let boxed_span = Box::new(span.clone()); effects.extend( - tasks::message_reader( + tasks::message_receiver( self.context.clone(), - stream, - self.incoming_limiter - .create_handle(peer_id, peer_consensus_public_key), - self.channel_management().close_incoming_receiver.clone(), + validator_status, + rpc_server, + self.shutdown_fuse.inner().clone(), peer_id, span.clone(), ) .instrument(span) - .event(move |result| Event::IncomingClosed { - result, - peer_id: Box::new(peer_id), - peer_addr, - span: boxed_span, + .event(move |result| { + // By moving the `rpc_client` into this closure to drop it, we ensure it + // does not get dropped until after `tasks::message_receiver` has returned. + // This is important because dropping `rpc_client` is one of the ways to + // trigger a connection shutdown from our end. + drop(rpc_client); + + Event::IncomingClosed { + result: result.map_err(Box::new), + peer_id: Box::new(peer_id), + peer_addr, + peer_consensus_public_key, + span: boxed_span, + } }), ); @@ -627,9 +713,10 @@ where fn handle_incoming_closed( &mut self, - result: io::Result<()>, + result: Result<(), Box>, peer_id: Box, peer_addr: SocketAddr, + peer_consensus_public_key: Option>, span: Span, ) -> Effects> { span.in_scope(|| { @@ -643,11 +730,19 @@ where } } - // Update the connection symmetries. - self.connection_symmetries + // Update the connection symmetries and cleanup if necessary. + if !self + .connection_symmetries .entry(*peer_id) - .or_default() - .remove_incoming(peer_addr, Instant::now()); + .or_default() // Should never occur. + .remove_incoming(peer_addr, Instant::now()) + { + if let Some(ref public_key) = peer_consensus_public_key { + self.incoming_validator_status.remove(public_key); + } + + self.connection_symmetries.remove(&peer_id); + } Effects::new() }) @@ -669,11 +764,11 @@ where | ConnectionError::TlsHandshake(_) | ConnectionError::HandshakeSend(_) | ConnectionError::HandshakeRecv(_) - | ConnectionError::IncompatibleVersion(_) => None, + | ConnectionError::IncompatibleVersion(_) + | ConnectionError::HandshakeTimeout => None, // These errors are potential bugs on our side. ConnectionError::HandshakeSenderCrashed(_) - | ConnectionError::FailedToReuniteHandshakeSinkAndStream | ConnectionError::CouldNotEncodeOurHandshake(_) => None, // These could be candidates for blocking, but for now we decided not to. @@ -706,7 +801,7 @@ where #[allow(clippy::redundant_clone)] fn handle_outgoing_connection( &mut self, - outgoing: OutgoingConnection

, + outgoing: OutgoingConnection, span: Span, ) -> Effects> { let now = Instant::now(); @@ -753,14 +848,19 @@ where OutgoingConnection::Established { peer_addr, peer_id, - peer_consensus_public_key, - sink, - is_syncing, + peer_consensus_public_key: _, // TODO: Use for limiting or remove. See also #4247. + transport, } => { info!("new outgoing connection established"); - let (sender, receiver) = mpsc::unbounded_channel(); - let handle = OutgoingHandle { sender, peer_addr }; + let (read_half, write_half) = tokio::io::split(transport); + + let (rpc_client, rpc_server) = self.rpc_builder.build(read_half, write_half); + + let handle = OutgoingHandle { + rpc_client, + peer_addr, + }; let request = self .outgoing_manager @@ -768,7 +868,6 @@ where addr: peer_addr, handle, node_id: peer_id, - when: now, }); let mut effects = self.process_dial_requests(request); @@ -781,23 +880,14 @@ where .mark_outgoing(now) { self.connection_completed(peer_id); - self.update_syncing_nodes_set(peer_id, is_syncing); } - effects.extend( - tasks::message_sender( - receiver, - sink, - self.outgoing_limiter - .create_handle(peer_id, peer_consensus_public_key), - self.net_metrics.queued_messages.clone(), - ) - .instrument(span) - .event(move |_| Event::OutgoingDropped { + effects.extend(tasks::rpc_sender_loop(rpc_server).instrument(span).event( + move |_| Event::OutgoingDropped { peer_id: Box::new(peer_id), peer_addr, - }), - ); + }, + )); effects } @@ -813,24 +903,18 @@ where NetworkRequest::SendMessage { dest, payload, - respond_after_queueing, - auto_closing_responder, + message_queued_responder, } => { // We're given a message to send. Pass on the responder so that confirmation // can later be given once the message has actually been buffered. self.net_metrics.direct_message_requests.inc(); - if respond_after_queueing { - self.send_message(*dest, Arc::new(Message::Payload(*payload)), None); - auto_closing_responder.respond(()).ignore() - } else { - self.send_message( - *dest, - Arc::new(Message::Payload(*payload)), - Some(auto_closing_responder), - ); - Effects::new() - } + self.send_message( + *dest, + Arc::new(Message::Payload(*payload)), + message_queued_responder, + ); + Effects::new() } NetworkRequest::ValidatorBroadcast { payload, @@ -875,15 +959,13 @@ where .or_default() .unmark_outgoing(Instant::now()); - self.outgoing_limiter.remove_connected_validator(&peer_id); - self.process_dial_requests(requests) } /// Processes a set of `DialRequest`s, updating the component and emitting needed effects. fn process_dial_requests(&mut self, requests: T) -> Effects> where - T: IntoIterator>>, + T: IntoIterator>, { let mut effects = Effects::new(); @@ -891,7 +973,7 @@ where trace!(%request, "processing dial request"); match request { DialRequest::Dial { addr, span } => effects.extend( - tasks::connect_outgoing(self.context.clone(), addr) + tasks::connect_outgoing::(self.context.clone(), addr) .instrument(span.clone()) .event(|outgoing| Event::OutgoingConnection { outgoing: Box::new(outgoing), @@ -904,14 +986,6 @@ where debug!("dropping connection, as requested"); }) } - DialRequest::SendPing { - peer_id, - nonce, - span, - } => span.in_scope(|| { - trace!("enqueuing ping to be sent"); - self.send_message(peer_id, Arc::new(Message::Ping { nonce }), None); - }), } } @@ -924,11 +998,13 @@ where effect_builder: EffectBuilder, peer_id: NodeId, msg: Message

, + ticket: Ticket, span: Span, ) -> Effects> where - REv: FromIncoming

+ From, + REv: FromIncoming

+ From> + From, { + // Note: For non-payload channels, we drop the `Ticket` implicitly at end of scope. span.in_scope(|| match msg { Message::Handshake { .. } => { // We should never receive a handshake message on an established connection. Simply @@ -937,29 +1013,9 @@ where warn!("received unexpected handshake"); Effects::new() } - Message::Ping { nonce } => { - // Send a pong. Incoming pings and pongs are rate limited. - - self.send_message(peer_id, Arc::new(Message::Pong { nonce }), None); - Effects::new() - } - Message::Pong { nonce } => { - // Record the time the pong arrived and forward it to outgoing. - let pong = TaggedTimestamp::from_parts(Instant::now(), nonce); - if self.outgoing_manager.record_pong(peer_id, pong) { - effect_builder - .announce_block_peer_with_justification( - peer_id, - BlocklistJustification::PongLimitExceeded, - ) - .ignore() - } else { - Effects::new() - } - } - Message::Payload(payload) => { - effect_builder.announce_incoming(peer_id, payload).ignore() - } + Message::Payload(payload) => effect_builder + .announce_incoming(peer_id, payload, ticket) + .ignore(), }) } @@ -969,19 +1025,6 @@ where self.net_metrics.peers.set(self.peers().len() as i64); } - /// Updates a set of known joining nodes. - /// If we've just connected to a non-joining node that peer will be removed from the set. - fn update_syncing_nodes_set(&mut self, peer_id: NodeId, is_syncing: bool) { - // Update set of syncing peers. - if is_syncing { - debug!(%peer_id, "is syncing"); - self.syncing_nodes.insert(peer_id); - } else { - debug!(%peer_id, "is no longer syncing"); - self.syncing_nodes.remove(&peer_id); - } - } - /// Returns the set of connected nodes. pub(crate) fn peers(&self) -> BTreeMap { let mut ret = BTreeMap::new(); @@ -1040,22 +1083,14 @@ where { fn finalize(mut self) -> BoxFuture<'static, ()> { async move { - if let Some(mut channel_management) = self.channel_management.take() { - // Close the shutdown socket, causing the server to exit. - drop(channel_management.shutdown_sender.take()); - drop(channel_management.close_incoming_sender.take()); - - // Wait for the server to exit cleanly. - if let Some(join_handle) = channel_management.server_join_handle.take() { - match join_handle.await { - Ok(_) => debug!(our_id=%self.context.our_id(), "server exited cleanly"), - Err(ref err) => { - error!( - our_id=%self.context.our_id(), - err=display_error(err), - "could not join server task cleanly" - ) - } + self.shutdown_fuse.inner().set(); + + // Wait for the server to exit cleanly. + if let Some(join_handle) = self.server_join_handle.take() { + match join_handle.await { + Ok(_) => debug!(our_id=%self.context.our_id(), "server exited cleanly"), + Err(ref err) => { + error!(our_id=%self.context.our_id(), err=display_error(err), "could not join server task cleanly") } } } @@ -1189,15 +1224,25 @@ where Event::IncomingConnection { incoming, span } => { self.handle_incoming_connection(incoming, span) } - Event::IncomingMessage { peer_id, msg, span } => { - self.handle_incoming_message(effect_builder, *peer_id, *msg, span) - } + Event::IncomingMessage { + peer_id, + msg, + span, + ticket, + } => self.handle_incoming_message(effect_builder, *peer_id, *msg, ticket, span), Event::IncomingClosed { result, peer_id, peer_addr, + peer_consensus_public_key, span, - } => self.handle_incoming_closed(result, peer_id, peer_addr, *span), + } => self.handle_incoming_closed( + result, + peer_id, + peer_addr, + peer_consensus_public_key, + *span, + ), Event::OutgoingConnection { outgoing, span } => { self.handle_outgoing_connection(*outgoing, span) } @@ -1247,7 +1292,7 @@ where } Event::SweepOutgoing => { let now = Instant::now(); - let requests = self.outgoing_manager.perform_housekeeping(rng, now); + let requests = self.outgoing_manager.perform_housekeeping(now); let mut effects = self.process_dial_requests(requests); @@ -1316,46 +1361,86 @@ where } } -/// Transport type alias for base encrypted connections. +impl ValidatorBoundComponent for Network +where + REv: ReactorEvent + + From> + + From> + + FromIncoming

+ + From + + From> + + From, + P: Payload, +{ + fn handle_validators( + &mut self, + _effect_builder: EffectBuilder, + _rng: &mut NodeRng, + ) -> Effects { + // If we receive an updated set of validators, recalculate validator status for every + // existing connection. + + let active_validators = self.validator_matrix.active_or_upcoming_validators(); + + // Update the validator status for every connection. + for (public_key, status) in self.incoming_validator_status.iter_mut() { + // If there is only a `Weak` ref, we lost the connection to the validator, but the + // disconnection has not reached us yet. + if let Some(arc) = status.upgrade() { + arc.store( + active_validators.contains(public_key), + std::sync::atomic::Ordering::Relaxed, + ) + } + } + + Effects::default() + } +} + +/// Transport type for base encrypted connections. type Transport = SslStream; -/// A framed transport for `Message`s. -pub(crate) type FullTransport

= tokio_serde::Framed< - FramedTransport, - Message

, - Arc>, - CountingFormat, +/// Transport-level RPC server. +type RpcServer = JulietRpcServer< + { Channel::COUNT }, + ReadHalf>, + WriteHalf>, >; -pub(crate) type FramedTransport = tokio_util::codec::Framed; +/// Setups bincode encoding used on the networking transport. +fn bincode_config() -> impl Options { + bincode::options() + .with_no_limit() // We rely on `juliet` to impose limits. + .with_little_endian() // Default at the time of this writing, we are merely pinning it. + .with_varint_encoding() // Same as above. + .reject_trailing_bytes() // There is no reason for us not to reject trailing bytes. +} -/// Constructs a new full transport on a stream. +/// Serializes a network message with the protocol specified encoding. /// -/// A full transport contains the framing as well as the encoding scheme used to send messages. -fn full_transport

( - metrics: Weak, - connection_id: ConnectionId, - framed: FramedTransport, - role: Role, -) -> FullTransport

+/// This function exists as a convenience, because there never should be a failure in serializing +/// messages we produced ourselves. +fn serialize_network_message(msg: &T) -> Option where - for<'de> P: Serialize + Deserialize<'de>, - for<'de> Message

: Serialize + Deserialize<'de>, + T: Serialize + ?Sized, { - tokio_serde::Framed::new( - framed, - CountingFormat::new(metrics, connection_id, role, BincodeFormat::default()), - ) + bincode_config() + .serialize(msg) + .map(Bytes::from) + .map_err(|err| { + error!(%err, "serialization failure when encoding outgoing message"); + err + }) + .ok() } -/// Constructs a framed transport. -fn framed_transport(transport: Transport, maximum_net_message_size: u32) -> FramedTransport { - tokio_util::codec::Framed::new( - transport, - LengthDelimitedCodec::builder() - .max_frame_length(maximum_net_message_size as usize) - .new_codec(), - ) +/// Deserializes a networking message from the protocol specified encoding. +fn deserialize_network_message

(bytes: &[u8]) -> Result, bincode::Error> +where + P: Payload, +{ + bincode_config().deserialize(bytes) } impl Debug for Network @@ -1373,6 +1458,26 @@ where } } +/// Processes a request guard obtained by making a request to a peer through Juliet RPC. +/// +/// Ensures that outgoing messages are not cancelled, a would be the case when simply dropping the +/// `RequestGuard`. Potential errors that are available early are dropped, later errors discarded. +#[inline] +fn process_request_guard(channel: Channel, guard: RequestGuard) { + match guard.try_get_response() { + Ok(Ok(_outcome)) => { + // We got an incredibly quick round-trip, lucky us! Nothing to do. + } + Ok(Err(err)) => { + debug!(%channel, %err, "failed to send message"); + } + Err(guard) => { + // No ACK received yet, forget, so we don't cancel. + guard.forget(); + } + } +} + #[cfg(test)] mod gossip_target_tests { use std::{collections::BTreeSet, iter}; diff --git a/node/src/components/network/bincode_format.rs b/node/src/components/network/bincode_format.rs deleted file mode 100644 index 0d6e47b344..0000000000 --- a/node/src/components/network/bincode_format.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Bincode wire format encoder. -//! -//! An encoder for `Bincode` messages with our specific settings pinned. - -use std::{fmt::Debug, io, pin::Pin, sync::Arc}; - -use bincode::{ - config::{ - RejectTrailing, VarintEncoding, WithOtherEndian, WithOtherIntEncoding, WithOtherLimit, - WithOtherTrailing, - }, - Options, -}; -use bytes::{Bytes, BytesMut}; -use serde::{Deserialize, Serialize}; -use tokio_serde::{Deserializer, Serializer}; - -use super::Message; - -/// bincode encoder/decoder for messages. -#[allow(clippy::type_complexity)] -pub struct BincodeFormat( - // Note: `bincode` encodes its options at the type level. The exact shape is determined by - // `BincodeFormat::default()`. - pub(crate) WithOtherTrailing< - WithOtherIntEncoding< - WithOtherEndian< - WithOtherLimit, - bincode::config::LittleEndian, - >, - VarintEncoding, - >, - RejectTrailing, - >, -); - -impl BincodeFormat { - /// Serializes an arbitrary serializable value with the networking bincode serializer. - #[inline] - pub(crate) fn serialize_arbitrary(&self, item: &T) -> io::Result> - where - T: Serialize, - { - self.0 - .serialize(item) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } -} - -impl Debug for BincodeFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("BincodeFormat") - } -} - -impl Default for BincodeFormat { - fn default() -> Self { - let opts = bincode::options() - .with_no_limit() // We rely on framed tokio transports to impose limits. - .with_little_endian() // Default at the time of this writing, we are merely pinning it. - .with_varint_encoding() // Same as above. - .reject_trailing_bytes(); // There is no reason for us not to reject trailing bytes. - BincodeFormat(opts) - } -} - -impl

Serializer>> for BincodeFormat -where - Message

: Serialize, -{ - type Error = io::Error; - - #[inline] - fn serialize(self: Pin<&mut Self>, item: &Arc>) -> Result { - let msg = &**item; - self.serialize_arbitrary(msg).map(Into::into) - } -} - -impl

Deserializer> for BincodeFormat -where - for<'de> Message

: Deserialize<'de>, -{ - type Error = io::Error; - - #[inline] - fn deserialize(self: Pin<&mut Self>, src: &BytesMut) -> Result, Self::Error> { - self.0 - .deserialize(src) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } -} diff --git a/node/src/components/network/blocklist.rs b/node/src/components/network/blocklist.rs index 1dfe232455..760e031845 100644 --- a/node/src/components/network/blocklist.rs +++ b/node/src/components/network/blocklist.rs @@ -37,8 +37,6 @@ pub(crate) enum BlocklistJustification { /// The era for which the invalid value was destined. era: EraId, }, - /// Too many unasked or expired pongs were sent by the peer. - PongLimitExceeded, /// Peer misbehaved during consensus and is blocked for it. BadConsensusBehavior, /// Peer is on the wrong network. @@ -76,9 +74,6 @@ impl Display for BlocklistJustification { BlocklistJustification::SentInvalidConsensusValue { era } => { write!(f, "sent an invalid consensus value in {}", era) } - BlocklistJustification::PongLimitExceeded => { - f.write_str("wrote too many expired or invalid pongs") - } BlocklistJustification::BadConsensusBehavior => { f.write_str("sent invalid data in consensus") } diff --git a/node/src/components/network/chain_info.rs b/node/src/components/network/chain_info.rs index 71e3349aad..ba0f17fe0f 100644 --- a/node/src/components/network/chain_info.rs +++ b/node/src/components/network/chain_info.rs @@ -10,7 +10,7 @@ use casper_types::ProtocolVersion; use datasize::DataSize; use super::{ - counting_format::ConnectionId, + connection_id::ConnectionId, message::{ConsensusCertificate, NodeKeyPair}, Message, }; @@ -51,7 +51,6 @@ impl ChainInfo { public_addr: SocketAddr, consensus_keys: Option<&NodeKeyPair>, connection_id: ConnectionId, - is_syncing: bool, ) -> Message

{ Message::Handshake { network_name: self.network_name.clone(), @@ -59,7 +58,6 @@ impl ChainInfo { protocol_version: self.protocol_version, consensus_certificate: consensus_keys .map(|key_pair| ConsensusCertificate::create(connection_id, key_pair)), - is_syncing, chainspec_hash: Some(self.chainspec_hash), } } diff --git a/node/src/components/network/config.rs b/node/src/components/network/config.rs index 217aeaab25..4e98802dd5 100644 --- a/node/src/components/network/config.rs +++ b/node/src/components/network/config.rs @@ -6,8 +6,6 @@ use casper_types::{ProtocolVersion, TimeDiff}; use datasize::DataSize; use serde::{Deserialize, Serialize}; -use super::EstimatorWeights; - /// Default binding address. /// /// Uses a fixed port per node, but binds on any interface. @@ -38,6 +36,7 @@ impl Default for Config { bind_address: DEFAULT_BIND_ADDRESS.to_string(), public_address: DEFAULT_PUBLIC_ADDRESS.to_string(), known_addresses: Vec::new(), + keylog_path: None, min_peers_for_initialization: DEFAULT_MIN_PEERS_FOR_INITIALIZATION, gossip_interval: DEFAULT_GOSSIP_INTERVAL, initial_gossip_delay: DEFAULT_INITIAL_GOSSIP_DELAY, @@ -46,7 +45,6 @@ impl Default for Config { max_incoming_peer_connections: 0, max_outgoing_byte_rate_non_validators: 0, max_incoming_message_rate_non_validators: 0, - estimator_weights: Default::default(), tarpit_version_threshold: None, tarpit_duration: TimeDiff::from_seconds(600), tarpit_chance: 0.2, @@ -83,6 +81,8 @@ pub struct Config { pub public_address: String, /// Known address of a node on the network used for joining. pub known_addresses: Vec, + /// If set, logs all TLS keys to this file. + pub keylog_path: Option, /// Minimum number of fully-connected peers to consider component initialized. pub min_peers_for_initialization: u16, /// Interval in milliseconds used for gossiping. @@ -99,8 +99,6 @@ pub struct Config { pub max_outgoing_byte_rate_non_validators: u32, /// Maximum of requests answered from non-validating peers. Unlimited if 0. pub max_incoming_message_rate_non_validators: u32, - /// Weight distribution for the payload impact estimator. - pub estimator_weights: EstimatorWeights, /// The protocol version at which (or under) tarpitting is enabled. pub tarpit_version_threshold: Option, /// If tarpitting is enabled, duration for which connections should be kept open. @@ -108,7 +106,7 @@ pub struct Config { /// The chance, expressed as a number between 0.0 and 1.0, of triggering the tarpit. pub tarpit_chance: f32, /// Maximum number of demands for objects that can be in-flight. - pub max_in_flight_demands: u32, + pub max_in_flight_demands: u16, /// Duration peers are kept on the block list, before being redeemed. pub blocklist_retain_duration: TimeDiff, /// Network identity configuration option. diff --git a/node/src/components/network/connection_id.rs b/node/src/components/network/connection_id.rs new file mode 100644 index 0000000000..43176f5bd6 --- /dev/null +++ b/node/src/components/network/connection_id.rs @@ -0,0 +1,130 @@ +//! Observability for network serialization/deserialization. +//! +//! This module introduces [`ConnectionId`], a unique ID per established connection that can be +//! independently derived by peers on either side of a connection. + +use openssl::ssl::SslRef; +#[cfg(test)] +use rand::RngCore; +use static_assertions::const_assert; +use tracing::warn; + +use casper_hashing::Digest; +#[cfg(test)] +use casper_types::testing::TestRng; + +use super::tls::KeyFingerprint; +use crate::{types::NodeId, utils}; + +/// An ID identifying a connection. +/// +/// The ID is guaranteed to be the same on both ends of the connection, but not guaranteed to be +/// unique or sufficiently random. Do not use it for any cryptographic/security related purposes. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(super) struct ConnectionId([u8; Digest::LENGTH]); + +// Invariant assumed by `ConnectionId`, `Digest` must be <= than `KeyFingerprint`. +const_assert!(KeyFingerprint::LENGTH >= Digest::LENGTH); +// We also assume it is at least 12 bytes. +const_assert!(Digest::LENGTH >= 12); + +/// Random data derived from TLS connections. +#[derive(Copy, Clone, Debug)] +pub(super) struct TlsRandomData { + /// Random data extract from the client of the connection. + combined_random: [u8; 12], +} + +/// Zero-randomness. +/// +/// Used to check random data. +const ZERO_RANDOMNESS: [u8; 12] = [0; 12]; + +impl TlsRandomData { + /// Collects random data from an existing SSL collection. + /// + /// Ideally we would use the TLS session ID, but it is not available on outgoing connections at + /// the times we need it. Instead, we use the `server_random` and `client_random` nonces, which + /// will be the same on both ends of the connection. + fn collect(ssl: &SslRef) -> Self { + // We are using only the first 12 bytes of these 32 byte values here, just in case we missed + // something in our assessment that hashing these should be safe. Additionally, these values + // are XOR'd, not concatenated. All this is done to prevent leaking information about these + // numbers. + // + // Some SSL implementations use timestamps for the first four bytes, so to be sufficiently + // random, we use 4 + 8 bytes of the nonces. + let mut server_random = [0; 12]; + let mut client_random = [0; 12]; + + ssl.server_random(&mut server_random); + + if server_random == ZERO_RANDOMNESS { + warn!("TLS server random is all zeros"); + } + + ssl.client_random(&mut client_random); + + if client_random == ZERO_RANDOMNESS { + warn!("TLS client random is all zeros"); + } + + // Combine using XOR. + utils::xor(&mut server_random, &client_random); + + Self { + combined_random: server_random, + } + } + + /// Creates random `TlsRandomData`. + #[cfg(test)] + fn random(rng: &mut TestRng) -> Self { + let mut buffer = [0u8; 12]; + + rng.fill_bytes(&mut buffer); + + Self { + combined_random: buffer, + } + } +} + +impl ConnectionId { + /// Creates a new connection ID, based on random values from server and client, as well as + /// node IDs. + fn create(random_data: TlsRandomData, our_id: NodeId, their_id: NodeId) -> ConnectionId { + // Hash the resulting random values. + let mut id = Digest::hash(random_data.combined_random).value(); + + // We XOR in a hashes of server and client fingerprint, to ensure that in the case of an + // accidental collision (e.g. when `server_random` and `client_random` turn out to be all + // zeros), we still have a chance of producing a reasonable ID. + utils::xor(&mut id, &our_id.hash_bytes()[0..Digest::LENGTH]); + utils::xor(&mut id, &their_id.hash_bytes()[0..Digest::LENGTH]); + + ConnectionId(id) + } + + #[inline] + /// Returns a reference to the raw bytes of the connection ID. + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Creates a new connection ID from an existing SSL connection. + #[inline] + pub(crate) fn from_connection(ssl: &SslRef, our_id: NodeId, their_id: NodeId) -> Self { + Self::create(TlsRandomData::collect(ssl), our_id, their_id) + } + + /// Creates a random `ConnectionId`. + #[cfg(test)] + pub(super) fn random(rng: &mut TestRng) -> Self { + ConnectionId::create( + TlsRandomData::random(rng), + NodeId::random(rng), + NodeId::random(rng), + ) + } +} diff --git a/node/src/components/network/counting_format.rs b/node/src/components/network/counting_format.rs deleted file mode 100644 index 412633084f..0000000000 --- a/node/src/components/network/counting_format.rs +++ /dev/null @@ -1,380 +0,0 @@ -//! Observability for network serialization/deserialization. -//! -//! This module introduces two IDs: [`ConnectionId`] and [`TraceId`]. The [`ConnectionId`] is a -//! unique ID per established connection that can be independently derive by peers on either of a -//! connection. [`TraceId`] identifies a single message, distinguishing even messages that are sent -//! to the same peer with equal contents. - -use std::{ - convert::TryFrom, - fmt::{self, Display, Formatter}, - pin::Pin, - sync::{Arc, Weak}, -}; - -use bytes::{Bytes, BytesMut}; -use openssl::ssl::SslRef; -use pin_project::pin_project; -#[cfg(test)] -use rand::RngCore; -use static_assertions::const_assert; -use tokio_serde::{Deserializer, Serializer}; -use tracing::{trace, warn}; - -use casper_hashing::Digest; -#[cfg(test)] -use casper_types::testing::TestRng; - -use super::{tls::KeyFingerprint, Message, Metrics, Payload}; -use crate::{types::NodeId, utils}; - -/// Lazily-evaluated network message ID generator. -/// -/// Calculates a hash for the wrapped value when `Display::fmt` is called. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -struct TraceId([u8; 8]); - -impl Display for TraceId { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str(&base16::encode_lower(&self.0)) - } -} - -/// A metric-updating serializer/deserializer wrapper for network messages. -/// -/// Classifies each message given and updates the `NetworkingMetrics` accordingly. Also emits a -/// TRACE-level message to the `net_out` and `net_in` target with a per-message unique hash when -/// a message is sent or received. -#[pin_project] -#[derive(Debug)] -pub struct CountingFormat { - /// The actual serializer performing the work. - #[pin] - inner: F, - /// Identifier for the connection. - connection_id: ConnectionId, - /// Counter for outgoing messages. - out_count: u64, - /// Counter for incoming messages. - in_count: u64, - /// Our role in the connection. - role: Role, - /// Metrics to update. - metrics: Weak, -} - -impl CountingFormat { - /// Creates a new counting formatter. - #[inline] - pub(super) fn new( - metrics: Weak, - connection_id: ConnectionId, - role: Role, - inner: F, - ) -> Self { - Self { - metrics, - connection_id, - out_count: 0, - in_count: 0, - role, - inner, - } - } -} - -impl Serializer>> for CountingFormat -where - F: Serializer>>, - P: Payload, -{ - type Error = F::Error; - - #[inline] - fn serialize(self: Pin<&mut Self>, item: &Arc>) -> Result { - let this = self.project(); - let projection: Pin<&mut F> = this.inner; - - let serialized = F::serialize(projection, item)?; - let msg_size = serialized.len() as u64; - let msg_kind = item.classify(); - Metrics::record_payload_out(this.metrics, msg_kind, msg_size); - - let trace_id = this - .connection_id - .create_trace_id(this.role.out_flag(), *this.out_count); - *this.out_count += 1; - - trace!(target: "net_out", - msg_id = %trace_id, - msg_size, - msg_kind = %msg_kind, "sending"); - - Ok(serialized) - } -} - -impl Deserializer> for CountingFormat -where - F: Deserializer>, - P: Payload, -{ - type Error = F::Error; - - #[inline] - fn deserialize(self: Pin<&mut Self>, src: &BytesMut) -> Result, Self::Error> { - let this = self.project(); - let projection: Pin<&mut F> = this.inner; - - let msg_size = src.len() as u64; - - let deserialized = F::deserialize(projection, src)?; - let msg_kind = deserialized.classify(); - Metrics::record_payload_in(this.metrics, msg_kind, msg_size); - - let trace_id = this - .connection_id - .create_trace_id(this.role.in_flag(), *this.in_count); - *this.in_count += 1; - - trace!(target: "net_in", - msg_id = %trace_id, - msg_size, - msg_kind = %msg_kind, "received"); - - Ok(deserialized) - } -} - -/// An ID identifying a connection. -/// -/// The ID is guaranteed to be the same on both ends of the connection, but not guaranteed to be -/// unique or sufficiently random. Do not use it for any cryptographic/security related purposes. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(super) struct ConnectionId([u8; Digest::LENGTH]); - -// Invariant assumed by `ConnectionId`, `Digest` must be <= than `KeyFingerprint`. -const_assert!(KeyFingerprint::LENGTH >= Digest::LENGTH); -// We also assume it is at least 12 bytes. -const_assert!(Digest::LENGTH >= 12); - -/// Random data derived from TLS connections. -#[derive(Copy, Clone, Debug)] -pub(super) struct TlsRandomData { - /// Random data extract from the client of the connection. - combined_random: [u8; 12], -} - -/// Zero-randomness. -/// -/// Used to check random data. -const ZERO_RANDOMNESS: [u8; 12] = [0; 12]; - -impl TlsRandomData { - /// Collects random data from an existing SSL collection. - /// - /// Ideally we would use the TLS session ID, but it is not available on outgoing connections at - /// the times we need it. Instead, we use the `server_random` and `client_random` nonces, which - /// will be the same on both ends of the connection. - fn collect(ssl: &SslRef) -> Self { - // We are using only the first 12 bytes of these 32 byte values here, just in case we missed - // something in our assessment that hashing these should be safe. Additionally, these values - // are XOR'd, not concatenated. All this is done to prevent leaking information about these - // numbers. - // - // Some SSL implementations use timestamps for the first four bytes, so to be sufficiently - // random, we use 4 + 8 bytes of the nonces. - let mut server_random = [0; 12]; - let mut client_random = [0; 12]; - - ssl.server_random(&mut server_random); - - if server_random == ZERO_RANDOMNESS { - warn!("TLS server random is all zeros"); - } - - ssl.client_random(&mut client_random); - - if server_random == ZERO_RANDOMNESS { - warn!("TLS client random is all zeros"); - } - - // Combine using XOR. - utils::xor(&mut server_random, &client_random); - - Self { - combined_random: server_random, - } - } - - /// Creates random `TlsRandomData`. - #[cfg(test)] - fn random(rng: &mut TestRng) -> Self { - let mut buffer = [0u8; 12]; - - rng.fill_bytes(&mut buffer); - - Self { - combined_random: buffer, - } - } -} - -impl ConnectionId { - /// Creates a new connection ID, based on random values from server and client, as well as - /// node IDs. - fn create(random_data: TlsRandomData, our_id: NodeId, their_id: NodeId) -> ConnectionId { - // Hash the resulting random values. - let mut id = Digest::hash(random_data.combined_random).value(); - - // We XOR in a hashes of server and client fingerprint, to ensure that in the case of an - // accidental collision (e.g. when `server_random` and `client_random` turn out to be all - // zeros), we still have a chance of producing a reasonable ID. - utils::xor(&mut id, &our_id.hash_bytes()[0..Digest::LENGTH]); - utils::xor(&mut id, &their_id.hash_bytes()[0..Digest::LENGTH]); - - ConnectionId(id) - } - - /// Creates a new [`TraceID`] based on the message count. - /// - /// The `flag` should be created using the [`Role::in_flag`] or [`Role::out_flag`] method and - /// must be created accordingly (`out_flag` when serializing, `in_flag` when deserializing). - fn create_trace_id(&self, flag: u8, count: u64) -> TraceId { - // Copy the basic network ID. - let mut buffer = self.0; - - // Direction set on first byte. - buffer[0] ^= flag; - - // XOR in message count. - utils::xor(&mut buffer[4..12], &count.to_ne_bytes()); - - // Hash again and truncate. - let full_hash = Digest::hash(buffer); - - // Safe to expect here, as we assert earlier that `Digest` is at least 12 bytes. - let truncated = TryFrom::try_from(&full_hash.value()[0..8]).expect("buffer size mismatch"); - - TraceId(truncated) - } - - #[inline] - /// Returns a reference to the raw bytes of the connection ID. - pub(crate) fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Creates a new connection ID from an existing SSL connection. - #[inline] - pub(crate) fn from_connection(ssl: &SslRef, our_id: NodeId, their_id: NodeId) -> Self { - Self::create(TlsRandomData::collect(ssl), our_id, their_id) - } - - /// Creates a random `ConnectionId`. - #[cfg(test)] - pub(super) fn random(rng: &mut TestRng) -> Self { - ConnectionId::create( - TlsRandomData::random(rng), - NodeId::random(rng), - NodeId::random(rng), - ) - } -} - -/// Message sending direction. -#[derive(Copy, Clone, Debug)] -#[repr(u8)] -pub(super) enum Role { - /// Dialer, i.e. initiator of the connection. - Dialer, - /// Listener, acceptor of the connection. - Listener, -} - -impl Role { - /// Returns a flag suitable for hashing incoming messages. - #[inline] - fn in_flag(self) -> u8 { - !(self.out_flag()) - } - - /// Returns a flag suitable for hashing outgoing messages. - #[inline] - fn out_flag(self) -> u8 { - // The magic flag uses 50% of the bits, to be XOR'd into the hash later. - const MAGIC_FLAG: u8 = 0b10101010; - - match self { - Role::Dialer => MAGIC_FLAG, - Role::Listener => !MAGIC_FLAG, - } - } -} - -#[cfg(test)] -mod tests { - use crate::types::NodeId; - - use super::{ConnectionId, Role, TlsRandomData, TraceId}; - - #[test] - fn trace_id_has_16_character() { - let data = [0, 1, 2, 3, 4, 5, 6, 7]; - - let output = format!("{}", TraceId(data)); - - assert_eq!(output.len(), 16); - } - - #[test] - fn can_create_deterministic_trace_id() { - let mut rng = crate::new_rng(); - - // Scenario: Nodes A and B are connecting to each other. Both connections are established. - let node_a = NodeId::random(&mut rng); - let node_b = NodeId::random(&mut rng); - - // We get two connections, with different Tls random data, but it will be the same on both - // ends of the connection. - let a_to_b_random = TlsRandomData::random(&mut rng); - let a_to_b = ConnectionId::create(a_to_b_random, node_a, node_b); - let a_to_b_alt = ConnectionId::create(a_to_b_random, node_b, node_a); - - // Ensure that either peer ends up with the same connection id. - assert_eq!(a_to_b, a_to_b_alt); - - let b_to_a_random = TlsRandomData::random(&mut rng); - let b_to_a = ConnectionId::create(b_to_a_random, node_b, node_a); - let b_to_a_alt = ConnectionId::create(b_to_a_random, node_a, node_b); - assert_eq!(b_to_a, b_to_a_alt); - - // The connection IDs must be distinct though. - assert_ne!(a_to_b, b_to_a); - - // We are only looking at messages sent on the `a_to_b` connection, although from both ends. - // In our example example, `node_a` is the dialing node, `node_b` the listener. - - // Trace ID on A, after sending to B. - let msg_ab_0_on_a = a_to_b.create_trace_id(Role::Dialer.out_flag(), 0); - - // The same message on B. - let msg_ab_0_on_b = a_to_b.create_trace_id(Role::Listener.in_flag(), 0); - - // These trace IDs must match. - assert_eq!(msg_ab_0_on_a, msg_ab_0_on_b); - - // The second message must have a distinct trace ID. - let msg_ab_1_on_a = a_to_b.create_trace_id(Role::Dialer.out_flag(), 1); - let msg_ab_1_on_b = a_to_b.create_trace_id(Role::Listener.in_flag(), 1); - assert_eq!(msg_ab_1_on_a, msg_ab_1_on_b); - assert_ne!(msg_ab_0_on_a, msg_ab_1_on_a); - - // Sending a message on the **same connection** in a **different direction** also must yield - // a different message id. - let msg_ba_0_on_b = a_to_b.create_trace_id(Role::Listener.out_flag(), 0); - let msg_ba_0_on_a = a_to_b.create_trace_id(Role::Dialer.in_flag(), 0); - assert_eq!(msg_ba_0_on_b, msg_ba_0_on_a); - assert_ne!(msg_ba_0_on_b, msg_ab_0_on_b); - } -} diff --git a/node/src/components/network/error.rs b/node/src/components/network/error.rs index 3a4d324676..8ab676d81c 100644 --- a/node/src/components/network/error.rs +++ b/node/src/components/network/error.rs @@ -1,6 +1,7 @@ -use std::{error, io, net::SocketAddr, result}; +use std::{io, net::SocketAddr}; use datasize::DataSize; +use juliet::rpc::{IncomingRequest, RpcServerError}; use openssl::{error::ErrorStack, ssl}; use serde::Serialize; use thiserror::Error; @@ -13,7 +14,7 @@ use crate::{ utils::ResolveAddressError, }; -pub(super) type Result = result::Result; +use super::Channel; /// Error type returned by the `Network` component. #[derive(Debug, Error, Serialize)] @@ -57,6 +58,14 @@ pub enum Error { #[source] ResolveAddressError, ), + /// Could not open the specified keylog file for appending. + #[error("could not open keylog for appending")] + CannotAppendToKeylog( + #[serde(skip_serializing)] + #[source] + io::Error, + ), + /// Instantiating metrics failed. #[error(transparent)] Metrics( @@ -95,7 +104,7 @@ impl DataSize for ConnectionError { } } -/// An error related to an incoming or outgoing connection. +/// An error related to the establishment of an incoming or outgoing connection. #[derive(Debug, Error, Serialize)] pub enum ConnectionError { /// Failed to create TLS acceptor. @@ -134,18 +143,10 @@ pub enum ConnectionError { PeerCertificateInvalid(#[source] ValidationError), /// Failed to send handshake. #[error("handshake send failed")] - HandshakeSend( - #[serde(skip_serializing)] - #[source] - IoError, - ), + HandshakeSend(#[source] RawFrameIoError), /// Failed to receive handshake. #[error("handshake receive failed")] - HandshakeRecv( - #[serde(skip_serializing)] - #[source] - IoError, - ), + HandshakeRecv(#[source] RawFrameIoError), /// Peer reported a network name that does not match ours. #[error("peer is on different network: {0}")] WrongNetwork(String), @@ -162,12 +163,15 @@ pub enum ConnectionError { /// Peer did not send any message, or a non-handshake as its first message. #[error("peer did not send handshake")] DidNotSendHandshake, + /// Handshake did not complete in time. + #[error("could not complete handshake in time")] + HandshakeTimeout, /// Failed to encode our handshake. #[error("could not encode our handshake")] CouldNotEncodeOurHandshake( #[serde(skip_serializing)] #[source] - io::Error, + rmp_serde::encode::Error, ), /// A background sender for our handshake panicked or crashed. /// @@ -183,7 +187,7 @@ pub enum ConnectionError { InvalidRemoteHandshakeMessage( #[serde(skip_serializing)] #[source] - io::Error, + rmp_serde::decode::Error, ), /// The peer sent a consensus certificate, but it was invalid. #[error("invalid consensus certificate")] @@ -192,26 +196,59 @@ pub enum ConnectionError { #[source] crypto::Error, ), - /// Failed to reunite handshake sink/stream. +} + +/// IO error sending a raw frame. +/// +/// Raw frame IO is used only during the handshake, but comes with its own error conditions. +#[derive(Debug, Error, Serialize)] +pub enum RawFrameIoError { + /// Could not send or receive the raw frame. + #[error("io error")] + Io( + #[serde(skip_serializing)] + #[source] + io::Error, + ), + + /// Length limit violation. + #[error("advertised length of {0} exceeds configured maximum raw frame size")] + MaximumLengthExceeded(usize), +} + +/// An error produced by reading messages. +#[derive(Debug, Error)] +pub enum MessageReceiverError { + /// The message receival stack returned an error. + #[error(transparent)] + ReceiveError(#[from] RpcServerError), + /// Empty request sent. /// - /// This is usually a bug. - #[error("handshake sink/stream could not be reunited")] - FailedToReuniteHandshakeSinkAndStream, + /// This should never happen with a well-behaved client, since the current protocol always + /// expects a request to carry a payload. + #[error("empty request")] + EmptyRequest, + /// Error deserializing message. + #[error("message deserialization error")] + DeserializationError(bincode::Error), + /// Invalid channel. + #[error("invalid channel: {0}")] + InvalidChannel(u8), + /// Wrong channel for received message. + #[error("received a {got} message on channel {expected}")] + WrongChannel { + /// The channel the message was actually received on. + got: Channel, + /// The channel on which the message should have been sent. + expected: Channel, + }, } -/// IO operation that can time out or close. +/// Error produced by sending messages. #[derive(Debug, Error)] -pub enum IoError -where - E: error::Error + 'static, -{ - /// IO operation timed out. - #[error("io timeout")] - Timeout, - /// Non-timeout IO error. +pub enum MessageSenderError { + #[error("received a request on a send-only channel: {0}")] + UnexpectedIncomingRequest(IncomingRequest), #[error(transparent)] - Error(#[from] E), - /// Unexpected close/end-of-file. - #[error("closed unexpectedly")] - UnexpectedEof, + JulietRpcServerError(#[from] RpcServerError), } diff --git a/node/src/components/network/event.rs b/node/src/components/network/event.rs index 59c34f1b52..58092eb6f1 100644 --- a/node/src/components/network/event.rs +++ b/node/src/components/network/event.rs @@ -1,19 +1,20 @@ use std::{ fmt::{self, Debug, Display, Formatter}, - io, mem, + mem, net::SocketAddr, - sync::Arc, }; use derive_more::From; -use futures::stream::{SplitSink, SplitStream}; use serde::Serialize; use static_assertions::const_assert; use tracing::Span; use casper_types::PublicKey; -use super::{error::ConnectionError, FullTransport, GossipedAddress, Message, NodeId}; +use super::{ + error::{ConnectionError, MessageReceiverError}, + GossipedAddress, Message, NodeId, Ticket, Transport, +}; use crate::{ effect::{ announcements::PeerBehaviorAnnouncement, @@ -27,12 +28,16 @@ const_assert!(_NETWORK_EVENT_SIZE < 65); /// A network event. #[derive(Debug, From, Serialize)] -pub(crate) enum Event

{ +pub(crate) enum Event

+where + // Note: See notes on the `OutgoingConnection`'s `P: Serialize` trait bound for details. + P: Serialize, +{ Initialize, /// The TLS handshake completed on the incoming connection. IncomingConnection { - incoming: Box>, + incoming: Box, #[serde(skip)] span: Span, }, @@ -43,21 +48,25 @@ pub(crate) enum Event

{ msg: Box>, #[serde(skip)] span: Span, + /// The backpressure-related ticket for the message. + #[serde(skip)] + ticket: Ticket, }, /// Incoming connection closed. IncomingClosed { #[serde(skip_serializing)] - result: io::Result<()>, + result: Result<(), Box>, peer_id: Box, peer_addr: SocketAddr, + peer_consensus_public_key: Option>, #[serde(skip_serializing)] span: Box, }, /// A new outgoing connection was successfully established. OutgoingConnection { - outgoing: Box>, + outgoing: Box, #[serde(skip_serializing)] span: Span, }, @@ -108,7 +117,10 @@ impl From for Event { } } -impl Display for Event

{ +impl

Display for Event

+where + P: Display + Serialize, +{ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Event::Initialize => write!(f, "initialize"), @@ -119,6 +131,7 @@ impl Display for Event

{ peer_id: node_id, msg, span: _, + ticket: _, } => write!(f, "msg from {}: {}", node_id, msg), Event::IncomingClosed { peer_addr, .. } => { write!(f, "closed connection from {}", peer_addr) @@ -146,8 +159,10 @@ impl Display for Event

{ } /// Outcome of an incoming connection negotiation. +// Note: `IncomingConnection` is typically used boxed anyway, so a larget variant is not an issue. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Serialize)] -pub(crate) enum IncomingConnection

{ +pub(crate) enum IncomingConnection { /// The connection failed early on, before even a peer's [`NodeId`] could be determined. FailedEarly { /// Remote port the peer dialed us from. @@ -175,14 +190,14 @@ pub(crate) enum IncomingConnection

{ /// Peer's [`NodeId`]. peer_id: NodeId, /// The public key the peer is validating with, if any. - peer_consensus_public_key: Option, + peer_consensus_public_key: Option>, /// Stream of incoming messages. for incoming connections. #[serde(skip_serializing)] - stream: SplitStream>, + transport: Transport, }, } -impl

Display for IncomingConnection

{ +impl Display for IncomingConnection { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { IncomingConnection::FailedEarly { peer_addr, error } => { @@ -199,7 +214,7 @@ impl

Display for IncomingConnection

{ public_addr, peer_id, peer_consensus_public_key, - stream: _, + transport: _, } => { write!( f, @@ -219,7 +234,7 @@ impl

Display for IncomingConnection

{ /// Outcome of an outgoing connection attempt. #[derive(Debug, Serialize)] -pub(crate) enum OutgoingConnection

{ +pub(crate) enum OutgoingConnection { /// The outgoing connection failed early on, before a peer's [`NodeId`] could be determined. FailedEarly { /// Address that was dialed. @@ -245,16 +260,14 @@ pub(crate) enum OutgoingConnection

{ /// Peer's [`NodeId`]. peer_id: NodeId, /// The public key the peer is validating with, if any. - peer_consensus_public_key: Option, + peer_consensus_public_key: Option>, /// Sink for outgoing messages. - #[serde(skip_serializing)] - sink: SplitSink, Arc>>, - /// Holds the information whether the remote node is syncing. - is_syncing: bool, + #[serde(skip)] + transport: Transport, }, } -impl

Display for OutgoingConnection

{ +impl Display for OutgoingConnection { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { OutgoingConnection::FailedEarly { peer_addr, error } => { @@ -270,14 +283,9 @@ impl

Display for OutgoingConnection

{ peer_addr, peer_id, peer_consensus_public_key, - sink: _, - is_syncing, + transport: _, } => { - write!( - f, - "connection established to {}/{}, is_syncing: {}", - peer_addr, peer_id, is_syncing - )?; + write!(f, "connection established to {}/{}", peer_addr, peer_id,)?; if let Some(public_key) = peer_consensus_public_key { write!(f, " [{}]", public_key) diff --git a/node/src/components/network/handshake.rs b/node/src/components/network/handshake.rs new file mode 100644 index 0000000000..6219a32c4f --- /dev/null +++ b/node/src/components/network/handshake.rs @@ -0,0 +1,239 @@ +//! Handshake handling for `small_network`. +//! +//! The handshake differs from the rest of the networking code since it is (almost) unmodified since +//! version 1.0, to allow nodes to make informed decisions about blocking other nodes. +//! +//! This module contains an implementation for a minimal framing format based on 32-bit fixed size +//! big endian length prefixes. + +use std::{net::SocketAddr, time::Duration}; + +use casper_types::PublicKey; +use rand::Rng; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use serde::{de::DeserializeOwned, Serialize}; +use tracing::{debug, info}; + +use super::{ + connection_id::ConnectionId, + error::{ConnectionError, RawFrameIoError}, + tasks::NetworkContext, + Message, Payload, Transport, +}; + +/// The outcome of the handshake process. +pub(super) struct HandshakeOutcome { + /// A framed transport for peer. + pub(super) transport: Transport, + /// Public address advertised by the peer. + pub(super) public_addr: SocketAddr, + /// The public key the peer is validating with, if any. + pub(super) peer_consensus_public_key: Option>, +} + +/// Reads a 32 byte big endian integer prefix, followed by an actual raw message. +async fn read_length_prefixed_frame( + max_length: u32, + stream: &mut R, +) -> Result, RawFrameIoError> +where + R: AsyncRead + Unpin, +{ + let mut length_prefix_raw: [u8; 4] = [0; 4]; + stream + .read_exact(&mut length_prefix_raw) + .await + .map_err(RawFrameIoError::Io)?; + + let length = u32::from_ne_bytes(length_prefix_raw); + + if length > max_length { + return Err(RawFrameIoError::MaximumLengthExceeded(length as usize)); + } + + let mut raw = Vec::new(); // not preallocating, to make DOS attacks harder. + + // We can now read the raw frame and return. + stream + .take(length as u64) + .read_to_end(&mut raw) + .await + .map_err(RawFrameIoError::Io)?; + + Ok(raw) +} + +/// Writes data to an async writer, prefixing it with the 32 bytes big endian message length. +/// +/// Output will be flushed after sending. +async fn write_length_prefixed_frame(stream: &mut W, data: &[u8]) -> Result<(), RawFrameIoError> +where + W: AsyncWrite + Unpin, +{ + if data.len() > u32::MAX as usize { + return Err(RawFrameIoError::MaximumLengthExceeded(data.len())); + } + + async move { + stream.write_all(&(data.len() as u32).to_ne_bytes()).await?; + stream.write_all(data).await?; + stream.flush().await?; + Ok(()) + } + .await + .map_err(RawFrameIoError::Io)?; + + Ok(()) +} + +/// Serializes an item with the encoding settings specified for handshakes. +pub(crate) fn serialize(item: &T) -> Result, rmp_serde::encode::Error> +where + T: Serialize, +{ + rmp_serde::to_vec(item) +} + +/// Deserialize an item with the encoding settings specified for handshakes. +pub(crate) fn deserialize(raw: &[u8]) -> Result +where + T: DeserializeOwned, +{ + rmp_serde::from_slice(raw) +} + +/// Negotiates a handshake between two peers. +pub(super) async fn negotiate_handshake( + context: &NetworkContext, + transport: Transport, + connection_id: ConnectionId, +) -> Result +where + P: Payload, +{ + tokio::time::timeout( + context.handshake_timeout.into(), + do_negotiate_handshake::(context, transport, connection_id), + ) + .await + .unwrap_or_else(|_elapsed| Err(ConnectionError::HandshakeTimeout)) +} + +/// Performs a handshake. +/// +/// This function is cancellation safe. +async fn do_negotiate_handshake( + context: &NetworkContext, + transport: Transport, + connection_id: ConnectionId, +) -> Result +where + P: Payload, +{ + // Manually encode a handshake. + let handshake_message = context.chain_info().create_handshake::

( + context.public_addr().expect("TODO: What to do?"), + context.node_key_pair(), + connection_id, + ); + + let serialized_handshake_message = + serialize(&handshake_message).map_err(ConnectionError::CouldNotEncodeOurHandshake)?; + + // To ensure we are not dead-locking, we split the transport here and send the handshake in a + // background task before awaiting one ourselves. This ensures we can make progress regardless + // of the size of the outgoing handshake. + let (mut read_half, mut write_half) = tokio::io::split(transport); + + let handshake_send = tokio::spawn(async move { + write_length_prefixed_frame(&mut write_half, &serialized_handshake_message).await?; + Ok::<_, RawFrameIoError>(write_half) + }); + + // The remote's message should be a handshake, but can technically be any message. We receive, + // deserialize and check it. + let remote_message_raw = read_length_prefixed_frame( + context.chain_info().maximum_net_message_size, + &mut read_half, + ) + .await + .map_err(ConnectionError::HandshakeRecv)?; + + // Ensure the handshake was sent correctly. + let write_half = handshake_send + .await + .map_err(ConnectionError::HandshakeSenderCrashed)? + .map_err(ConnectionError::HandshakeSend)?; + + let remote_message: Message

= + deserialize(&remote_message_raw).map_err(ConnectionError::InvalidRemoteHandshakeMessage)?; + + if let Message::Handshake { + network_name, + public_addr, + protocol_version, + consensus_certificate, + chainspec_hash, + } = remote_message + { + debug!(%protocol_version, "handshake received"); + + // The handshake was valid, we can check the network name. + if network_name != context.chain_info().network_name { + return Err(ConnectionError::WrongNetwork(network_name)); + } + + // If there is a version mismatch, we treat it as a connection error. We do not ban peers + // for this error, but instead rely on exponential backoff, as bans would result in issues + // during upgrades where nodes may have a legitimate reason for differing versions. + // + // Since we are not using SemVer for versioning, we cannot make any assumptions about + // compatibility, so we allow only exact version matches. + if protocol_version != context.chain_info().protocol_version { + if let Some(threshold) = context.tarpit_version_threshold() { + if protocol_version <= threshold { + let mut rng = crate::new_rng(); + + if rng.gen_bool(context.tarpit_chance() as f64) { + // If tarpitting is enabled, we hold open the connection for a specific + // amount of time, to reduce load on other nodes and keep them from + // reconnecting. + info!(duration=?context.tarpit_duration(), "randomly tarpitting node"); + tokio::time::sleep(Duration::from(context.tarpit_duration())).await; + } else { + debug!(p = context.tarpit_chance(), "randomly not tarpitting node"); + } + } + } + return Err(ConnectionError::IncompatibleVersion(protocol_version)); + } + + // We check the chainspec hash to ensure peer is using the same chainspec as us. + // The remote message should always have a chainspec hash at this point since + // we checked the protocol version previously. + let peer_chainspec_hash = chainspec_hash.ok_or(ConnectionError::MissingChainspecHash)?; + if peer_chainspec_hash != context.chain_info().chainspec_hash { + return Err(ConnectionError::WrongChainspecHash(peer_chainspec_hash)); + } + + let peer_consensus_public_key = consensus_certificate + .map(|cert| { + cert.validate(connection_id) + .map_err(ConnectionError::InvalidConsensusCertificate) + }) + .transpose()? + .map(Box::new); + + let transport = read_half.unsplit(write_half); + + Ok(HandshakeOutcome { + transport, + public_addr, + peer_consensus_public_key, + }) + } else { + // Received a non-handshake, this is an error. + Err(ConnectionError::DidNotSendHandshake) + } +} diff --git a/node/src/components/network/health.rs b/node/src/components/network/health.rs deleted file mode 100644 index 18d018f12e..0000000000 --- a/node/src/components/network/health.rs +++ /dev/null @@ -1,825 +0,0 @@ -//! Health-check state machine. -//! -//! Health checks perform periodic pings to remote peers to ensure the connection is still alive. It -//! has somewhat complicated logic that is encoded in the `ConnectionHealth` struct, which has -//! multiple implicit states. - -use std::{ - fmt::{self, Display, Formatter}, - time::{Duration, Instant}, -}; - -use datasize::DataSize; -use rand::Rng; -use serde::{Deserialize, Serialize}; - -use crate::utils::specimen::{Cache, LargestSpecimen, SizeEstimator}; - -/// Connection health information. -/// -/// All data related to the ping/pong functionality used to verify a peer's networking liveness. -#[derive(Clone, Copy, DataSize, Debug)] -pub(crate) struct ConnectionHealth { - /// The moment the connection was established. - pub(crate) connected_since: Instant, - /// The last ping that was requested to be sent. - pub(crate) last_ping_sent: Option, - /// The most recent pong received. - pub(crate) last_pong_received: Option, - /// Number of invalid pongs received, reset upon receiving a valid pong. - pub(crate) invalid_pong_count: u32, - /// Number of pings that timed out. - pub(crate) ping_timeouts: u32, -} - -/// Health check configuration. -#[derive(DataSize, Debug)] -pub(crate) struct HealthConfig { - /// How often to send a ping to ensure a connection is established. - /// - /// Determines how soon after connecting or a successful ping another ping is sent. - pub(crate) ping_interval: Duration, - /// Duration during which a ping must succeed to be considered successful. - pub(crate) ping_timeout: Duration, - /// Number of retries before giving up and disconnecting a peer due to too many failed pings. - pub(crate) ping_retries: u16, - /// How many spurious pongs to tolerate before banning a peer. - pub(crate) pong_limit: u32, -} - -/// A timestamp with an associated nonce. -#[derive(Clone, Copy, DataSize, Debug)] -pub(crate) struct TaggedTimestamp { - /// The actual timestamp. - timestamp: Instant, - /// The nonce of the timestamp. - nonce: Nonce, -} - -impl TaggedTimestamp { - /// Creates a new tagged timestamp with a random nonce. - pub(crate) fn new(rng: &mut R, timestamp: Instant) -> Self { - Self { - timestamp, - nonce: rng.gen(), - } - } - - /// Creates a new tagged timestamp from parts. - pub(crate) fn from_parts(timestamp: Instant, nonce: Nonce) -> Self { - TaggedTimestamp { nonce, timestamp } - } - - /// Returns the actual timestamp. - pub(crate) fn timestamp(&self) -> Instant { - self.timestamp - } - - /// Returns the nonce inside the timestamp. - pub(crate) fn nonce(self) -> Nonce { - self.nonce - } -} - -/// A number-used-once, specifically one used in pings. -// Note: This nonce used to be a `u32`, but that is too small - since we immediately disconnect when -// a duplicate ping is generated, a `u32` has a ~ 1/(2^32) chance of a consecutive collision. -// -// If we ping every 5 seconds, this is a ~ 0.01% chance over a month, which is too high over -// thousands over nodes. At 64 bits, in theory the upper bound is 0.0000000002%, which is -// better (the period of the RNG used should be >> 64 bits). -// -// While we do check for consecutive ping nonces being generated, we still like the lower -// collision chance for repeated pings being sent. -#[derive(Clone, Copy, DataSize, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub(crate) struct Nonce(u64); - -impl Display for Nonce { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{:016X}", self.0) - } -} - -impl rand::distributions::Distribution for rand::distributions::Standard { - #[inline(always)] - fn sample(&self, rng: &mut R) -> Nonce { - Nonce(rng.gen()) - } -} - -impl ConnectionHealth { - /// Creates a new connection health instance, recording when the connection was established. - pub(crate) fn new(connected_since: Instant) -> Self { - Self { - connected_since, - last_ping_sent: None, - last_pong_received: None, - invalid_pong_count: 0, - ping_timeouts: 0, - } - } -} - -impl ConnectionHealth { - /// Calculate the round-trip time, if possible. - pub(crate) fn calc_rrt(&self) -> Option { - match (self.last_ping_sent, self.last_pong_received) { - (Some(last_ping), Some(last_pong)) if last_ping.nonce == last_pong.nonce => { - Some(last_pong.timestamp.duration_since(last_ping.timestamp)) - } - _ => None, - } - } - - /// Check current health status. - /// - /// This function must be polled periodically and returns a potential action to be performed. - pub(crate) fn update_health( - &mut self, - rng: &mut R, - cfg: &HealthConfig, - now: Instant, - ) -> HealthCheckOutcome { - // Having received too many pongs should always result in a disconnect. - if self.invalid_pong_count > cfg.pong_limit { - return HealthCheckOutcome::GiveUp; - } - - // Our honeymoon period is from first establishment of the connection until we send a ping. - if now.saturating_duration_since(self.connected_since) < cfg.ping_interval { - return HealthCheckOutcome::DoNothing; - } - - let send_ping = match self.last_ping_sent { - Some(last_ping) => { - match self.last_pong_received { - Some(prev_pong) if prev_pong.nonce() == last_ping.nonce() => { - // Normal operation. The next ping should be sent in a regular interval - // after receiving the last pong. - now >= prev_pong.timestamp() + cfg.ping_interval - } - - _ => { - // No matching pong on record. Check if we need to timeout the ping. - if now >= last_ping.timestamp() + cfg.ping_timeout { - self.ping_timeouts += 1; - // Clear the `last_ping_sent`, schedule another to be sent. - self.last_ping_sent = None; - true - } else { - false - } - } - } - } - None => true, - }; - - if send_ping { - if self.ping_timeouts > cfg.ping_retries as u32 { - // We have exceeded the timeouts and will give up as a result. - return HealthCheckOutcome::GiveUp; - } - - let ping = loop { - let candidate = TaggedTimestamp::new(rng, now); - - if let Some(prev) = self.last_ping_sent { - if prev.nonce() == candidate.nonce() { - // Ensure we don't produce consecutive pings. - continue; - } - } - - break candidate; - }; - - self.last_ping_sent = Some(ping); - HealthCheckOutcome::SendPing(ping.nonce()) - } else { - HealthCheckOutcome::DoNothing - } - } - - /// Records a pong that has been sent. - /// - /// If `true`, the maximum number of pongs has been exceeded and the peer should be banned. - pub(crate) fn record_pong(&mut self, cfg: &HealthConfig, tt: TaggedTimestamp) -> bool { - let is_valid_pong = match self.last_ping_sent { - Some(last_ping) if last_ping.nonce() == tt.nonce => { - // Check if we already received a pong for this ping, which is a protocol violation. - if self - .last_pong_received - .map(|existing| existing.nonce() == tt.nonce) - .unwrap_or(false) - { - // Ping is a collsion, ban. - return true; - } - - if last_ping.timestamp() > tt.timestamp() { - // Ping is from the past somehow, ignore it (probably a bug on our side). - return false; - } - - // The ping is valid if it is within the timeout period. - last_ping.timestamp() + cfg.ping_timeout >= tt.timestamp() - } - _ => { - // Either the nonce did not match, or the nonce mismatched. - false - } - }; - - if is_valid_pong { - // Our pong is valid, reset invalid and ping count, then record it. - self.invalid_pong_count = 0; - self.ping_timeouts = 0; - self.last_pong_received = Some(tt); - false - } else { - self.invalid_pong_count += 1; - // If we have exceeded the invalid pong limit, ban. - self.invalid_pong_count > cfg.pong_limit - } - } -} - -/// The outcome of periodic health check. -#[derive(Clone, Copy, Debug)] - -pub(crate) enum HealthCheckOutcome { - /// Do nothing, as we recently took action. - DoNothing, - /// Send a ping with the given nonce. - SendPing(Nonce), - /// Give up on (i.e. terminate) the connection, as we exceeded the allowable ping limit. - GiveUp, -} - -impl LargestSpecimen for Nonce { - fn largest_specimen(estimator: &E, cache: &mut Cache) -> Self { - Self(LargestSpecimen::largest_specimen(estimator, cache)) - } -} - -#[cfg(test)] -mod tests { - use std::{collections::HashSet, time::Duration}; - - use assert_matches::assert_matches; - use rand::Rng; - - use super::{ConnectionHealth, HealthCheckOutcome, HealthConfig}; - use crate::{ - components::network::health::TaggedTimestamp, testing::test_clock::TestClock, - types::NodeRng, - }; - - impl HealthConfig { - pub(crate) fn test_config() -> Self { - // Note: These values are assumed in tests, so do not change them. - HealthConfig { - ping_interval: Duration::from_secs(5), - ping_timeout: Duration::from_secs(2), - ping_retries: 3, - pong_limit: 6, - } - } - } - - struct Fixtures { - clock: TestClock, - cfg: HealthConfig, - rng: NodeRng, - health: ConnectionHealth, - } - - /// Sets up fixtures used in almost every test. - fn fixtures() -> Fixtures { - let clock = TestClock::new(); - let cfg = HealthConfig::test_config(); - let rng = crate::new_rng(); - - let health = ConnectionHealth::new(clock.now()); - - Fixtures { - clock, - cfg, - rng, - health, - } - } - - #[test] - fn scenario_no_response() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // Repeated checks should not change the outcome. - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // After 4.9 seconds, we still do not send a ping. - clock.advance(Duration::from_millis(4900)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // At 5, we expect our first ping. - clock.advance(Duration::from_millis(100)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Checking health again should not result in another ping. - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - clock.advance(Duration::from_millis(100)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // After two seconds, we expect another ping to be sent, due to timeouts. - clock.advance(Duration::from_millis(2000)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // At this point, two pings have been sent. Configuration says to retry 3 times, so a total - // of five pings is expected. - clock.advance(Duration::from_millis(2000)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - clock.advance(Duration::from_millis(2000)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Finally, without receiving a ping at all, we give up. - clock.advance(Duration::from_millis(2000)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::GiveUp - ); - } - - #[test] - fn pings_use_different_nonces() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - clock.advance(Duration::from_secs(5)); - - let mut nonce_set = HashSet::new(); - - nonce_set.insert(assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - )); - clock.advance(Duration::from_secs(2)); - - nonce_set.insert(assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - )); - clock.advance(Duration::from_secs(2)); - - nonce_set.insert(assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - )); - clock.advance(Duration::from_secs(2)); - - nonce_set.insert(assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - )); - - // Since it is a set, we expect less than 4 items if there were any duplicates. - assert_eq!(nonce_set.len(), 4); - } - - #[test] - fn scenario_all_working() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // At 5 seconds, we expect our first ping. - clock.advance(Duration::from_secs(5)); - - let nonce_1 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - // Record a reply 500 ms later. - clock.advance(Duration::from_millis(500)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_1))); - - // Our next pong should be 5 seconds later, not 4.5. - clock.advance(Duration::from_millis(4500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - clock.advance(Duration::from_millis(500)); - - let nonce_2 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - // We test an edge case here where we use the same timestamp for the received pong. - clock.advance(Duration::from_millis(500)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_2))); - - // Afterwards, no ping should be sent. - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // Do 1000 additional ping/pongs. - for _ in 0..1000 { - clock.advance(Duration::from_millis(5000)); - let nonce = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - clock.advance(Duration::from_millis(250)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce))); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - } - } - - #[test] - fn scenario_intermittent_failures() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - // We miss two pings initially, before recovering. - clock.advance(Duration::from_secs(5)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - clock.advance(Duration::from_secs(2)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - clock.advance(Duration::from_secs(2)); - - let nonce_1 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - clock.advance(Duration::from_secs(1)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_1))); - - // We successfully "recovered", this should reset our ping counts. Miss three pings before - // successfully receiving a pong from 4th from here on out. - clock.advance(Duration::from_millis(5500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - let nonce_2 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - clock.advance(Duration::from_millis(500)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_2))); - - // This again should reset. We miss four more pings and are disconnected. - clock.advance(Duration::from_millis(5500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - clock.advance(Duration::from_millis(2500)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::GiveUp - ); - } - - #[test] - fn ignores_unwanted_pongs() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(5)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Make the `ConnectionHealth` receive some unasked pongs, without exceeding the unasked - // pong limit. - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - - // The retry delay is 2 seconds (instead of 5 for the next pong after success), so ensure - // we retry due to not having received the correct nonce in the pong. - - clock.advance(Duration::from_secs(2)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - } - - #[test] - fn ensure_excessive_pongs_result_in_ban() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(5)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Make the `ConnectionHealth` receive some unasked pongs, without exceeding the unasked - // pong limit. - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - // 6 unasked pongs is still okay. - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - assert!(health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - // 7 is too much. - - // For good measure, we expect the health check to also output a disconnect instruction. - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::GiveUp - ); - } - - #[test] - fn time_reversal_does_not_crash_but_is_ignored() { - // Usually a pong for a given (or any) nonce should always be received with a timestamp - // equal or later than the ping sent out. Due to a programming error or a lucky attacker + - // scheduling issue, there is a very minute chance this can actually happen. - // - // In these cases, the pongs should just be discarded, not crashing due to a underflow in - // the comparison. - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(5)); // t = 5 - - let nonce_1 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - // Ignore the nonce if sent in the past (and also don't crash). - clock.rewind(Duration::from_secs(1)); // t = 4 - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_1))); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - - // Another ping should be sent out, since `nonce_1` was ignored. - clock.advance(Duration::from_secs(3)); // t = 7 - let nonce_2 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - // Nonce 2 will be received seemingly before the connection was even established. - clock.rewind(Duration::from_secs(3600)); - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_2))); - } - - #[test] - fn handles_missed_health_checks() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(15)); - - // We initially exceed our scheduled first ping by 10 seconds. This will cause the ping to - // be sent right there and then. - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Going forward 1 second should not change anything. - clock.advance(Duration::from_secs(1)); - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - // After another second, two seconds have passed since sending the first ping in total, so - // send another once. - clock.advance(Duration::from_secs(1)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // We have missed two pings total, now wait an hour. This will trigger the third ping. - clock.advance(Duration::from_secs(3600)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Fourth right after - clock.advance(Duration::from_secs(2)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - // Followed by a disconnect. - clock.advance(Duration::from_secs(2)); - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::GiveUp - ); - } - - #[test] - fn ignores_time_travel() { - // Any call of the health update with timestamps that are provably from the past (i.e. - // before a recorded timestamp like a previous ping) should be ignored. - - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(5)); // t = 5 - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - - clock.rewind(Duration::from_secs(3)); // t = 2 - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - - clock.advance(Duration::from_secs(4)); // t = 6 - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::DoNothing - ); - clock.advance(Duration::from_secs(1)); // t = 7 - - assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(_) - ); - } - - #[test] - fn duplicate_pong_immediately_terminates() { - let Fixtures { - mut clock, - cfg, - mut rng, - mut health, - } = fixtures(); - - clock.advance(Duration::from_secs(5)); - let nonce_1 = assert_matches!( - health.update_health(&mut rng, &cfg, clock.now()), - HealthCheckOutcome::SendPing(nonce) => nonce - ); - - clock.advance(Duration::from_secs(1)); - - // Recording the pong once is fine, but the second time should result in a ban. - assert!(!health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_1))); - assert!(health.record_pong(&cfg, TaggedTimestamp::from_parts(clock.now(), nonce_1))); - } -} diff --git a/node/src/components/network/identity.rs b/node/src/components/network/identity.rs index 81a592fcd4..6d96326048 100644 --- a/node/src/components/network/identity.rs +++ b/node/src/components/network/identity.rs @@ -9,7 +9,7 @@ use openssl::{ use thiserror::Error; use tracing::warn; -use super::{Config, IdentityConfig}; +use super::config::{Config, IdentityConfig}; use crate::{ tls::{self, LoadCertError, LoadSecretKeyError, TlsCert, ValidationError}, types::NodeId, diff --git a/node/src/components/network/insights.rs b/node/src/components/network/insights.rs index 3117ac4d9e..db7355b9be 100644 --- a/node/src/components/network/insights.rs +++ b/node/src/components/network/insights.rs @@ -6,11 +6,10 @@ //! insights should neither be abused just because they are available. use std::{ - collections::{BTreeSet, HashSet}, + collections::BTreeSet, fmt::{self, Debug, Display, Formatter}, net::SocketAddr, - sync::atomic::Ordering, - time::{Duration, SystemTime}, + time::SystemTime, }; use casper_types::{EraId, PublicKey}; @@ -35,16 +34,10 @@ pub(crate) struct NetworkInsights { network_ca: bool, /// The public address of the node. public_addr: Option, - /// Whether or not the node is syncing. - is_syncing: bool, + /// The fingerprint of a consensus key installed. + node_key_pair: Option, /// The active era as seen by the networking component. net_active_era: EraId, - /// The list of node IDs that are being preferred due to being active validators. - privileged_active_outgoing_nodes: Option>, - /// The list of node IDs that are being preferred due to being upcoming validators. - privileged_upcoming_outgoing_nodes: Option>, - /// The amount of bandwidth allowance currently buffered, ready to be spent. - unspent_bandwidth_allowance_bytes: Option, /// Map of outgoing connections, along with their current state. outgoing_connections: Vec<(SocketAddr, OutgoingInsight)>, /// Map of incoming connections. @@ -75,10 +68,6 @@ enum OutgoingStateInsight { Connected { peer_id: NodeId, peer_addr: SocketAddr, - last_ping_sent: Option, - last_pong_received: Option, - invalid_pong_count: u32, - rtt: Option, }, Blocked { since: SystemTime, @@ -98,9 +87,9 @@ fn time_delta(now: SystemTime, then: SystemTime) -> impl Display { impl OutgoingStateInsight { /// Constructs a new outgoing state insight from a given outgoing state. - fn from_outgoing_state

( + fn from_outgoing_state( anchor: &TimeAnchor, - state: &OutgoingState, ConnectionError>, + state: &OutgoingState, ) -> Self { match state { OutgoingState::Connecting { @@ -119,21 +108,9 @@ impl OutgoingStateInsight { error: error.as_ref().map(ToString::to_string), last_failure: anchor.convert(*last_failure), }, - OutgoingState::Connected { - peer_id, - handle, - health, - } => OutgoingStateInsight::Connected { + OutgoingState::Connected { peer_id, handle } => OutgoingStateInsight::Connected { peer_id: *peer_id, peer_addr: handle.peer_addr, - last_ping_sent: health - .last_ping_sent - .map(|tt| anchor.convert(tt.timestamp())), - last_pong_received: health - .last_pong_received - .map(|tt| anchor.convert(tt.timestamp())), - invalid_pong_count: health.invalid_pong_count, - rtt: health.calc_rrt(), }, OutgoingState::Blocked { since, @@ -169,26 +146,8 @@ impl OutgoingStateInsight { OptDisplay::new(error.as_ref(), "none"), time_delta(now, *last_failure) ), - OutgoingStateInsight::Connected { - peer_id, - peer_addr, - last_ping_sent, - last_pong_received, - invalid_pong_count, - rtt, - } => { - let rtt_ms = rtt.map(|duration| duration.as_millis()); - - write!( - f, - "connected -> {} @ {} (rtt {}, invalid {}, last ping/pong {}/{})", - peer_id, - peer_addr, - OptDisplay::new(rtt_ms, "?"), - invalid_pong_count, - OptDisplay::new(last_ping_sent.map(|t| time_delta(now, t)), "-"), - OptDisplay::new(last_pong_received.map(|t| time_delta(now, t)), "-"), - ) + OutgoingStateInsight::Connected { peer_id, peer_addr } => { + write!(f, "connected -> {} @ {}", peer_id, peer_addr,) } OutgoingStateInsight::Blocked { since, @@ -268,15 +227,6 @@ impl NetworkInsights { where P: Payload, { - // Since we are at the top level of the component, we gain access to inner values of the - // respective structs. We abuse this to gain debugging insights. Note: If limiters are no - // longer a `trait`, the trait methods can be removed as well in favor of direct access. - let (privileged_active_outgoing_nodes, privileged_upcoming_outgoing_nodes) = net - .outgoing_limiter - .debug_inspect_validators(&net.active_era) - .map(|(a, b)| (Some(a), Some(b))) - .unwrap_or_default(); - let anchor = TimeAnchor::now(); let outgoing_connections = net @@ -310,13 +260,11 @@ impl NetworkInsights { our_id: net.context.our_id(), network_ca: net.context.network_ca().is_some(), public_addr: net.context.public_addr(), - is_syncing: net.context.is_syncing().load(Ordering::Relaxed), + node_key_pair: net + .context + .node_key_pair() + .map(|kp| kp.public_key().clone()), net_active_era: net.active_era, - privileged_active_outgoing_nodes, - privileged_upcoming_outgoing_nodes, - unspent_bandwidth_allowance_bytes: net - .outgoing_limiter - .debug_inspect_unspent_allowance(), outgoing_connections, connection_symmetries, } @@ -334,34 +282,9 @@ impl Display for NetworkInsights { } writeln!( f, - "node {} @ {:?} (syncing: {})", - self.our_id, self.public_addr, self.is_syncing - )?; - writeln!( - f, - "active era: {} unspent_bandwidth_allowance_bytes: {}", - self.net_active_era, - OptDisplay::new(self.unspent_bandwidth_allowance_bytes, "inactive"), - )?; - let active = self - .privileged_active_outgoing_nodes - .as_ref() - .map(HashSet::iter) - .map(DisplayIter::new); - writeln!( - f, - "privileged active: {}", - OptDisplay::new(active, "inactive") - )?; - let upcoming = self - .privileged_upcoming_outgoing_nodes - .as_ref() - .map(HashSet::iter) - .map(DisplayIter::new); - writeln!( - f, - "privileged upcoming: {}", - OptDisplay::new(upcoming, "inactive") + "node {} @ {}", + self.our_id, + OptDisplay::new(self.public_addr, "no listen addr") )?; f.write_str("outgoing connections:\n")?; diff --git a/node/src/components/network/limiter.rs b/node/src/components/network/limiter.rs deleted file mode 100644 index fcba95d2af..0000000000 --- a/node/src/components/network/limiter.rs +++ /dev/null @@ -1,550 +0,0 @@ -//! Resource limiters -//! -//! Resource limiters restrict the usable amount of a resource through slowing down the request rate -//! by making each user request an allowance first. - -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, RwLock}, - time::{Duration, Instant}, -}; - -use prometheus::Counter; -use tokio::{runtime::Handle, sync::Mutex, task}; -use tracing::{error, trace, warn}; - -use casper_types::{EraId, PublicKey}; - -use crate::types::{NodeId, ValidatorMatrix}; - -/// Amount of resource allowed to buffer in `Limiter`. -const STORED_BUFFER_SECS: Duration = Duration::from_secs(2); - -/// A limiter dividing resources into two classes based on their validator status. -/// -/// Any consumer of a specific resource is expected to call `create_handle` for every peer and use -/// the returned handle to request a access to a resource. -/// -/// Imposes a limit on non-validator resources while not limiting active validator resources at all. -#[derive(Debug)] -pub(super) struct Limiter { - /// Shared data across all handles. - data: Arc, - /// Set of active and upcoming validators shared across all handles. - validator_matrix: ValidatorMatrix, -} - -impl Limiter { - /// Creates a new class based limiter. - /// - /// Starts the background worker task as well. - pub(super) fn new( - resources_per_second: u32, - wait_time_sec: Counter, - validator_matrix: ValidatorMatrix, - ) -> Self { - Limiter { - data: Arc::new(LimiterData::new(resources_per_second, wait_time_sec)), - validator_matrix, - } - } - - /// Create a handle for a connection using the given peer and optional consensus key. - pub(super) fn create_handle( - &self, - peer_id: NodeId, - consensus_key: Option, - ) -> LimiterHandle { - if let Some(public_key) = consensus_key.as_ref().cloned() { - match self.data.connected_validators.write() { - Ok(mut connected_validators) => { - let _ = connected_validators.insert(peer_id, public_key); - } - Err(_) => { - error!( - "could not update connected validator data set of limiter, lock poisoned" - ); - } - } - } - LimiterHandle { - data: self.data.clone(), - validator_matrix: self.validator_matrix.clone(), - consumer_id: ConsumerId { - _peer_id: peer_id, - consensus_key, - }, - } - } - - pub(super) fn remove_connected_validator(&self, peer_id: &NodeId) { - match self.data.connected_validators.write() { - Ok(mut connected_validators) => { - let _ = connected_validators.remove(peer_id); - } - Err(_) => { - error!( - "could not remove connected validator from data set of limiter, lock poisoned" - ); - } - } - } - - pub(super) fn is_validator_in_era(&self, era: EraId, peer_id: &NodeId) -> bool { - let public_key = match self.data.connected_validators.read() { - Ok(connected_validators) => match connected_validators.get(peer_id) { - None => return false, - Some(public_key) => public_key.clone(), - }, - Err(_) => { - error!("could not read from connected_validators of limiter, lock poisoned"); - return false; - } - }; - - match self.validator_matrix.is_validator_in_era(era, &public_key) { - None => { - warn!(%era, "missing validator weights for given era"); - false - } - Some(is_validator) => is_validator, - } - } - - pub(super) fn debug_inspect_unspent_allowance(&self) -> Option { - Some(task::block_in_place(move || { - Handle::current().block_on(async move { self.data.resources.lock().await.available }) - })) - } - - pub(super) fn debug_inspect_validators( - &self, - current_era: &EraId, - ) -> Option<(HashSet, HashSet)> { - Some(( - self.validator_keys_for_era(current_era), - self.validator_keys_for_era(¤t_era.successor()), - )) - } - - fn validator_keys_for_era(&self, era: &EraId) -> HashSet { - self.validator_matrix - .validator_weights(*era) - .map(|validator_weights| validator_weights.validator_public_keys().cloned().collect()) - .unwrap_or_default() - } -} - -/// The limiter's state. -#[derive(Debug)] -struct LimiterData { - /// Number of resource units to allow for non-validators per second. - resources_per_second: u32, - /// A mapping from node IDs to public keys of validators to which we have an outgoing - /// connection. - connected_validators: RwLock>, - /// Information about available resources. - resources: Mutex, - /// Total time spent waiting. - wait_time_sec: Counter, -} - -/// Resource data. -#[derive(Debug)] -struct ResourceData { - /// How many resource units are buffered. - /// - /// May go negative in the case of a deficit. - available: i64, - /// Last time resource data was refilled. - last_refill: Instant, -} - -impl LimiterData { - /// Creates a new set of class based limiter data. - /// - /// Initial resources will be initialized to 0, with the last refill set to the current time. - fn new(resources_per_second: u32, wait_time_sec: Counter) -> Self { - LimiterData { - resources_per_second, - connected_validators: Default::default(), - resources: Mutex::new(ResourceData { - available: 0, - last_refill: Instant::now(), - }), - wait_time_sec, - } - } -} - -/// Peer class for the `Limiter`. -enum PeerClass { - /// A validator. - Validator, - /// Unclassified/low-priority peer. - NonValidator, -} - -/// A per-peer handle for `Limiter`. -#[derive(Debug)] -pub(super) struct LimiterHandle { - /// Data shared between handles and limiter. - data: Arc, - /// Set of active and upcoming validators. - validator_matrix: ValidatorMatrix, - /// Consumer ID for the sender holding this handle. - consumer_id: ConsumerId, -} - -impl LimiterHandle { - /// Waits until the requester is allocated `amount` additional resources. - pub(super) async fn request_allowance(&self, amount: u32) { - // As a first step, determine the peer class by checking if our id is in the validator set. - - if self.validator_matrix.is_empty() { - // It is likely that we have not been initialized, thus no node is getting the - // reserved resources. In this case, do not limit at all. - trace!("empty set of validators, not limiting resources at all"); - - return; - } - - let peer_class = if let Some(ref public_key) = self.consumer_id.consensus_key { - if self - .validator_matrix - .is_active_or_upcoming_validator(public_key) - { - PeerClass::Validator - } else { - PeerClass::NonValidator - } - } else { - PeerClass::NonValidator - }; - - match peer_class { - PeerClass::Validator => { - // No limit imposed on validators. - } - PeerClass::NonValidator => { - if self.data.resources_per_second == 0 { - return; - } - - let max_stored_resource = ((self.data.resources_per_second as f64) - * STORED_BUFFER_SECS.as_secs_f64()) - as u32; - - // We are a low-priority sender. Obtain a lock on the resources and wait an - // appropriate amount of time to fill them up. - { - let mut resources = self.data.resources.lock().await; - - while resources.available < 0 { - // Determine time delta since last refill. - let now = Instant::now(); - let elapsed = now - resources.last_refill; - resources.last_refill = now; - - // Add appropriate amount of resources, capped at `max_stored_bytes`. We - // are still maintaining the lock here to avoid issues with other - // low-priority requestors. - resources.available += ((elapsed.as_nanos() - * self.data.resources_per_second as u128) - / 1_000_000_000) as i64; - resources.available = resources.available.min(max_stored_resource as i64); - - // If we do not have enough resources available, sleep until we do. - if resources.available < 0 { - let estimated_time_remaining = Duration::from_millis( - (-resources.available) as u64 * 1000 - / self.data.resources_per_second as u64, - ); - - // Note: This sleep call is the reason we are using a tokio mutex - // instead of a regular `std` one, as we are holding it across the - // await point here. - tokio::time::sleep(estimated_time_remaining).await; - self.data - .wait_time_sec - .inc_by(estimated_time_remaining.as_secs_f64()); - } - } - - // Subtract the amount. If available resources go negative as a result, it - // is the next sender's problem. - resources.available -= amount as i64; - } - } - } - } -} - -/// An identity for a consumer. -#[derive(Debug)] -struct ConsumerId { - /// The peer's ID. - _peer_id: NodeId, - /// The remote node's public consensus key. - consensus_key: Option, -} - -#[cfg(test)] -mod tests { - use std::{sync::Arc, time::Duration}; - - use casper_types::{EraId, SecretKey}; - use num_rational::Ratio; - use prometheus::Counter; - use tokio::time::Instant; - - use super::{Limiter, NodeId, PublicKey}; - use crate::{testing::init_logging, types::ValidatorMatrix}; - - /// Something that happens almost immediately, with some allowance for test jitter. - const SHORT_TIME: Duration = Duration::from_millis(250); - - /// Creates a new counter for testing. - fn new_wait_time_sec() -> Counter { - Counter::new("test_time_waiting", "wait time counter used in tests") - .expect("could not create new counter") - } - - #[tokio::test] - async fn unlimited_limiter_is_unlimited() { - let mut rng = crate::new_rng(); - - // We insert one unrelated active validator to avoid triggering the automatic disabling of - // the limiter in case there are no active validators. - let validator_matrix = - ValidatorMatrix::new_with_validator(Arc::new(SecretKey::random(&mut rng))); - let limiter = Limiter::new(0, new_wait_time_sec(), validator_matrix); - - // Try with non-validators or unknown nodes. - let handles = vec![ - limiter.create_handle(NodeId::random(&mut rng), Some(PublicKey::random(&mut rng))), - limiter.create_handle(NodeId::random(&mut rng), None), - ]; - - for handle in handles { - let start = Instant::now(); - handle.request_allowance(0).await; - handle.request_allowance(u32::MAX).await; - handle.request_allowance(1).await; - assert!(start.elapsed() < SHORT_TIME); - } - } - - #[tokio::test] - async fn active_validator_is_unlimited() { - let mut rng = crate::new_rng(); - - let secret_key = SecretKey::random(&mut rng); - let consensus_key = PublicKey::from(&secret_key); - let validator_matrix = ValidatorMatrix::new_with_validator(Arc::new(secret_key)); - let limiter = Limiter::new(1_000, new_wait_time_sec(), validator_matrix); - - let handle = limiter.create_handle(NodeId::random(&mut rng), Some(consensus_key)); - - let start = Instant::now(); - handle.request_allowance(0).await; - handle.request_allowance(u32::MAX).await; - handle.request_allowance(1).await; - assert!(start.elapsed() < SHORT_TIME); - } - - #[tokio::test] - async fn inactive_validator_limited() { - let rng = &mut crate::new_rng(); - - // We insert one unrelated active validator to avoid triggering the automatic disabling of - // the limiter in case there are no active validators. - let validator_matrix = - ValidatorMatrix::new_with_validator(Arc::new(SecretKey::random(rng))); - let peers = [ - (NodeId::random(rng), Some(PublicKey::random(rng))), - (NodeId::random(rng), None), - ]; - - let limiter = Limiter::new(1_000, new_wait_time_sec(), validator_matrix); - - for (peer, maybe_public_key) in peers { - let start = Instant::now(); - let handle = limiter.create_handle(peer, maybe_public_key); - - // Send 9_0001 bytes, we expect this to take roughly 15 seconds. - handle.request_allowance(1000).await; - handle.request_allowance(1000).await; - handle.request_allowance(1000).await; - handle.request_allowance(2000).await; - handle.request_allowance(4000).await; - handle.request_allowance(1).await; - let elapsed = start.elapsed(); - - assert!( - elapsed >= Duration::from_secs(9), - "{}s", - elapsed.as_secs_f64() - ); - assert!( - elapsed <= Duration::from_secs(10), - "{}s", - elapsed.as_secs_f64() - ); - } - } - - #[tokio::test] - async fn nonvalidators_parallel_limited() { - let mut rng = crate::new_rng(); - - let wait_metric = new_wait_time_sec(); - - // We insert one unrelated active validator to avoid triggering the automatic disabling of - // the limiter in case there are no active validators. - let validator_matrix = - ValidatorMatrix::new_with_validator(Arc::new(SecretKey::random(&mut rng))); - let limiter = Limiter::new(1_000, wait_metric.clone(), validator_matrix); - - let start = Instant::now(); - - // Parallel test, 5 non-validators sharing 1000 bytes per second. Each sends 1001 bytes, so - // total time is expected to be just over 5 seconds. - let join_handles = (0..5) - .map(|_| { - limiter.create_handle(NodeId::random(&mut rng), Some(PublicKey::random(&mut rng))) - }) - .map(|handle| { - tokio::spawn(async move { - handle.request_allowance(500).await; - handle.request_allowance(150).await; - handle.request_allowance(350).await; - handle.request_allowance(1).await; - }) - }); - - for join_handle in join_handles { - join_handle.await.expect("could not join task"); - } - - let elapsed = start.elapsed(); - assert!(elapsed >= Duration::from_secs(5)); - assert!(elapsed <= Duration::from_secs(6)); - - // Ensure metrics recorded the correct number of seconds. - assert!( - wait_metric.get() <= 6.0, - "wait metric is too large: {}", - wait_metric.get() - ); - - // Note: The limiting will not apply to all data, so it should be slightly below 5 seconds. - assert!( - wait_metric.get() >= 4.5, - "wait metric is too small: {}", - wait_metric.get() - ); - } - - #[tokio::test] - async fn inactive_validators_unlimited_when_no_validators_known() { - init_logging(); - - let mut rng = crate::new_rng(); - - let secret_key = SecretKey::random(&mut rng); - let consensus_key = PublicKey::from(&secret_key); - let wait_metric = new_wait_time_sec(); - let limiter = Limiter::new( - 1_000, - wait_metric.clone(), - ValidatorMatrix::new( - Ratio::new(1, 3), - None, - EraId::from(0), - Arc::new(secret_key), - consensus_key.clone(), - 2, - ), - ); - - // Try with non-validators or unknown nodes. - let handles = vec![ - limiter.create_handle(NodeId::random(&mut rng), Some(PublicKey::random(&mut rng))), - limiter.create_handle(NodeId::random(&mut rng), None), - ]; - - for handle in handles { - let start = Instant::now(); - - // Send 9_0001 bytes, should now finish instantly. - handle.request_allowance(1000).await; - handle.request_allowance(1000).await; - handle.request_allowance(1000).await; - handle.request_allowance(2000).await; - handle.request_allowance(4000).await; - handle.request_allowance(1).await; - assert!(start.elapsed() < SHORT_TIME); - } - - // There should have been no time spent waiting. - assert!( - wait_metric.get() < SHORT_TIME.as_secs_f64(), - "wait_metric is too large: {}", - wait_metric.get() - ); - } - - /// Regression test for #2929. - #[tokio::test] - async fn throttling_of_non_validators_does_not_affect_validators() { - init_logging(); - - let mut rng = crate::new_rng(); - - let secret_key = SecretKey::random(&mut rng); - let consensus_key = PublicKey::from(&secret_key); - let validator_matrix = ValidatorMatrix::new_with_validator(Arc::new(secret_key)); - let limiter = Limiter::new(1_000, new_wait_time_sec(), validator_matrix); - - let non_validator_handle = limiter.create_handle(NodeId::random(&mut rng), None); - let validator_handle = limiter.create_handle(NodeId::random(&mut rng), Some(consensus_key)); - - // We request a large resource at once using a non-validator handle. At the same time, - // validator requests should be still served, even while waiting for the long-delayed - // request still blocking. - let start = Instant::now(); - let background_nv_request = tokio::spawn(async move { - non_validator_handle.request_allowance(5000).await; - non_validator_handle.request_allowance(5000).await; - - Instant::now() - }); - - // Allow for a little bit of time to pass to ensure the background task is running. - tokio::time::sleep(Duration::from_secs(1)).await; - - validator_handle.request_allowance(10000).await; - validator_handle.request_allowance(10000).await; - - let v_finished = Instant::now(); - - let nv_finished = background_nv_request - .await - .expect("failed to join background nv task"); - - let nv_completed = nv_finished.duration_since(start); - assert!( - nv_completed >= Duration::from_millis(4500), - "non-validator did not delay sufficiently: {:?}", - nv_completed - ); - - let v_completed = v_finished.duration_since(start); - assert!( - v_completed <= Duration::from_millis(1500), - "validator did not finish quickly enough: {:?}", - v_completed - ); - } -} diff --git a/node/src/components/network/message.rs b/node/src/components/network/message.rs index c40b98c0d2..fa41a799b5 100644 --- a/node/src/components/network/message.rs +++ b/node/src/components/network/message.rs @@ -4,20 +4,20 @@ use std::{ sync::Arc, }; -use datasize::DataSize; use futures::future::BoxFuture; +use juliet::ChannelId; use serde::{ de::{DeserializeOwned, Error as SerdeError}, Deserialize, Deserializer, Serialize, Serializer, }; -use strum::EnumDiscriminants; +use strum::{Display, EnumCount, EnumDiscriminants, EnumIter, FromRepr}; use casper_hashing::Digest; #[cfg(test)] use casper_types::testing::TestRng; use casper_types::{crypto, AsymmetricType, ProtocolVersion, PublicKey, SecretKey, Signature}; -use super::{counting_format::ConnectionId, health::Nonce, BincodeFormat}; +use super::{connection_id::ConnectionId, serialize_network_message, Ticket}; use crate::{ effect::EffectBuilder, protocol, @@ -49,34 +49,20 @@ pub(crate) enum Message

{ /// A self-signed certificate indicating validator status. #[serde(default)] consensus_certificate: Option, - /// True if the node is syncing. - #[serde(default)] - is_syncing: bool, /// Hash of the chainspec the node is running. #[serde(default)] chainspec_hash: Option, }, - /// A ping request. - Ping { - /// The nonce to be returned with the pong. - nonce: Nonce, - }, - /// A pong response. - Pong { - /// Nonce to match pong to ping. - nonce: Nonce, - }, Payload(P), } impl Message

{ /// Classifies a message based on its payload. #[inline] + #[allow(dead_code)] // TODO: Re-add, once decision is made whether to keep message classses. pub(super) fn classify(&self) -> MessageKind { match self { - Message::Handshake { .. } | Message::Ping { .. } | Message::Pong { .. } => { - MessageKind::Protocol - } + Message::Handshake { .. } => MessageKind::Protocol, Message::Payload(payload) => payload.message_kind(), } } @@ -85,59 +71,22 @@ impl Message

{ #[inline] pub(super) fn is_low_priority(&self) -> bool { match self { - Message::Handshake { .. } | Message::Ping { .. } | Message::Pong { .. } => false, + Message::Handshake { .. } => false, Message::Payload(payload) => payload.is_low_priority(), } } - /// Returns the incoming resource estimate of the payload. - #[inline] - pub(super) fn payload_incoming_resource_estimate(&self, weights: &EstimatorWeights) -> u32 { - match self { - Message::Handshake { .. } => 0, - // Ping and Pong have a hardcoded weights. Since every ping will result in a pong being - // sent as a reply, it has a higher weight. - Message::Ping { .. } => 2, - Message::Pong { .. } => 1, - Message::Payload(payload) => payload.incoming_resource_estimate(weights), - } - } - - /// Returns whether or not the payload is unsafe for syncing node consumption. - #[inline] - pub(super) fn payload_is_unsafe_for_syncing_nodes(&self) -> bool { - match self { - Message::Handshake { .. } | Message::Ping { .. } | Message::Pong { .. } => false, - Message::Payload(payload) => payload.is_unsafe_for_syncing_peers(), - } - } - - /// Attempts to create a demand-event from this message. - /// - /// Succeeds if the outer message contains a payload that can be converted into a demand. - pub(super) fn try_into_demand( - self, - effect_builder: EffectBuilder, - sender: NodeId, - ) -> Result<(REv, BoxFuture<'static, Option

>), Box> - where - REv: FromIncoming

+ Send, - { + /// Determine which channel this message should be sent on. + pub(super) fn get_channel(&self) -> Channel { match self { - Message::Handshake { .. } | Message::Ping { .. } | Message::Pong { .. } => { - Err(self.into()) - } - Message::Payload(payload) => { - // Note: For now, the wrapping/unwrap of the payload is a bit unfortunate here. - REv::try_demand_from_incoming(effect_builder, sender, payload) - .map_err(|err| Message::Payload(err).into()) - } + Message::Handshake { .. } => Channel::Network, + Message::Payload(payload) => payload.get_channel(), } } } /// A pair of secret keys used by consensus. -pub(super) struct NodeKeyPair { +pub(crate) struct NodeKeyPair { secret_key: Arc, public_key: PublicKey, } @@ -155,6 +104,11 @@ impl NodeKeyPair { fn sign>(&self, value: T) -> Signature { crypto::sign(value, &self.secret_key, &self.public_key) } + + /// Returns a reference to the public key of this key pair. + pub(super) fn public_key(&self) -> &PublicKey { + &self.public_key + } } /// Certificate used to indicate that the peer is a validator using the specified public key. @@ -291,22 +245,19 @@ impl Display for Message

{ public_addr, protocol_version, consensus_certificate, - is_syncing, chainspec_hash, } => { write!( f, - "handshake: {}, public addr: {}, protocol_version: {}, consensus_certificate: {}, is_syncing: {}, chainspec_hash: {}", + "handshake: {}, public addr: {}, protocol_version: {}, consensus_certificate: {}, chainspec_hash: {}", network_name, public_addr, protocol_version, OptDisplay::new(consensus_certificate.as_ref(), "none"), - is_syncing, + OptDisplay::new(chainspec_hash.as_ref(), "none") ) } - Message::Ping { nonce } => write!(f, "ping({})", nonce), - Message::Pong { nonce } => write!(f, "pong({})", nonce), Message::Payload(payload) => write!(f, "payload: {}", payload), } } @@ -314,6 +265,7 @@ impl Display for Message

{ /// A classification system for networking messages. #[derive(Copy, Clone, Debug)] +#[allow(dead_code)] // TODO: Re-add, once decision is made whether or not to keep message classses. pub(crate) enum MessageKind { /// Non-payload messages, like handshakes. Protocol, @@ -354,34 +306,71 @@ impl Display for MessageKind { } } +/// Multiplexed channel identifier used across a single connection. +/// +/// Channels are separated mainly to avoid deadlocking issues where two nodes requests a large +/// amount of items from each other simultaneously, with responses being queued behind requests, +/// whilst the latter are buffered due to backpressure. +/// +/// Further separation is done to improve quality of service of certain subsystems, e.g. to +/// guarantee that consensus is not impaired by the transfer of large trie nodes. +#[derive( + Copy, Clone, Debug, Display, Eq, EnumCount, EnumIter, FromRepr, PartialEq, Ord, PartialOrd, +)] +#[repr(u8)] +pub enum Channel { + /// Networking layer messages, handshakes and ping/pong. + Network = 0, + /// Data solely used for syncing being requested. + /// + /// We separate sync data (e.g. trie nodes) requests from regular ("data") requests since the + /// former are not required for a validating node to make progress on consensus, thus + /// separating these can improve latency. + SyncDataRequests = 1, + /// Sync data requests being answered. + /// + /// Responses are separated from requests to ensure liveness (see [`Channel`] documentation). + SyncDataResponses = 2, + /// Requests for data used during regular validator operation. + DataRequests = 3, + /// Responses for data used during regular validator operation. + DataResponses = 4, + /// Consensus-level messages, like finality signature announcements and consensus messages. + Consensus = 5, + /// Regular gossip announcements and responses (e.g. for deploys and blocks). + BulkGossip = 6, +} + +impl Channel { + #[inline(always)] + pub(crate) fn into_channel_id(self) -> ChannelId { + ChannelId::new(self as u8) + } +} + /// Network message payload. /// /// Payloads are what is transferred across the network outside of control messages from the /// networking component itself. pub(crate) trait Payload: - Serialize + DeserializeOwned + Clone + Debug + Display + Send + Sync + 'static + Serialize + DeserializeOwned + Clone + Debug + Display + Send + Sync + Unpin + 'static { /// Classifies the payload based on its contents. fn message_kind(&self) -> MessageKind; - /// The penalty for resource usage of a message to be applied when processed as incoming. - fn incoming_resource_estimate(&self, _weights: &EstimatorWeights) -> u32; - /// Determines if the payload should be considered low priority. fn is_low_priority(&self) -> bool { false } - /// Indicates a message is not safe to send to a syncing node. - /// - /// This functionality should be removed once multiplexed networking lands. - fn is_unsafe_for_syncing_peers(&self) -> bool; + /// Determine which channel a message is supposed to sent/received on. + fn get_channel(&self) -> Channel; } /// Network message conversion support. pub(crate) trait FromIncoming

{ /// Creates a new value from a received payload. - fn from_incoming(sender: NodeId, payload: P) -> Self; + fn from_incoming(sender: NodeId, payload: P, ticket: Ticket) -> Self; /// Tries to convert a payload into a demand. /// @@ -401,38 +390,6 @@ pub(crate) trait FromIncoming

{ Err(payload) } } -/// A generic configuration for payload weights. -/// -/// Implementors of `Payload` are free to interpret this as they see fit. -/// -/// The default implementation sets all weights to zero. -#[derive(DataSize, Debug, Default, Clone, Deserialize, Serialize)] -pub struct EstimatorWeights { - pub consensus: u32, - pub block_gossip: u32, - pub deploy_gossip: u32, - pub finality_signature_gossip: u32, - pub address_gossip: u32, - pub finality_signature_broadcasts: u32, - pub deploy_requests: u32, - pub deploy_responses: u32, - pub legacy_deploy_requests: u32, - pub legacy_deploy_responses: u32, - pub block_requests: u32, - pub block_responses: u32, - pub block_header_requests: u32, - pub block_header_responses: u32, - pub trie_requests: u32, - pub trie_responses: u32, - pub finality_signature_requests: u32, - pub finality_signature_responses: u32, - pub sync_leap_requests: u32, - pub sync_leap_responses: u32, - pub approvals_hashes_requests: u32, - pub approvals_hashes_responses: u32, - pub execution_results_requests: u32, - pub execution_results_responses: u32, -} mod specimen_support { use std::iter; @@ -462,15 +419,8 @@ mod specimen_support { public_addr: LargestSpecimen::largest_specimen(estimator, cache), protocol_version: LargestSpecimen::largest_specimen(estimator, cache), consensus_certificate: LargestSpecimen::largest_specimen(estimator, cache), - is_syncing: LargestSpecimen::largest_specimen(estimator, cache), chainspec_hash: LargestSpecimen::largest_specimen(estimator, cache), }, - MessageDiscriminants::Ping => Message::Ping { - nonce: LargestSpecimen::largest_specimen(estimator, cache), - }, - MessageDiscriminants::Pong => Message::Pong { - nonce: LargestSpecimen::largest_specimen(estimator, cache), - }, MessageDiscriminants::Payload => { Message::Payload(LargestSpecimen::largest_specimen(estimator, cache)) } @@ -561,18 +511,6 @@ impl<'a> NetworkMessageEstimator<'a> { } } -/// Encoding helper function. -/// -/// Encodes a message in the same manner the network component would before sending it. -fn serialize_net_message(data: &T) -> Vec -where - T: Serialize, -{ - BincodeFormat::default() - .serialize_arbitrary(data) - .expect("did not expect serialization to fail") -} - /// Creates a serialized specimen of the largest possible networking message. pub(crate) fn generate_largest_message(chainspec: &Chainspec) -> Message { let estimator = &NetworkMessageEstimator::new(chainspec); @@ -582,12 +520,16 @@ pub(crate) fn generate_largest_message(chainspec: &Chainspec) -> Message Vec { - serialize_net_message(&generate_largest_message(chainspec)) + serialize_network_message(&generate_largest_message(chainspec)) + .expect("did not expect serialization to fail") // it would fail in `SizeEstimator` before failing here + .into() } impl<'a> SizeEstimator for NetworkMessageEstimator<'a> { fn estimate(&self, val: &T) -> usize { - serialize_net_message(&val).len() + serialize_network_message(&val) + .expect("could not serialize given item with network encoding") + .len() } fn parameter>(&self, name: &'static str) -> T { @@ -608,15 +550,13 @@ impl<'a> SizeEstimator for NetworkMessageEstimator<'a> { // We use a variety of weird names in these tests. #[allow(non_camel_case_types)] mod tests { - use std::{net::SocketAddr, pin::Pin}; + use std::net::SocketAddr; use assert_matches::assert_matches; - use bytes::BytesMut; use casper_types::ProtocolVersion; use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use tokio_serde::{Deserializer, Serializer}; - use crate::{components::network::message_pack_format::MessagePackFormat, protocol}; + use crate::{components::network::handshake, protocol}; use super::*; @@ -700,22 +640,12 @@ mod tests { /// Serialize a message using the standard serialization method for handshakes. fn serialize_message(msg: &M) -> Vec { - let mut serializer = MessagePackFormat; - - Pin::new(&mut serializer) - .serialize(&msg) - .expect("handshake serialization failed") - .into_iter() - .collect() + handshake::serialize(msg).expect("handshake serialization failed") } /// Deserialize a message using the standard deserialization method for handshakes. fn deserialize_message(serialized: &[u8]) -> M { - let mut deserializer = MessagePackFormat; - - Pin::new(&mut deserializer) - .deserialize(&BytesMut::from(serialized)) - .expect("message deserialization failed") + handshake::deserialize(serialized).expect("message deserialization failed") } /// Given a message `from` of type `F`, serializes it, then deserializes it as `T`. @@ -766,7 +696,6 @@ mod tests { public_addr: ([12, 34, 56, 78], 12346).into(), protocol_version: ProtocolVersion::from_parts(5, 6, 7), consensus_certificate: Some(ConsensusCertificate::random(&mut rng)), - is_syncing: false, chainspec_hash: Some(Digest::hash("example-chainspec")), }; @@ -800,7 +729,6 @@ mod tests { public_addr, protocol_version, consensus_certificate, - is_syncing, chainspec_hash, } = modern_handshake { @@ -808,7 +736,6 @@ mod tests { assert_eq!(public_addr, ([12, 34, 56, 78], 12346).into()); assert_eq!(protocol_version, ProtocolVersion::V1_0_0); assert!(consensus_certificate.is_none()); - assert!(!is_syncing); assert!(chainspec_hash.is_none()) } else { panic!("did not expect modern handshake to deserialize to anything but") @@ -824,16 +751,13 @@ mod tests { public_addr, protocol_version, consensus_certificate, - is_syncing, chainspec_hash, } = modern_handshake { - assert!(!is_syncing); assert_eq!(network_name, "serialization-test"); assert_eq!(public_addr, ([12, 34, 56, 78], 12346).into()); assert_eq!(protocol_version, ProtocolVersion::V1_0_0); assert!(consensus_certificate.is_none()); - assert!(!is_syncing); assert!(chainspec_hash.is_none()) } else { panic!("did not expect modern handshake to deserialize to anything but") @@ -849,14 +773,12 @@ mod tests { public_addr, protocol_version, consensus_certificate, - is_syncing, chainspec_hash, } = modern_handshake { assert_eq!(network_name, "example-handshake"); assert_eq!(public_addr, ([12, 34, 56, 78], 12346).into()); assert_eq!(protocol_version, ProtocolVersion::from_parts(1, 4, 2)); - assert!(!is_syncing); let ConsensusCertificate { public_key, signature, @@ -877,7 +799,6 @@ mod tests { ) .unwrap() ); - assert!(!is_syncing); assert!(chainspec_hash.is_none()) } else { panic!("did not expect modern handshake to deserialize to anything but") @@ -893,11 +814,9 @@ mod tests { public_addr, protocol_version, consensus_certificate, - is_syncing, chainspec_hash, } = modern_handshake { - assert!(!is_syncing); assert_eq!(network_name, "example-handshake"); assert_eq!(public_addr, ([12, 34, 56, 78], 12346).into()); assert_eq!(protocol_version, ProtocolVersion::from_parts(1, 4, 3)); @@ -921,7 +840,6 @@ mod tests { ) .unwrap() ); - assert!(!is_syncing); assert!(chainspec_hash.is_none()) } else { panic!("did not expect modern handshake to deserialize to anything but") @@ -952,6 +870,14 @@ mod tests { roundtrip_certificate(false) } + #[test] + fn channels_enum_does_not_have_holes() { + for idx in 0..Channel::COUNT { + let result = Channel::from_repr(idx as u8); + result.expect("must not have holes in channel enum"); + } + } + #[test] fn assert_the_largest_specimen_type_and_size() { let (chainspec, _) = crate::utils::Loadable::from_resources("production"); @@ -963,7 +889,7 @@ mod tests { "the type of the largest possible network message based on the production chainspec has changed" ); - let serialized = serialize_net_message(&specimen); + let serialized = serialize_network_message(&specimen).expect("serialization failed"); assert_eq!( serialized.len(), diff --git a/node/src/components/network/message_pack_format.rs b/node/src/components/network/message_pack_format.rs deleted file mode 100644 index 27a9ee2457..0000000000 --- a/node/src/components/network/message_pack_format.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Message pack wire format encoder. -//! -//! This module is used to pin the correct version of message pack used throughout the codebase to -//! our network decoder via `Cargo.toml`; using `tokio_serde::MessagePack` would instead tie it -//! to the dependency specified in `tokio_serde`'s `Cargo.toml`. - -use std::{ - io::{self, Cursor}, - pin::Pin, -}; - -use bytes::{Bytes, BytesMut}; -use serde::{Deserialize, Serialize}; -use tokio_serde::{Deserializer, Serializer}; - -/// msgpack encoder/decoder for messages. -#[derive(Debug)] -pub struct MessagePackFormat; - -impl Serializer for MessagePackFormat -where - M: Serialize, -{ - // Note: We cast to `io::Error` because of the `Codec::Error: Into` - // requirement. - type Error = io::Error; - - #[inline] - fn serialize(self: Pin<&mut Self>, item: &M) -> Result { - rmp_serde::to_vec(item) - .map(Into::into) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } -} - -impl Deserializer for MessagePackFormat -where - for<'de> M: Deserialize<'de>, -{ - type Error = io::Error; - - #[inline] - fn deserialize(self: Pin<&mut Self>, src: &BytesMut) -> Result { - rmp_serde::from_read(Cursor::new(src)) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } -} diff --git a/node/src/components/network/metrics.rs b/node/src/components/network/metrics.rs index a407b6885a..1ba0adae91 100644 --- a/node/src/components/network/metrics.rs +++ b/node/src/components/network/metrics.rs @@ -4,399 +4,337 @@ use prometheus::{Counter, IntCounter, IntGauge, Registry}; use tracing::debug; use super::{outgoing::OutgoingMetrics, MessageKind}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Network-type agnostic networking metrics. #[derive(Debug)] pub(super) struct Metrics { /// How often a request was made by a component to broadcast. - pub(super) broadcast_requests: IntCounter, + pub(super) broadcast_requests: RegisteredMetric, /// How often a request to send a message directly to a peer was made. - pub(super) direct_message_requests: IntCounter, + pub(super) direct_message_requests: RegisteredMetric, /// Number of messages still waiting to be sent out (broadcast and direct). - pub(super) queued_messages: IntGauge, + pub(super) queued_messages: RegisteredMetric, /// Number of connected peers. - pub(super) peers: IntGauge, + pub(super) peers: RegisteredMetric, /// Count of outgoing messages that are protocol overhead. - pub(super) out_count_protocol: IntCounter, + pub(super) out_count_protocol: RegisteredMetric, /// Count of outgoing messages with consensus payload. - pub(super) out_count_consensus: IntCounter, + pub(super) out_count_consensus: RegisteredMetric, /// Count of outgoing messages with deploy gossiper payload. - pub(super) out_count_deploy_gossip: IntCounter, - pub(super) out_count_block_gossip: IntCounter, - pub(super) out_count_finality_signature_gossip: IntCounter, + pub(super) out_count_deploy_gossip: RegisteredMetric, + pub(super) out_count_block_gossip: RegisteredMetric, + pub(super) out_count_finality_signature_gossip: RegisteredMetric, /// Count of outgoing messages with address gossiper payload. - pub(super) out_count_address_gossip: IntCounter, + pub(super) out_count_address_gossip: RegisteredMetric, /// Count of outgoing messages with deploy request/response payload. - pub(super) out_count_deploy_transfer: IntCounter, + pub(super) out_count_deploy_transfer: RegisteredMetric, /// Count of outgoing messages with block request/response payload. - pub(super) out_count_block_transfer: IntCounter, + pub(super) out_count_block_transfer: RegisteredMetric, /// Count of outgoing messages with trie request/response payload. - pub(super) out_count_trie_transfer: IntCounter, + pub(super) out_count_trie_transfer: RegisteredMetric, /// Count of outgoing messages with other payload. - pub(super) out_count_other: IntCounter, + pub(super) out_count_other: RegisteredMetric, /// Volume in bytes of outgoing messages that are protocol overhead. - pub(super) out_bytes_protocol: IntCounter, + pub(super) out_bytes_protocol: RegisteredMetric, /// Volume in bytes of outgoing messages with consensus payload. - pub(super) out_bytes_consensus: IntCounter, + pub(super) out_bytes_consensus: RegisteredMetric, /// Volume in bytes of outgoing messages with deploy gossiper payload. - pub(super) out_bytes_deploy_gossip: IntCounter, - pub(super) out_bytes_block_gossip: IntCounter, - pub(super) out_bytes_finality_signature_gossip: IntCounter, + pub(super) out_bytes_deploy_gossip: RegisteredMetric, + /// Volume in bytes of outgoing messages with block gossiper payload. + pub(super) out_bytes_block_gossip: RegisteredMetric, + /// Volume in bytes of outgoing messages with finality signature payload. + pub(super) out_bytes_finality_signature_gossip: RegisteredMetric, /// Volume in bytes of outgoing messages with address gossiper payload. - pub(super) out_bytes_address_gossip: IntCounter, + pub(super) out_bytes_address_gossip: RegisteredMetric, /// Volume in bytes of outgoing messages with deploy request/response payload. - pub(super) out_bytes_deploy_transfer: IntCounter, + pub(super) out_bytes_deploy_transfer: RegisteredMetric, /// Volume in bytes of outgoing messages with block request/response payload. - pub(super) out_bytes_block_transfer: IntCounter, + pub(super) out_bytes_block_transfer: RegisteredMetric, /// Volume in bytes of outgoing messages with block request/response payload. - pub(super) out_bytes_trie_transfer: IntCounter, + pub(super) out_bytes_trie_transfer: RegisteredMetric, /// Volume in bytes of outgoing messages with other payload. - pub(super) out_bytes_other: IntCounter, + pub(super) out_bytes_other: RegisteredMetric, /// Number of outgoing connections in connecting state. - pub(super) out_state_connecting: IntGauge, + pub(super) out_state_connecting: RegisteredMetric, /// Number of outgoing connections in waiting state. - pub(super) out_state_waiting: IntGauge, + pub(super) out_state_waiting: RegisteredMetric, /// Number of outgoing connections in connected state. - pub(super) out_state_connected: IntGauge, + pub(super) out_state_connected: RegisteredMetric, /// Number of outgoing connections in blocked state. - pub(super) out_state_blocked: IntGauge, + pub(super) out_state_blocked: RegisteredMetric, /// Number of outgoing connections in loopback state. - pub(super) out_state_loopback: IntGauge, + pub(super) out_state_loopback: RegisteredMetric, /// Volume in bytes of incoming messages that are protocol overhead. - pub(super) in_bytes_protocol: IntCounter, + pub(super) in_bytes_protocol: RegisteredMetric, /// Volume in bytes of incoming messages with consensus payload. - pub(super) in_bytes_consensus: IntCounter, + pub(super) in_bytes_consensus: RegisteredMetric, /// Volume in bytes of incoming messages with deploy gossiper payload. - pub(super) in_bytes_deploy_gossip: IntCounter, - pub(super) in_bytes_block_gossip: IntCounter, - pub(super) in_bytes_finality_signature_gossip: IntCounter, + pub(super) in_bytes_deploy_gossip: RegisteredMetric, + /// Volume in bytes of incoming messages with block gossiper payload. + pub(super) in_bytes_block_gossip: RegisteredMetric, + /// Volume in bytes of incoming messages with finality signature gossiper payload. + pub(super) in_bytes_finality_signature_gossip: RegisteredMetric, /// Volume in bytes of incoming messages with address gossiper payload. - pub(super) in_bytes_address_gossip: IntCounter, + pub(super) in_bytes_address_gossip: RegisteredMetric, /// Volume in bytes of incoming messages with deploy request/response payload. - pub(super) in_bytes_deploy_transfer: IntCounter, + pub(super) in_bytes_deploy_transfer: RegisteredMetric, /// Volume in bytes of incoming messages with block request/response payload. - pub(super) in_bytes_block_transfer: IntCounter, + pub(super) in_bytes_block_transfer: RegisteredMetric, /// Volume in bytes of incoming messages with block request/response payload. - pub(super) in_bytes_trie_transfer: IntCounter, + pub(super) in_bytes_trie_transfer: RegisteredMetric, /// Volume in bytes of incoming messages with other payload. - pub(super) in_bytes_other: IntCounter, + pub(super) in_bytes_other: RegisteredMetric, /// Count of incoming messages that are protocol overhead. - pub(super) in_count_protocol: IntCounter, + pub(super) in_count_protocol: RegisteredMetric, /// Count of incoming messages with consensus payload. - pub(super) in_count_consensus: IntCounter, + pub(super) in_count_consensus: RegisteredMetric, /// Count of incoming messages with deploy gossiper payload. - pub(super) in_count_deploy_gossip: IntCounter, - pub(super) in_count_block_gossip: IntCounter, - pub(super) in_count_finality_signature_gossip: IntCounter, + pub(super) in_count_deploy_gossip: RegisteredMetric, + /// Count of incoming messages with block gossiper payload. + pub(super) in_count_block_gossip: RegisteredMetric, + /// Count of incoming messages with finality signature gossiper payload. + pub(super) in_count_finality_signature_gossip: RegisteredMetric, /// Count of incoming messages with address gossiper payload. - pub(super) in_count_address_gossip: IntCounter, + pub(super) in_count_address_gossip: RegisteredMetric, /// Count of incoming messages with deploy request/response payload. - pub(super) in_count_deploy_transfer: IntCounter, + pub(super) in_count_deploy_transfer: RegisteredMetric, /// Count of incoming messages with block request/response payload. - pub(super) in_count_block_transfer: IntCounter, + pub(super) in_count_block_transfer: RegisteredMetric, /// Count of incoming messages with trie request/response payload. - pub(super) in_count_trie_transfer: IntCounter, + pub(super) in_count_trie_transfer: RegisteredMetric, /// Count of incoming messages with other payload. - pub(super) in_count_other: IntCounter, + pub(super) in_count_other: RegisteredMetric, /// Number of trie requests accepted for processing. - pub(super) requests_for_trie_accepted: IntCounter, + pub(super) requests_for_trie_accepted: RegisteredMetric, /// Number of trie requests finished (successful or unsuccessful). - pub(super) requests_for_trie_finished: IntCounter, + pub(super) requests_for_trie_finished: RegisteredMetric, /// Total time spent delaying outgoing traffic to non-validators due to limiter, in seconds. - pub(super) accumulated_outgoing_limiter_delay: Counter, - /// Total time spent delaying incoming traffic from non-validators due to limiter, in seconds. - pub(super) accumulated_incoming_limiter_delay: Counter, - - /// Registry instance. - registry: Registry, + #[allow(dead_code)] // Metric kept for backwards compabitility. + pub(super) accumulated_outgoing_limiter_delay: RegisteredMetric, } impl Metrics { /// Creates a new instance of networking metrics. pub(super) fn new(registry: &Registry) -> Result { - let broadcast_requests = - IntCounter::new("net_broadcast_requests", "number of broadcasting requests")?; - let direct_message_requests = IntCounter::new( + let broadcast_requests = registry + .new_int_counter("net_broadcast_requests", "number of broadcasting requests")?; + let direct_message_requests = registry.new_int_counter( "net_direct_message_requests", "number of requests to send a message directly to a peer", )?; - let queued_messages = IntGauge::new( + + let queued_messages = registry.new_int_gauge( "net_queued_direct_messages", "number of messages waiting to be sent out", )?; - let peers = IntGauge::new("peers", "number of connected peers")?; + let peers = registry.new_int_gauge("peers", "number of connected peers")?; - let out_count_protocol = IntCounter::new( + let out_count_protocol = registry.new_int_counter( "net_out_count_protocol", "count of outgoing messages that are protocol overhead", )?; - let out_count_consensus = IntCounter::new( + let out_count_consensus = registry.new_int_counter( "net_out_count_consensus", "count of outgoing messages with consensus payload", )?; - let out_count_deploy_gossip = IntCounter::new( + let out_count_deploy_gossip = registry.new_int_counter( "net_out_count_deploy_gossip", "count of outgoing messages with deploy gossiper payload", )?; - let out_count_block_gossip = IntCounter::new( + let out_count_block_gossip = registry.new_int_counter( "net_out_count_block_gossip", "count of outgoing messages with block gossiper payload", )?; - let out_count_finality_signature_gossip = IntCounter::new( + let out_count_finality_signature_gossip = registry.new_int_counter( "net_out_count_finality_signature_gossip", "count of outgoing messages with finality signature gossiper payload", )?; - let out_count_address_gossip = IntCounter::new( + let out_count_address_gossip = registry.new_int_counter( "net_out_count_address_gossip", "count of outgoing messages with address gossiper payload", )?; - let out_count_deploy_transfer = IntCounter::new( + let out_count_deploy_transfer = registry.new_int_counter( "net_out_count_deploy_transfer", "count of outgoing messages with deploy request/response payload", )?; - let out_count_block_transfer = IntCounter::new( + let out_count_block_transfer = registry.new_int_counter( "net_out_count_block_transfer", "count of outgoing messages with block request/response payload", )?; - let out_count_trie_transfer = IntCounter::new( + let out_count_trie_transfer = registry.new_int_counter( "net_out_count_trie_transfer", "count of outgoing messages with trie payloads", )?; - let out_count_other = IntCounter::new( + let out_count_other = registry.new_int_counter( "net_out_count_other", "count of outgoing messages with other payload", )?; - let out_bytes_protocol = IntCounter::new( + let out_bytes_protocol = registry.new_int_counter( "net_out_bytes_protocol", "volume in bytes of outgoing messages that are protocol overhead", )?; - let out_bytes_consensus = IntCounter::new( + let out_bytes_consensus = registry.new_int_counter( "net_out_bytes_consensus", "volume in bytes of outgoing messages with consensus payload", )?; - let out_bytes_deploy_gossip = IntCounter::new( + let out_bytes_deploy_gossip = registry.new_int_counter( "net_out_bytes_deploy_gossip", "volume in bytes of outgoing messages with deploy gossiper payload", )?; - let out_bytes_block_gossip = IntCounter::new( + let out_bytes_block_gossip = registry.new_int_counter( "net_out_bytes_block_gossip", "volume in bytes of outgoing messages with block gossiper payload", )?; - let out_bytes_finality_signature_gossip = IntCounter::new( + let out_bytes_finality_signature_gossip = registry.new_int_counter( "net_out_bytes_finality_signature_gossip", "volume in bytes of outgoing messages with finality signature gossiper payload", )?; - let out_bytes_address_gossip = IntCounter::new( + let out_bytes_address_gossip = registry.new_int_counter( "net_out_bytes_address_gossip", "volume in bytes of outgoing messages with address gossiper payload", )?; - let out_bytes_deploy_transfer = IntCounter::new( + let out_bytes_deploy_transfer = registry.new_int_counter( "net_out_bytes_deploy_transfer", "volume in bytes of outgoing messages with deploy request/response payload", )?; - let out_bytes_block_transfer = IntCounter::new( + let out_bytes_block_transfer = registry.new_int_counter( "net_out_bytes_block_transfer", "volume in bytes of outgoing messages with block request/response payload", )?; - let out_bytes_trie_transfer = IntCounter::new( + let out_bytes_trie_transfer = registry.new_int_counter( "net_out_bytes_trie_transfer", "volume in bytes of outgoing messages with trie payloads", )?; - let out_bytes_other = IntCounter::new( + let out_bytes_other = registry.new_int_counter( "net_out_bytes_other", "volume in bytes of outgoing messages with other payload", )?; - let out_state_connecting = IntGauge::new( + let out_state_connecting = registry.new_int_gauge( "out_state_connecting", "number of connections in the connecting state", )?; - let out_state_waiting = IntGauge::new( + let out_state_waiting = registry.new_int_gauge( "out_state_waiting", "number of connections in the waiting state", )?; - let out_state_connected = IntGauge::new( + let out_state_connected = registry.new_int_gauge( "out_state_connected", "number of connections in the connected state", )?; - let out_state_blocked = IntGauge::new( + let out_state_blocked = registry.new_int_gauge( "out_state_blocked", "number of connections in the blocked state", )?; - let out_state_loopback = IntGauge::new( + let out_state_loopback = registry.new_int_gauge( "out_state_loopback", "number of connections in the loopback state", )?; - let in_count_protocol = IntCounter::new( + let in_count_protocol = registry.new_int_counter( "net_in_count_protocol", "count of incoming messages that are protocol overhead", )?; - let in_count_consensus = IntCounter::new( + let in_count_consensus = registry.new_int_counter( "net_in_count_consensus", "count of incoming messages with consensus payload", )?; - let in_count_deploy_gossip = IntCounter::new( + let in_count_deploy_gossip = registry.new_int_counter( "net_in_count_deploy_gossip", "count of incoming messages with deploy gossiper payload", )?; - let in_count_block_gossip = IntCounter::new( + let in_count_block_gossip = registry.new_int_counter( "net_in_count_block_gossip", "count of incoming messages with block gossiper payload", )?; - let in_count_finality_signature_gossip = IntCounter::new( + let in_count_finality_signature_gossip = registry.new_int_counter( "net_in_count_finality_signature_gossip", "count of incoming messages with finality signature gossiper payload", )?; - let in_count_address_gossip = IntCounter::new( + let in_count_address_gossip = registry.new_int_counter( "net_in_count_address_gossip", "count of incoming messages with address gossiper payload", )?; - let in_count_deploy_transfer = IntCounter::new( + let in_count_deploy_transfer = registry.new_int_counter( "net_in_count_deploy_transfer", "count of incoming messages with deploy request/response payload", )?; - let in_count_block_transfer = IntCounter::new( + let in_count_block_transfer = registry.new_int_counter( "net_in_count_block_transfer", "count of incoming messages with block request/response payload", )?; - let in_count_trie_transfer = IntCounter::new( + let in_count_trie_transfer = registry.new_int_counter( "net_in_count_trie_transfer", "count of incoming messages with trie payloads", )?; - let in_count_other = IntCounter::new( + let in_count_other = registry.new_int_counter( "net_in_count_other", "count of incoming messages with other payload", )?; - let in_bytes_protocol = IntCounter::new( + let in_bytes_protocol = registry.new_int_counter( "net_in_bytes_protocol", "volume in bytes of incoming messages that are protocol overhead", )?; - let in_bytes_consensus = IntCounter::new( + let in_bytes_consensus = registry.new_int_counter( "net_in_bytes_consensus", "volume in bytes of incoming messages with consensus payload", )?; - let in_bytes_deploy_gossip = IntCounter::new( + let in_bytes_deploy_gossip = registry.new_int_counter( "net_in_bytes_deploy_gossip", "volume in bytes of incoming messages with deploy gossiper payload", )?; - let in_bytes_block_gossip = IntCounter::new( + let in_bytes_block_gossip = registry.new_int_counter( "net_in_bytes_block_gossip", "volume in bytes of incoming messages with block gossiper payload", )?; - let in_bytes_finality_signature_gossip = IntCounter::new( + let in_bytes_finality_signature_gossip = registry.new_int_counter( "net_in_bytes_finality_signature_gossip", "volume in bytes of incoming messages with finality signature gossiper payload", )?; - let in_bytes_address_gossip = IntCounter::new( + let in_bytes_address_gossip = registry.new_int_counter( "net_in_bytes_address_gossip", "volume in bytes of incoming messages with address gossiper payload", )?; - let in_bytes_deploy_transfer = IntCounter::new( + let in_bytes_deploy_transfer = registry.new_int_counter( "net_in_bytes_deploy_transfer", "volume in bytes of incoming messages with deploy request/response payload", )?; - let in_bytes_block_transfer = IntCounter::new( + let in_bytes_block_transfer = registry.new_int_counter( "net_in_bytes_block_transfer", "volume in bytes of incoming messages with block request/response payload", )?; - let in_bytes_trie_transfer = IntCounter::new( + let in_bytes_trie_transfer = registry.new_int_counter( "net_in_bytes_trie_transfer", "volume in bytes of incoming messages with trie payloads", )?; - let in_bytes_other = IntCounter::new( + let in_bytes_other = registry.new_int_counter( "net_in_bytes_other", "volume in bytes of incoming messages with other payload", )?; - let requests_for_trie_accepted = IntCounter::new( + let requests_for_trie_accepted = registry.new_int_counter( "requests_for_trie_accepted", "number of trie requests accepted for processing", )?; - let requests_for_trie_finished = IntCounter::new( + let requests_for_trie_finished = registry.new_int_counter( "requests_for_trie_finished", "number of trie requests finished, successful or not", )?; - let accumulated_outgoing_limiter_delay = Counter::new( + let accumulated_outgoing_limiter_delay = registry.new_counter( "accumulated_outgoing_limiter_delay", "seconds spent delaying outgoing traffic to non-validators due to limiter, in seconds", )?; - let accumulated_incoming_limiter_delay = Counter::new( - "accumulated_incoming_limiter_delay", - "seconds spent delaying incoming traffic from non-validators due to limiter, in seconds." - )?; - - registry.register(Box::new(broadcast_requests.clone()))?; - registry.register(Box::new(direct_message_requests.clone()))?; - registry.register(Box::new(queued_messages.clone()))?; - registry.register(Box::new(peers.clone()))?; - - registry.register(Box::new(out_count_protocol.clone()))?; - registry.register(Box::new(out_count_consensus.clone()))?; - registry.register(Box::new(out_count_deploy_gossip.clone()))?; - registry.register(Box::new(out_count_block_gossip.clone()))?; - registry.register(Box::new(out_count_finality_signature_gossip.clone()))?; - registry.register(Box::new(out_count_address_gossip.clone()))?; - registry.register(Box::new(out_count_deploy_transfer.clone()))?; - registry.register(Box::new(out_count_block_transfer.clone()))?; - registry.register(Box::new(out_count_trie_transfer.clone()))?; - registry.register(Box::new(out_count_other.clone()))?; - - registry.register(Box::new(out_bytes_protocol.clone()))?; - registry.register(Box::new(out_bytes_consensus.clone()))?; - registry.register(Box::new(out_bytes_deploy_gossip.clone()))?; - registry.register(Box::new(out_bytes_block_gossip.clone()))?; - registry.register(Box::new(out_bytes_finality_signature_gossip.clone()))?; - registry.register(Box::new(out_bytes_address_gossip.clone()))?; - registry.register(Box::new(out_bytes_deploy_transfer.clone()))?; - registry.register(Box::new(out_bytes_block_transfer.clone()))?; - registry.register(Box::new(out_bytes_trie_transfer.clone()))?; - registry.register(Box::new(out_bytes_other.clone()))?; - - registry.register(Box::new(out_state_connecting.clone()))?; - registry.register(Box::new(out_state_waiting.clone()))?; - registry.register(Box::new(out_state_connected.clone()))?; - registry.register(Box::new(out_state_blocked.clone()))?; - registry.register(Box::new(out_state_loopback.clone()))?; - - registry.register(Box::new(in_count_protocol.clone()))?; - registry.register(Box::new(in_count_consensus.clone()))?; - registry.register(Box::new(in_count_deploy_gossip.clone()))?; - registry.register(Box::new(in_count_block_gossip.clone()))?; - registry.register(Box::new(in_count_finality_signature_gossip.clone()))?; - registry.register(Box::new(in_count_address_gossip.clone()))?; - registry.register(Box::new(in_count_deploy_transfer.clone()))?; - registry.register(Box::new(in_count_block_transfer.clone()))?; - registry.register(Box::new(in_count_trie_transfer.clone()))?; - registry.register(Box::new(in_count_other.clone()))?; - - registry.register(Box::new(in_bytes_protocol.clone()))?; - registry.register(Box::new(in_bytes_consensus.clone()))?; - registry.register(Box::new(in_bytes_deploy_gossip.clone()))?; - registry.register(Box::new(in_bytes_block_gossip.clone()))?; - registry.register(Box::new(in_bytes_finality_signature_gossip.clone()))?; - registry.register(Box::new(in_bytes_address_gossip.clone()))?; - registry.register(Box::new(in_bytes_deploy_transfer.clone()))?; - registry.register(Box::new(in_bytes_block_transfer.clone()))?; - registry.register(Box::new(in_bytes_trie_transfer.clone()))?; - registry.register(Box::new(in_bytes_other.clone()))?; - - registry.register(Box::new(requests_for_trie_accepted.clone()))?; - registry.register(Box::new(requests_for_trie_finished.clone()))?; - - registry.register(Box::new(accumulated_outgoing_limiter_delay.clone()))?; - registry.register(Box::new(accumulated_incoming_limiter_delay.clone()))?; Ok(Metrics { broadcast_requests, @@ -451,12 +389,12 @@ impl Metrics { requests_for_trie_accepted, requests_for_trie_finished, accumulated_outgoing_limiter_delay, - accumulated_incoming_limiter_delay, - registry: registry.clone(), }) } /// Records an outgoing payload. + #[allow(dead_code)] // TODO: Readd once metrics are tracked again. + pub(crate) fn record_payload_out(this: &Weak, kind: MessageKind, size: u64) { if let Some(metrics) = this.upgrade() { match kind { @@ -507,6 +445,7 @@ impl Metrics { } /// Records an incoming payload. + #[allow(dead_code)] // TODO: Readd once metrics are tracked again. pub(crate) fn record_payload_in(this: &Weak, kind: MessageKind, size: u64) { if let Some(metrics) = this.upgrade() { match kind { @@ -559,15 +498,16 @@ impl Metrics { /// Creates a set of outgoing metrics that is connected to this set of metrics. pub(super) fn create_outgoing_metrics(&self) -> OutgoingMetrics { OutgoingMetrics { - out_state_connecting: self.out_state_connecting.clone(), - out_state_waiting: self.out_state_waiting.clone(), - out_state_connected: self.out_state_connected.clone(), - out_state_blocked: self.out_state_blocked.clone(), - out_state_loopback: self.out_state_loopback.clone(), + out_state_connecting: self.out_state_connecting.inner().clone(), + out_state_waiting: self.out_state_waiting.inner().clone(), + out_state_connected: self.out_state_connected.inner().clone(), + out_state_blocked: self.out_state_blocked.inner().clone(), + out_state_loopback: self.out_state_loopback.inner().clone(), } } /// Records that a trie request has been started. + #[allow(dead_code)] // TODO: Readd once metrics are tracked again. pub(super) fn record_trie_request_start(this: &Weak) { if let Some(metrics) = this.upgrade() { metrics.requests_for_trie_accepted.inc(); @@ -577,6 +517,8 @@ impl Metrics { } /// Records that a trie request has ended. + + #[allow(dead_code)] // TODO: Readd once metrics are tracked again. pub(super) fn record_trie_request_end(this: &Weak) { if let Some(metrics) = this.upgrade() { metrics.requests_for_trie_finished.inc(); @@ -585,68 +527,3 @@ impl Metrics { } } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.broadcast_requests); - unregister_metric!(self.registry, self.direct_message_requests); - unregister_metric!(self.registry, self.queued_messages); - unregister_metric!(self.registry, self.peers); - - unregister_metric!(self.registry, self.out_count_protocol); - unregister_metric!(self.registry, self.out_count_consensus); - unregister_metric!(self.registry, self.out_count_deploy_gossip); - unregister_metric!(self.registry, self.out_count_block_gossip); - unregister_metric!(self.registry, self.out_count_finality_signature_gossip); - unregister_metric!(self.registry, self.out_count_address_gossip); - unregister_metric!(self.registry, self.out_count_deploy_transfer); - unregister_metric!(self.registry, self.out_count_block_transfer); - unregister_metric!(self.registry, self.out_count_trie_transfer); - unregister_metric!(self.registry, self.out_count_other); - - unregister_metric!(self.registry, self.out_bytes_protocol); - unregister_metric!(self.registry, self.out_bytes_consensus); - unregister_metric!(self.registry, self.out_bytes_deploy_gossip); - unregister_metric!(self.registry, self.out_bytes_block_gossip); - unregister_metric!(self.registry, self.out_bytes_finality_signature_gossip); - unregister_metric!(self.registry, self.out_bytes_address_gossip); - unregister_metric!(self.registry, self.out_bytes_deploy_transfer); - unregister_metric!(self.registry, self.out_bytes_block_transfer); - unregister_metric!(self.registry, self.out_bytes_trie_transfer); - unregister_metric!(self.registry, self.out_bytes_other); - - unregister_metric!(self.registry, self.out_state_connecting); - unregister_metric!(self.registry, self.out_state_waiting); - unregister_metric!(self.registry, self.out_state_connected); - unregister_metric!(self.registry, self.out_state_blocked); - unregister_metric!(self.registry, self.out_state_loopback); - - unregister_metric!(self.registry, self.in_count_protocol); - unregister_metric!(self.registry, self.in_count_consensus); - unregister_metric!(self.registry, self.in_count_deploy_gossip); - unregister_metric!(self.registry, self.in_count_block_gossip); - unregister_metric!(self.registry, self.in_count_finality_signature_gossip); - unregister_metric!(self.registry, self.in_count_address_gossip); - unregister_metric!(self.registry, self.in_count_deploy_transfer); - unregister_metric!(self.registry, self.in_count_block_transfer); - unregister_metric!(self.registry, self.in_count_trie_transfer); - unregister_metric!(self.registry, self.in_count_other); - - unregister_metric!(self.registry, self.in_bytes_protocol); - unregister_metric!(self.registry, self.in_bytes_consensus); - unregister_metric!(self.registry, self.in_bytes_deploy_gossip); - unregister_metric!(self.registry, self.in_bytes_block_gossip); - unregister_metric!(self.registry, self.in_bytes_finality_signature_gossip); - unregister_metric!(self.registry, self.in_bytes_address_gossip); - unregister_metric!(self.registry, self.in_bytes_deploy_transfer); - unregister_metric!(self.registry, self.in_bytes_block_transfer); - unregister_metric!(self.registry, self.in_bytes_trie_transfer); - unregister_metric!(self.registry, self.in_bytes_other); - - unregister_metric!(self.registry, self.requests_for_trie_accepted); - unregister_metric!(self.registry, self.requests_for_trie_finished); - - unregister_metric!(self.registry, self.accumulated_outgoing_limiter_delay); - unregister_metric!(self.registry, self.accumulated_incoming_limiter_delay); - } -} diff --git a/node/src/components/network/outgoing.rs b/node/src/components/network/outgoing.rs index 72a201763d..2c8be197d2 100644 --- a/node/src/components/network/outgoing.rs +++ b/node/src/components/network/outgoing.rs @@ -104,15 +104,9 @@ use std::{ use datasize::DataSize; use prometheus::IntGauge; -use rand::Rng; -use tracing::{debug, error, error_span, field::Empty, info, trace, warn, Span}; - -use super::{ - blocklist::BlocklistJustification, - display_error, - health::{ConnectionHealth, HealthCheckOutcome, HealthConfig, Nonce, TaggedTimestamp}, - NodeId, -}; +use tracing::{debug, error_span, field::Empty, info, trace, warn, Span}; + +use super::{blocklist::BlocklistJustification, display_error, NodeId}; /// An outgoing connection/address in various states. #[derive(DataSize, Debug)] @@ -160,8 +154,6 @@ where /// /// Can be a channel to decouple sending, or even a direct connection handle. handle: H, - /// Health of the connection. - health: ConnectionHealth, }, /// The address was blocked and will not be retried. Blocked { @@ -207,8 +199,6 @@ pub enum DialOutcome { handle: H, /// The remote peer's authenticated node ID. node_id: NodeId, - /// The moment the connection was established. - when: Instant, }, /// The connection attempt failed. Failed { @@ -256,13 +246,6 @@ pub(crate) enum DialRequest { /// this request can immediately be followed by a connection request, as in the case of a ping /// timeout. Disconnect { handle: H, span: Span }, - - /// Send a ping to a peer. - SendPing { - peer_id: NodeId, - nonce: Nonce, - span: Span, - }, } impl Display for DialRequest @@ -277,9 +260,6 @@ where DialRequest::Disconnect { handle, .. } => { write!(f, "disconnect: {}", handle) } - DialRequest::SendPing { peer_id, nonce, .. } => { - write!(f, "ping[{}]: {}", nonce, peer_id) - } } } } @@ -295,8 +275,6 @@ pub struct OutgoingConfig { pub(crate) unblock_after: Duration, /// Safety timeout, after which a connection is no longer expected to finish dialing. pub(crate) sweep_timeout: Duration, - /// Health check configuration. - pub(crate) health: HealthConfig, } impl OutgoingConfig { @@ -682,41 +660,13 @@ where }) } - /// Records a pong being received. - pub(super) fn record_pong(&mut self, peer_id: NodeId, pong: TaggedTimestamp) -> bool { - let addr = if let Some(addr) = self.routes.get(&peer_id) { - *addr - } else { - debug!(%peer_id, nonce=%pong.nonce(), "ignoring pong received from peer without route"); - return false; - }; - - if let Some(outgoing) = self.outgoing.get_mut(&addr) { - if let OutgoingState::Connected { ref mut health, .. } = outgoing.state { - health.record_pong(&self.config.health, pong) - } else { - debug!(%peer_id, nonce=%pong.nonce(), "ignoring pong received from peer that is not in connected state"); - false - } - } else { - debug!(%peer_id, nonce=%pong.nonce(), "ignoring pong received from peer without route"); - false - } - } - /// Performs housekeeping like reconnection or unblocking peers. /// /// This function must periodically be called. A good interval is every second. - pub(super) fn perform_housekeeping( - &mut self, - rng: &mut R, - now: Instant, - ) -> Vec> { + pub(super) fn perform_housekeeping(&mut self, now: Instant) -> Vec> { let mut to_forget = Vec::new(); let mut to_fail = Vec::new(); - let mut to_ping_timeout = Vec::new(); let mut to_reconnect = Vec::new(); - let mut to_ping = Vec::new(); for (&addr, outgoing) in self.outgoing.iter_mut() { // Note: `Span::in_scope` is no longer serviceable here due to borrow limitations. @@ -776,27 +726,8 @@ where to_fail.push((addr, failures_so_far + 1)); } } - OutgoingState::Connected { - peer_id, - ref mut health, - .. - } => { - // Check if we need to send a ping, or give up and disconnect. - let health_outcome = health.update_health(rng, &self.config.health, now); - - match health_outcome { - HealthCheckOutcome::DoNothing => { - // Nothing to do. - } - HealthCheckOutcome::SendPing(nonce) => { - trace!(%nonce, "sending ping"); - to_ping.push((peer_id, addr, nonce)); - } - HealthCheckOutcome::GiveUp => { - info!("disconnecting after ping retries were exhausted"); - to_ping_timeout.push(addr); - } - } + OutgoingState::Connected { .. } => { + // Nothing to do. } OutgoingState::Loopback => { // Entry is ignored. Not outputting any `trace` because this is log spam even at @@ -828,31 +759,6 @@ where let mut dial_requests = Vec::new(); - // Request disconnection from failed pings. - for addr in to_ping_timeout { - let span = make_span(addr, self.outgoing.get(&addr)); - - let (_, opt_handle) = span.clone().in_scope(|| { - self.change_outgoing_state( - addr, - OutgoingState::Connecting { - failures_so_far: 0, - since: now, - }, - ) - }); - - if let Some(handle) = opt_handle { - dial_requests.push(DialRequest::Disconnect { - handle, - span: span.clone(), - }); - } else { - error!("did not expect connection under ping timeout to not have a residual connection handle. this is a bug"); - } - dial_requests.push(DialRequest::Dial { addr, span }); - } - // Reconnect others. dial_requests.extend(to_reconnect.into_iter().map(|(addr, failures_so_far)| { let span = make_span(addr, self.outgoing.get(&addr)); @@ -870,16 +776,6 @@ where DialRequest::Dial { addr, span } })); - // Finally, schedule pings. - dial_requests.extend(to_ping.into_iter().map(|(peer_id, addr, nonce)| { - let span = make_span(addr, self.outgoing.get(&addr)); - DialRequest::SendPing { - peer_id, - nonce, - span, - } - })); - dial_requests } @@ -898,7 +794,6 @@ where addr, handle, node_id, - when } => { info!("established outgoing connection"); @@ -917,7 +812,6 @@ where OutgoingState::Connected { peer_id: node_id, handle, - health: ConnectionHealth::new(when), }, ); None @@ -1022,17 +916,12 @@ where mod tests { use std::{net::SocketAddr, time::Duration}; - use assert_matches::assert_matches; use datasize::DataSize; - use rand::Rng; use thiserror::Error; use super::{DialOutcome, DialRequest, NodeId, OutgoingConfig, OutgoingManager}; use crate::{ - components::network::{ - blocklist::BlocklistJustification, - health::{HealthConfig, TaggedTimestamp}, - }, + components::network::blocklist::BlocklistJustification, testing::{init_logging, test_clock::TestClock}, }; @@ -1052,7 +941,6 @@ mod tests { base_timeout: Duration::from_secs(1), unblock_after: Duration::from_secs(60), sweep_timeout: Duration::from_secs(45), - health: HealthConfig::test_config(), } } @@ -1123,25 +1011,14 @@ mod tests { assert_eq!(manager.metrics().out_state_waiting.get(), 1); // Performing housekeeping multiple times should not make a difference. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Advancing the clock will trigger a reconnection on the next housekeeping. clock.advance_time(2_000); - assert!(dials( - addr_a, - &manager.perform_housekeeping(&mut rng, clock.now()) - )); + assert!(dials(addr_a, &manager.perform_housekeeping(clock.now()))); assert_eq!(manager.metrics().out_state_connecting.get(), 1); assert_eq!(manager.metrics().out_state_waiting.get(), 0); @@ -1151,7 +1028,6 @@ mod tests { addr: addr_a, handle: 99, node_id: id_a, - when: clock.now(), },) .is_none()); assert_eq!(manager.metrics().out_state_connecting.get(), 0); @@ -1162,9 +1038,7 @@ mod tests { assert_eq!(manager.get_addr(id_a), Some(addr_a)); // Time passes, and our connection drops. Reconnecting should be immediate. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); clock.advance_time(20_000); assert!(dials( addr_a, @@ -1178,16 +1052,13 @@ mod tests { assert!(manager.get_addr(id_a).is_none()); // Reconnection is already in progress, so we do not expect another request on housekeeping. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); } #[test] fn connections_forgotten_after_too_many_tries() { init_logging(); - let mut rng = crate::new_rng(); let mut clock = TestClock::new(); let addr_a: SocketAddr = "1.2.3.4:1234".parse().unwrap(); @@ -1226,21 +1097,17 @@ mod tests { assert!(manager.learn_addr(addr_a, false, clock.now()).is_none()); assert!(manager.learn_addr(addr_b, false, clock.now()).is_none()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); assert!(manager.learn_addr(addr_a, false, clock.now()).is_none()); assert!(manager.learn_addr(addr_b, false, clock.now()).is_none()); // After 1.999 seconds, reconnection should still be delayed. clock.advance_time(1_999); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Adding 0.001 seconds finally is enough to reconnect. clock.advance_time(1); - let requests = manager.perform_housekeeping(&mut rng, clock.now()); + let requests = manager.perform_housekeeping(clock.now()); assert!(dials(addr_a, &requests)); assert!(dials(addr_b, &requests)); @@ -1248,9 +1115,7 @@ mod tests { // anything, as we are currently connecting. clock.advance_time(6_000); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Fail the connection again, wait 3.999 seconds, expecting no reconnection. assert!(manager @@ -1269,13 +1134,11 @@ mod tests { .is_none()); clock.advance_time(3_999); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Adding 0.001 seconds finally again pushes us over the threshold. clock.advance_time(1); - let requests = manager.perform_housekeeping(&mut rng, clock.now()); + let requests = manager.perform_housekeeping(clock.now()); assert!(dials(addr_a, &requests)); assert!(dials(addr_b, &requests)); @@ -1295,18 +1158,14 @@ mod tests { when: clock.now(), },) .is_none()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // The last attempt should happen 8 seconds after the error, not the last attempt. clock.advance_time(7_999); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); clock.advance_time(1); - let requests = manager.perform_housekeeping(&mut rng, clock.now()); + let requests = manager.perform_housekeeping(clock.now()); assert!(dials(addr_a, &requests)); assert!(dials(addr_b, &requests)); @@ -1327,15 +1186,13 @@ mod tests { .is_none()); // Only the unforgettable address should be reconnecting. - let requests = manager.perform_housekeeping(&mut rng, clock.now()); + let requests = manager.perform_housekeeping(clock.now()); assert!(!dials(addr_a, &requests)); assert!(dials(addr_b, &requests)); // But not `addr_a`, even after a long wait. clock.advance_time(1_000_000_000); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); } #[test] @@ -1371,9 +1228,7 @@ mod tests { &manager.learn_addr(addr_b, true, clock.now()) )); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Fifteen seconds later we succeed in connecting to `addr_b`. clock.advance_time(15_000); @@ -1382,15 +1237,12 @@ mod tests { addr: addr_b, handle: 101, node_id: id_b, - when: clock.now(), },) .is_none()); assert_eq!(manager.get_route(id_b), Some(&101)); // Invariant through housekeeping. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); assert_eq!(manager.get_route(id_b), Some(&101)); @@ -1426,13 +1278,10 @@ mod tests { addr: addr_c, handle: 42, node_id: id_c, - when: clock.now(), },) )); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); assert!(manager.get_route(id_c).is_none()); @@ -1440,16 +1289,11 @@ mod tests { // unblocked due to the block timing out. clock.advance_time(30_000); - assert!(dials( - addr_a, - &manager.perform_housekeeping(&mut rng, clock.now()) - )); + assert!(dials(addr_a, &manager.perform_housekeeping(clock.now()))); // Fifteen seconds later, B and C are still blocked, but we redeem B early. clock.advance_time(15_000); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); assert!(dials(addr_b, &manager.redeem_addr(addr_b, clock.now()))); @@ -1459,7 +1303,6 @@ mod tests { addr: addr_b, handle: 77, node_id: id_b, - when: clock.now(), },) .is_none()); assert!(manager @@ -1467,7 +1310,6 @@ mod tests { addr: addr_a, handle: 66, node_id: id_a, - when: clock.now(), },) .is_none()); @@ -1479,7 +1321,6 @@ mod tests { fn loopback_handled_correctly() { init_logging(); - let mut rng = crate::new_rng(); let mut clock = TestClock::new(); let loopback_addr: SocketAddr = "1.2.3.4:1234".parse().unwrap(); @@ -1498,9 +1339,7 @@ mod tests { },) .is_none()); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // Learning loopbacks again should not trigger another connection assert!(manager @@ -1519,9 +1358,7 @@ mod tests { clock.advance_time(1_000_000_000); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); } #[test] @@ -1546,13 +1383,11 @@ mod tests { addr: addr_a, handle: 22, node_id: id_a, - when: clock.now(), }); manager.handle_dial_outcome(DialOutcome::Successful { addr: addr_b, handle: 33, node_id: id_b, - when: clock.now(), }); let mut peer_ids: Vec<_> = manager.connected_peers().collect(); @@ -1586,17 +1421,12 @@ mod tests { // We now let enough time pass to cause the connection to be considered failed aborted. // No effects are expected at this point. clock.advance_time(50_000); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); // The connection will now experience a regular failure. Since this is the first connection // failure, it should reconnect after 2 seconds. clock.advance_time(2_000); - assert!(dials( - addr_a, - &manager.perform_housekeeping(&mut rng, clock.now()) - )); + assert!(dials(addr_a, &manager.perform_housekeeping(clock.now()))); // We now simulate the second connection (`handle: 2`) succeeding first, after 1 second. clock.advance_time(1_000); @@ -1605,7 +1435,6 @@ mod tests { addr: addr_a, handle: 2, node_id: id_a, - when: clock.now(), }) .is_none()); @@ -1619,7 +1448,6 @@ mod tests { addr: addr_a, handle: 1, node_id: id_a, - when: clock.now(), }) .is_none()); @@ -1631,7 +1459,6 @@ mod tests { fn blocking_not_overridden_by_racing_failed_connections() { init_logging(); - let mut rng = crate::new_rng(); let mut clock = TestClock::new(); let addr_a: SocketAddr = "1.2.3.4:1234".parse().unwrap(); @@ -1668,161 +1495,7 @@ mod tests { clock.advance_time(60); assert!(manager.is_blocked(addr_a)); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); + assert!(manager.perform_housekeeping(clock.now()).is_empty()); assert!(manager.is_blocked(addr_a)); } - - #[test] - fn emits_and_accepts_pings() { - init_logging(); - - let mut rng = crate::new_rng(); - let mut clock = TestClock::new(); - - let addr: SocketAddr = "1.2.3.4:1234".parse().unwrap(); - let id = NodeId::random(&mut rng); - - // Setup a connection and put it into the connected state. - let mut manager = OutgoingManager::::new(test_config()); - - // Trigger a new connection via learning an address. - assert!(dials(addr, &manager.learn_addr(addr, false, clock.now()))); - - assert!(manager - .handle_dial_outcome(DialOutcome::Successful { - addr, - handle: 1, - node_id: id, - when: clock.now(), - }) - .is_none()); - - // Initial housekeeping should do nothing. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - - // Go through 50 pings, which should be happening every 5 seconds. - for _ in 0..50 { - clock.advance(Duration::from_secs(3)); - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - clock.advance(Duration::from_secs(2)); - - let (_first_nonce, peer_id) = assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { nonce, peer_id, .. }] => (nonce, peer_id) - ); - assert_eq!(peer_id, id); - - // After a second, nothing should have changed. - assert!(manager - .perform_housekeeping(&mut rng, clock.now()) - .is_empty()); - - clock.advance(Duration::from_secs(1)); - // Waiting another second (two in total) should trigger another ping. - clock.advance(Duration::from_secs(1)); - - let (second_nonce, peer_id) = assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { nonce, peer_id, .. }] => (nonce, peer_id) - ); - - // Ensure the ID is correct. - assert_eq!(peer_id, id); - - // Pong arrives 1 second later. - clock.advance(Duration::from_secs(1)); - - // We now feed back the ping with the correct nonce. This should not result in a ban. - assert!(!manager.record_pong( - peer_id, - TaggedTimestamp::from_parts(clock.now(), second_nonce), - )); - - // This resets the "cycle", the next ping is due in 5 seconds. - } - - // Now we are going to miss 4 pings in a row and expect a disconnect. - clock.advance(Duration::from_secs(5)); - assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { .. }] - ); - clock.advance(Duration::from_secs(2)); - assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { .. }] - ); - clock.advance(Duration::from_secs(2)); - assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { .. }] - ); - clock.advance(Duration::from_secs(2)); - assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::SendPing { .. }] - ); - - // This results in a disconnect, followed by a reconnect. - clock.advance(Duration::from_secs(2)); - let dial_addr = assert_matches!( - manager - .perform_housekeeping(&mut rng, clock.now()) - .as_slice(), - &[DialRequest::Disconnect { .. }, DialRequest::Dial { addr, .. }] => addr - ); - - assert_eq!(dial_addr, addr); - } - - #[test] - fn indicates_issue_when_excessive_pongs_are_encountered() { - let mut rng = crate::new_rng(); - let mut clock = TestClock::new(); - - let addr: SocketAddr = "1.2.3.4:1234".parse().unwrap(); - let id = NodeId::random(&mut rng); - - // Ensure we have one connected node. - let mut manager = OutgoingManager::::new(test_config()); - - assert!(dials(addr, &manager.learn_addr(addr, false, clock.now()))); - assert!(manager - .handle_dial_outcome(DialOutcome::Successful { - addr, - handle: 1, - node_id: id, - when: clock.now(), - }) - .is_none()); - - clock.advance(Duration::from_millis(50)); - - // We can now receive excessive pongs. - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(!manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - assert!(manager.record_pong(id, TaggedTimestamp::from_parts(clock.now(), rng.gen()))); - } } diff --git a/node/src/components/network/symmetry.rs b/node/src/components/network/symmetry.rs index 9477bca6e7..37433fd24a 100644 --- a/node/src/components/network/symmetry.rs +++ b/node/src/components/network/symmetry.rs @@ -9,7 +9,7 @@ use datasize::DataSize; use tracing::{debug, warn}; /// Describes whether a connection is uni- or bi-directional. -#[derive(DataSize, Debug)] +#[derive(DataSize, Debug, Default)] pub(super) enum ConnectionSymmetry { /// We have only seen an incoming connection. IncomingOnly { @@ -29,15 +29,10 @@ pub(super) enum ConnectionSymmetry { peer_addrs: BTreeSet, }, /// The connection is invalid/missing and should be removed. + #[default] Gone, } -impl Default for ConnectionSymmetry { - fn default() -> Self { - ConnectionSymmetry::Gone - } -} - impl ConnectionSymmetry { /// A new incoming connection has been registered. /// diff --git a/node/src/components/network/tasks.rs b/node/src/components/network/tasks.rs index 671c2a11f5..4cf53c18e6 100644 --- a/node/src/components/network/tasks.rs +++ b/node/src/components/network/tasks.rs @@ -1,88 +1,58 @@ //! Tasks run by the component. use std::{ - error::Error as StdError, fmt::Display, - io, net::SocketAddr, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, Arc, Weak, }, - time::Duration, }; -use bincode::Options; use futures::{ future::{self, Either}, - stream::{SplitSink, SplitStream}, - Future, SinkExt, StreamExt, + pin_mut, }; + use openssl::{ pkey::{PKey, Private}, ssl::Ssl, x509::X509, }; -use prometheus::IntGauge; -use rand::Rng; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use tokio::{ - net::TcpStream, - sync::{mpsc::UnboundedReceiver, watch, Semaphore}, -}; +use serde::de::DeserializeOwned; +use tokio::net::TcpStream; use tokio_openssl::SslStream; -use tokio_serde::{Deserializer, Serializer}; use tracing::{ - debug, error, error_span, + debug, error_span, field::{self, Empty}, info, trace, warn, Instrument, Span, }; -use casper_types::{ProtocolVersion, PublicKey, TimeDiff}; +use casper_types::{ProtocolVersion, TimeDiff}; use super::{ chain_info::ChainInfo, - counting_format::{ConnectionId, Role}, - error::{ConnectionError, IoError}, + connection_id::ConnectionId, + error::{ConnectionError, MessageReceiverError, MessageSenderError}, event::{IncomingConnection, OutgoingConnection}, - full_transport, - limiter::LimiterHandle, message::NodeKeyPair, - message_pack_format::MessagePackFormat, - EstimatorWeights, Event, FramedTransport, FullTransport, Identity, Message, Metrics, Payload, - Transport, + Channel, Event, FromIncoming, Identity, Message, Metrics, Payload, RpcServer, Transport, }; + use crate::{ - components::network::{framed_transport, BincodeFormat, Config, FromIncoming}, - effect::{ - announcements::PeerBehaviorAnnouncement, requests::NetworkRequest, AutoClosingResponder, - EffectBuilder, + components::network::{ + deserialize_network_message, + handshake::{negotiate_handshake, HandshakeOutcome}, + Config, Ticket, }, + effect::{announcements::PeerBehaviorAnnouncement, requests::NetworkRequest}, reactor::{EventQueueHandle, QueueKind}, tls::{self, TlsCert, ValidationError}, types::NodeId, - utils::display_error, + utils::{display_error, LockedLineWriter, ObservableFuse, Peel}, }; -/// An item on the internal outgoing message queue. -/// -/// Contains a reference counted message and an optional responder to call once the message has been -/// successfully handed over to the kernel for sending. -pub(super) type MessageQueueItem

= (Arc>, Option>); - -/// The outcome of the handshake process. -struct HandshakeOutcome { - /// A framed transport for peer. - framed_transport: FramedTransport, - /// Public address advertised by the peer. - public_addr: SocketAddr, - /// The public key the peer is validating with, if any. - peer_consensus_public_key: Option, - /// Holds the information whether the remote node is syncing. - is_peer_syncing: bool, -} - /// Low-level TLS connection function. /// /// Performs the actual TCP+TLS connection setup. @@ -101,14 +71,18 @@ where .set_nodelay(true) .map_err(ConnectionError::TcpNoDelay)?; - let mut transport = tls::create_tls_connector(context.our_cert.as_x509(), &context.secret_key) - .and_then(|connector| connector.configure()) - .and_then(|mut config| { - config.set_verify_hostname(false); - config.into_ssl("this-will-not-be-checked.example.com") - }) - .and_then(|ssl| SslStream::new(ssl, stream)) - .map_err(ConnectionError::TlsInitialization)?; + let mut transport = tls::create_tls_connector( + context.our_cert.as_x509(), + &context.secret_key, + context.keylog.clone(), + ) + .and_then(|connector| connector.configure()) + .and_then(|mut config| { + config.set_verify_hostname(false); + config.into_ssl("this-will-not-be-checked.example.com") + }) + .and_then(|ssl| SslStream::new(ssl, stream)) + .map_err(ConnectionError::TlsInitialization)?; SslStream::connect(Pin::new(&mut transport)) .await @@ -132,7 +106,7 @@ where pub(super) async fn connect_outgoing( context: Arc>, peer_addr: SocketAddr, -) -> OutgoingConnection

+) -> OutgoingConnection where REv: 'static, P: Payload, @@ -154,15 +128,13 @@ where // Setup connection id and framed transport. let connection_id = ConnectionId::from_connection(transport.ssl(), context.our_id, peer_id); - let framed_transport = framed_transport(transport, context.chain_info.maximum_net_message_size); // Negotiate the handshake, concluding the incoming connection process. - match negotiate_handshake::(&context, framed_transport, connection_id).await { + match negotiate_handshake::(&context, transport, connection_id).await { Ok(HandshakeOutcome { - framed_transport, + transport, public_addr, peer_consensus_public_key, - is_peer_syncing: is_syncing, }) => { if let Some(ref public_key) = peer_consensus_public_key { Span::current().record("consensus_key", &field::display(public_key)); @@ -173,21 +145,11 @@ where warn!(%public_addr, %peer_addr, "peer advertises a different public address than what we connected to"); } - // Setup full framed transport, then close down receiving end of the transport. - let full_transport = full_transport::

( - context.net_metrics.clone(), - connection_id, - framed_transport, - Role::Dialer, - ); - let (sink, _stream) = full_transport.split(); - OutgoingConnection::Established { peer_addr, peer_id, peer_consensus_public_key, - sink, - is_syncing, + transport, } } Err(error) => OutgoingConnection::Failed { @@ -213,8 +175,11 @@ where /// TLS certificate authority associated with this node's identity. network_ca: Option>, /// Secret key associated with `our_cert`. - secret_key: Arc>, + pub(super) secret_key: Arc>, + /// Logfile to log TLS keys to. If given, automatically enables logging. + pub(super) keylog: Option, /// Weak reference to the networking metrics shared by all sender/receiver tasks. + #[allow(dead_code)] // TODO: Readd once metrics are tracked again. net_metrics: Weak, /// Chain info extract from chainspec. chain_info: ChainInfo, @@ -223,9 +188,7 @@ where /// Our own public listening address. public_addr: Option, /// Timeout for handshake completion. - handshake_timeout: TimeDiff, - /// Weights to estimate payloads with. - payload_weights: EstimatorWeights, + pub(super) handshake_timeout: TimeDiff, /// The protocol version at which (or under) tarpitting is enabled. tarpit_version_threshold: Option, /// If tarpitting is enabled, duration for which connections should be kept open. @@ -233,15 +196,15 @@ where /// The chance, expressed as a number between 0.0 and 1.0, of triggering the tarpit. tarpit_chance: f32, /// Maximum number of demands allowed to be running at once. If 0, no limit is enforced. + #[allow(dead_code)] // TODO: Readd if necessary for backpressure. max_in_flight_demands: usize, - /// Flag indicating whether this node is syncing. - is_syncing: AtomicBool, } impl NetworkContext { pub(super) fn new( cfg: Config, our_identity: Identity, + keylog: Option, node_key_pair: Option, chain_info: ChainInfo, net_metrics: &Arc, @@ -271,12 +234,11 @@ impl NetworkContext { chain_info, node_key_pair, handshake_timeout: cfg.handshake_timeout, - payload_weights: cfg.estimator_weights.clone(), tarpit_version_threshold: cfg.tarpit_version_threshold, tarpit_duration: cfg.tarpit_duration, tarpit_chance: cfg.tarpit_chance, max_in_flight_demands, - is_syncing: AtomicBool::new(false), + keylog, } } @@ -315,8 +277,20 @@ impl NetworkContext { self.network_ca.as_ref() } - pub(crate) fn is_syncing(&self) -> &AtomicBool { - &self.is_syncing + pub(crate) fn node_key_pair(&self) -> Option<&NodeKeyPair> { + self.node_key_pair.as_ref() + } + + pub(crate) fn tarpit_chance(&self) -> f32 { + self.tarpit_chance + } + + pub(crate) fn tarpit_duration(&self) -> TimeDiff { + self.tarpit_duration + } + + pub(crate) fn tarpit_version_threshold(&self) -> Option { + self.tarpit_version_threshold } } @@ -327,12 +301,10 @@ async fn handle_incoming( context: Arc>, stream: TcpStream, peer_addr: SocketAddr, -) -> IncomingConnection

+) -> IncomingConnection where REv: From> + 'static, P: Payload, - for<'de> P: Serialize + Deserialize<'de>, - for<'de> Message

: Serialize + Deserialize<'de>, { let (peer_id, transport) = match server_setup_tls(&context, stream).await { Ok(value) => value, @@ -353,36 +325,24 @@ where // Setup connection id and framed transport. let connection_id = ConnectionId::from_connection(transport.ssl(), context.our_id, peer_id); - let framed_transport = framed_transport(transport, context.chain_info.maximum_net_message_size); // Negotiate the handshake, concluding the incoming connection process. - match negotiate_handshake::(&context, framed_transport, connection_id).await { + match negotiate_handshake::(&context, transport, connection_id).await { Ok(HandshakeOutcome { - framed_transport, + transport, public_addr, peer_consensus_public_key, - is_peer_syncing: _, }) => { if let Some(ref public_key) = peer_consensus_public_key { Span::current().record("consensus_key", &field::display(public_key)); } - // Establish full transport and close the receiving end. - let full_transport = full_transport::

( - context.net_metrics.clone(), - connection_id, - framed_transport, - Role::Listener, - ); - - let (_sink, stream) = full_transport.split(); - IncomingConnection::Established { peer_addr, public_addr, peer_id, peer_consensus_public_key, - stream, + transport, } } Err(error) => IncomingConnection::Failed { @@ -403,6 +363,7 @@ pub(super) async fn server_setup_tls( let mut tls_stream = tls::create_tls_acceptor( context.our_cert.as_x509().as_ref(), context.secret_key.as_ref(), + context.keylog.clone(), ) .and_then(|ssl_acceptor| Ssl::new(ssl_acceptor.context())) .and_then(|ssl| SslStream::new(ssl, stream)) @@ -428,161 +389,11 @@ pub(super) async fn server_setup_tls( )) } -/// Performs an IO-operation that can time out. -async fn io_timeout(duration: Duration, future: F) -> Result> -where - F: Future>, - E: StdError + 'static, -{ - tokio::time::timeout(duration, future) - .await - .map_err(|_elapsed| IoError::Timeout)? - .map_err(IoError::Error) -} - -/// Performs an IO-operation that can time out or result in a closed connection. -async fn io_opt_timeout(duration: Duration, future: F) -> Result> -where - F: Future>>, - E: StdError + 'static, -{ - let item = tokio::time::timeout(duration, future) - .await - .map_err(|_elapsed| IoError::Timeout)?; - - match item { - Some(Ok(value)) => Ok(value), - Some(Err(err)) => Err(IoError::Error(err)), - None => Err(IoError::UnexpectedEof), - } -} - -/// Negotiates a handshake between two peers. -async fn negotiate_handshake( - context: &NetworkContext, - framed: FramedTransport, - connection_id: ConnectionId, -) -> Result -where - P: Payload, -{ - let mut encoder = MessagePackFormat; - - // Manually encode a handshake. - let handshake_message = context.chain_info.create_handshake::

( - context.public_addr.expect("component not initialized"), - context.node_key_pair.as_ref(), - connection_id, - context.is_syncing.load(Ordering::SeqCst), - ); - - let serialized_handshake_message = Pin::new(&mut encoder) - .serialize(&Arc::new(handshake_message)) - .map_err(ConnectionError::CouldNotEncodeOurHandshake)?; - - // To ensure we are not dead-locking, we split the framed transport here and send the handshake - // in a background task before awaiting one ourselves. This ensures we can make progress - // regardless of the size of the outgoing handshake. - let (mut sink, mut stream) = framed.split(); - - let handshake_send = tokio::spawn(io_timeout(context.handshake_timeout.into(), async move { - sink.send(serialized_handshake_message).await?; - Ok(sink) - })); - - // The remote's message should be a handshake, but can technically be any message. We receive, - // deserialize and check it. - let remote_message_raw = io_opt_timeout(context.handshake_timeout.into(), stream.next()) - .await - .map_err(ConnectionError::HandshakeRecv)?; - - // Ensure the handshake was sent correctly. - let sink = handshake_send - .await - .map_err(ConnectionError::HandshakeSenderCrashed)? - .map_err(ConnectionError::HandshakeSend)?; - - let remote_message: Message

= Pin::new(&mut encoder) - .deserialize(&remote_message_raw) - .map_err(ConnectionError::InvalidRemoteHandshakeMessage)?; - - if let Message::Handshake { - network_name, - public_addr, - protocol_version, - consensus_certificate, - is_syncing, - chainspec_hash, - } = remote_message - { - debug!(%protocol_version, "handshake received"); - - // The handshake was valid, we can check the network name. - if network_name != context.chain_info.network_name { - return Err(ConnectionError::WrongNetwork(network_name)); - } - - // If there is a version mismatch, we treat it as a connection error. We do not ban peers - // for this error, but instead rely on exponential backoff, as bans would result in issues - // during upgrades where nodes may have a legitimate reason for differing versions. - // - // Since we are not using SemVer for versioning, we cannot make any assumptions about - // compatibility, so we allow only exact version matches. - if protocol_version != context.chain_info.protocol_version { - if let Some(threshold) = context.tarpit_version_threshold { - if protocol_version <= threshold { - let mut rng = crate::new_rng(); - - if rng.gen_bool(context.tarpit_chance as f64) { - // If tarpitting is enabled, we hold open the connection for a specific - // amount of time, to reduce load on other nodes and keep them from - // reconnecting. - info!(duration=?context.tarpit_duration, "randomly tarpitting node"); - tokio::time::sleep(Duration::from(context.tarpit_duration)).await; - } else { - debug!(p = context.tarpit_chance, "randomly not tarpitting node"); - } - } - } - return Err(ConnectionError::IncompatibleVersion(protocol_version)); - } - - // We check the chainspec hash to ensure peer is using the same chainspec as us. - // The remote message should always have a chainspec hash at this point since - // we checked the protocol version previously. - let peer_chainspec_hash = chainspec_hash.ok_or(ConnectionError::MissingChainspecHash)?; - if peer_chainspec_hash != context.chain_info.chainspec_hash { - return Err(ConnectionError::WrongChainspecHash(peer_chainspec_hash)); - } - - let peer_consensus_public_key = consensus_certificate - .map(|cert| { - cert.validate(connection_id) - .map_err(ConnectionError::InvalidConsensusCertificate) - }) - .transpose()?; - - let framed_transport = sink - .reunite(stream) - .map_err(|_| ConnectionError::FailedToReuniteHandshakeSinkAndStream)?; - - Ok(HandshakeOutcome { - framed_transport, - public_addr, - peer_consensus_public_key, - is_peer_syncing: is_syncing, - }) - } else { - // Received a non-handshake, this is an error. - Err(ConnectionError::DidNotSendHandshake) - } -} - /// Runs the server core acceptor loop. pub(super) async fn server( context: Arc>, listener: tokio::net::TcpListener, - mut shutdown_receiver: watch::Receiver<()>, + shutdown_receiver: ObservableFuse, ) where REv: From> + Send, P: Payload, @@ -616,7 +427,7 @@ pub(super) async fn server( incoming: Box::new(incoming), span, }, - QueueKind::NetworkIncoming, + QueueKind::MessageIncoming, ) .await; } @@ -638,11 +449,13 @@ pub(super) async fn server( } }; - let shutdown_messages = async move { while shutdown_receiver.changed().await.is_ok() {} }; + let shutdown_messages = shutdown_receiver.wait(); + pin_mut!(shutdown_messages); + pin_mut!(accept_connections); // Now we can wait for either the `shutdown` channel's remote end to do be dropped or the // infinite loop to terminate, which never happens. - match future::select(Box::pin(shutdown_messages), Box::pin(accept_connections)).await { + match future::select(shutdown_messages, accept_connections).await { Either::Left(_) => info!( %context.our_id, "shutting down socket, no longer accepting incoming connections" @@ -651,17 +464,15 @@ pub(super) async fn server( } } -/// Network message reader. -/// -/// Schedules all received messages until the stream is closed or an error occurs. -pub(super) async fn message_reader( +/// Juliet-based message receiver. +pub(super) async fn message_receiver( context: Arc>, - mut stream: SplitStream>, - limiter: LimiterHandle, - mut close_incoming_receiver: watch::Receiver<()>, + validator_status: Arc, + mut rpc_server: RpcServer, + shutdown: ObservableFuse, peer_id: NodeId, span: Span, -) -> io::Result<()> +) -> Result<(), MessageReceiverError> where P: DeserializeOwned + Send + Display + Payload, REv: From> @@ -670,176 +481,93 @@ where + From + Send, { - let demands_in_flight = Arc::new(Semaphore::new(context.max_in_flight_demands)); - let event_queue = context.event_queue.expect("component not initialized"); - - let read_messages = async move { - while let Some(msg_result) = stream.next().await { - match msg_result { - Ok(msg) => { - trace!(%msg, "message received"); - - let effect_builder = EffectBuilder::new(event_queue); - - match msg.try_into_demand(effect_builder, peer_id) { - Ok((event, wait_for_response)) => { - // Note: For now, demands bypass the limiter, as we expect the - // backpressure to handle this instead. - - // Acquire a permit. If we are handling too many demands at this - // time, this will block, halting the processing of new message, - // thus letting the peer they have reached their maximum allowance. - let in_flight = demands_in_flight - .clone() - .acquire_owned() - .await - // Note: Since the semaphore is reference counted, it must - // explicitly be closed for acquisition to fail, which we - // never do. If this happens, there is a bug in the code; - // we exit with an error and close the connection. - .map_err(|_| { - io::Error::new( - io::ErrorKind::Other, - "demand limiter semaphore closed unexpectedly", - ) - })?; - - Metrics::record_trie_request_start(&context.net_metrics); - - let net_metrics = context.net_metrics.clone(); - // Spawn a future that will eventually send the returned message. It - // will essentially buffer the response. - tokio::spawn(async move { - if let Some(payload) = wait_for_response.await { - // Send message and await its return. `send_message` should - // only return when the message has been buffered, if the - // peer is not accepting data, we will block here until the - // send buffer has sufficient room. - effect_builder.send_message(peer_id, payload).await; - - // Note: We could short-circuit the event queue here and - // directly insert into the outgoing message queue, - // which may be potential performance improvement. - } - - // Missing else: The handler of the demand did not deem it - // worthy a response. Just drop it. - - // After we have either successfully buffered the message for - // sending, failed to do so or did not have a message to send - // out, we consider the request handled and free up the permit. - Metrics::record_trie_request_end(&net_metrics); - drop(in_flight); - }); - - // Schedule the created event. - event_queue - .schedule::(event, QueueKind::NetworkDemand) - .await; - } - Err(msg) => { - // We've received a non-demand message. Ensure we have the proper amount - // of resources, then push it to the reactor. - limiter - .request_allowance( - msg.payload_incoming_resource_estimate( - &context.payload_weights, - ), - ) - .await; - - let queue_kind = if msg.is_low_priority() { - QueueKind::NetworkLowPriority - } else { - QueueKind::NetworkIncoming - }; - - event_queue - .schedule( - Event::IncomingMessage { - peer_id: Box::new(peer_id), - msg, - span: span.clone(), - }, - queue_kind, - ) - .await; - } + loop { + let next_item = rpc_server.next_request(); + + // TODO: Get rid of shutdown fuse, we can drop the client instead? + let wait_for_close_incoming = shutdown.wait(); + + pin_mut!(next_item); + pin_mut!(wait_for_close_incoming); + + let request = match future::select(next_item, wait_for_close_incoming) + .await + .peel() + { + Either::Left(outcome) => { + if let Some(request) = outcome? { + request + } else { + { + // Remote closed the connection. + return Ok(()); } } - Err(err) => { - warn!( - err = display_error(&err), - "receiving message failed, closing connection" - ); - return Err(err); - } } + Either::Right(()) => { + // We were asked to shut down. + return Ok(()); + } + }; + + let channel = Channel::from_repr(request.channel().get()) + .ok_or_else(|| MessageReceiverError::InvalidChannel(request.channel().get()))?; + let payload = request + .payload() + .as_ref() + .ok_or_else(|| MessageReceiverError::EmptyRequest)?; + + let msg: Message

= deserialize_network_message(payload) + .map_err(MessageReceiverError::DeserializationError)?; + + trace!(%msg, %channel, "message received"); + + // Ensure the peer did not try to sneak in a message on a different channel. + // TODO: Verify we still need this. + let msg_channel = msg.get_channel(); + if msg_channel != channel { + return Err(MessageReceiverError::WrongChannel { + got: msg_channel, + expected: channel, + }); } - Ok(()) - }; - let shutdown_messages = async move { while close_incoming_receiver.changed().await.is_ok() {} }; + let queue_kind = if validator_status.load(Ordering::Relaxed) { + QueueKind::MessageValidator + } else if msg.is_low_priority() { + QueueKind::MessageLowPriority + } else { + QueueKind::MessageIncoming + }; - // Now we can wait for either the `shutdown` channel's remote end to do be dropped or the - // while loop to terminate. - match future::select(Box::pin(shutdown_messages), Box::pin(read_messages)).await { - Either::Left(_) => info!("shutting down incoming connection message reader"), - Either::Right(_) => (), + context + .event_queue + .expect("TODO: What to do if event queue is missing here?") + .schedule( + Event::IncomingMessage { + peer_id: Box::new(peer_id), + msg: Box::new(msg), + span: span.clone(), + ticket: Ticket::from_rpc_request(request), + }, + queue_kind, + ) + .await; } - - Ok(()) } -/// Network message sender. +/// RPC sender task. /// -/// Reads from a channel and sends all messages, until the stream is closed or an error occurs. -pub(super) async fn message_sender

( - mut queue: UnboundedReceiver>, - mut sink: SplitSink, Arc>>, - limiter: LimiterHandle, - counter: IntGauge, -) where - P: Payload, -{ - while let Some((message, opt_responder)) = queue.recv().await { - counter.dec(); - - let estimated_wire_size = match BincodeFormat::default().0.serialized_size(&*message) { - Ok(size) => size as u32, - Err(error) => { - error!( - error = display_error(&error), - "failed to get serialized size of outgoing message, closing outgoing connection" - ); - break; - } - }; - limiter.request_allowance(estimated_wire_size).await; - - let mut outcome = sink.send(message).await; - - // Notify via responder that the message has been buffered by the kernel. - if let Some(auto_closing_responder) = opt_responder { - // Since someone is interested in the message, flush the socket to ensure it was sent. - outcome = outcome.and(sink.flush().await); - auto_closing_responder.respond(()).await; +/// While the sending connection does not receive any messages, it is still necessary to run the +/// server portion in a loop to ensure outgoing messages are actually processed. +pub(super) async fn rpc_sender_loop(mut rpc_server: RpcServer) -> Result<(), MessageSenderError> { + loop { + if let Some(incoming_request) = rpc_server.next_request().await? { + return Err(MessageSenderError::UnexpectedIncomingRequest( + incoming_request, + )); + } else { + // Connection closed regularly. } - - // We simply error-out if the sink fails, it means that our connection broke. - if let Err(ref err) = outcome { - info!( - err = display_error(err), - "message send failed, closing outgoing connection" - ); - - // To ensure, metrics are up to date, we close the queue and drain it. - queue.close(); - while queue.recv().await.is_some() { - counter.dec(); - } - - break; - }; } } diff --git a/node/src/components/network/tests.rs b/node/src/components/network/tests.rs index a30a432587..435bc4822f 100644 --- a/node/src/components/network/tests.rs +++ b/node/src/components/network/tests.rs @@ -22,7 +22,7 @@ use casper_types::SecretKey; use super::{ chain_info::ChainInfo, Config, Event as NetworkEvent, FromIncoming, GossipedAddress, Identity, - MessageKind, Network, Payload, + MessageKind, Network, Payload, Ticket, }; use crate::{ components::{ @@ -123,11 +123,12 @@ impl From for Event { } impl FromIncoming for Event { - fn from_incoming(sender: NodeId, payload: Message) -> Self { + fn from_incoming(sender: NodeId, payload: Message, ticket: Ticket) -> Self { match payload { Message::AddressGossiper(message) => Event::AddressGossiperIncoming(GossiperIncoming { sender, message: Box::new(message), + ticket: Arc::new(ticket), }), } } @@ -159,12 +160,8 @@ impl Payload for Message { } } - fn incoming_resource_estimate(&self, _weights: &super::EstimatorWeights) -> u32 { - 0 - } - - fn is_unsafe_for_syncing_peers(&self) -> bool { - false + fn get_channel(&self) -> super::Channel { + super::Channel::Network } } @@ -294,7 +291,7 @@ impl Finalize for TestReactor { /// Checks whether or not a given network with potentially blocked nodes is completely connected. fn network_is_complete( blocklist: &HashSet, - nodes: &HashMap>>, + nodes: &HashMap>>>, ) -> bool { // Collect expected nodes. let expected: HashSet<_> = nodes @@ -446,6 +443,10 @@ async fn check_varying_size_network_connects() { // Try with a few predefined sets of network sizes. for &number_of_nodes in &[2u16, 3, 5, 9, 15] { + info!( + number_of_nodes, + "begin varying size network connection test" + ); let timeout = Duration::from_secs(3 * number_of_nodes as u64); let mut net = TestingNetwork::new(); @@ -487,6 +488,11 @@ async fn check_varying_size_network_connects() { // This test will run multiple times, so ensure we cleanup all ports. net.finalize().await; + + info!( + number_of_nodes, + "finished varying size network connection test" + ); } } diff --git a/node/src/components/network/transport.rs b/node/src/components/network/transport.rs new file mode 100644 index 0000000000..9fbcd9c145 --- /dev/null +++ b/node/src/components/network/transport.rs @@ -0,0 +1,77 @@ +//! Low-level network transport configuration. +//! +//! The low-level transport is built on top of an existing TLS stream, handling all multiplexing. It +//! is based on a configuration of the Juliet protocol implemented in the `juliet` crate. + +use juliet::{rpc::IncomingRequest, ChannelConfiguration}; +use strum::EnumCount; + +use super::Channel; + +/// Creats a new RPC builder with the currently fixed Juliet configuration. +/// +/// The resulting `RpcBuilder` can be reused for multiple connections. +pub(super) fn create_rpc_builder( + maximum_message_size: u32, + max_in_flight_demands: u16, +) -> juliet::rpc::RpcBuilder<{ Channel::COUNT }> { + // Note: `maximum_message_size` is a bit misleading, since it is actually the maximum payload + // size. In the future, the chainspec setting should be overhauled and the + // one-size-fits-all limit replaced with a per-channel limit. Similarly, + // `max_in_flight_demands` should be tweaked on a per-channel basis. + + // Since we do not currently configure individual message size limits and make no distinction + // between requests and responses, we simply set all limits to the maximum message size. + let channel_cfg = ChannelConfiguration::new() + .with_request_limit(max_in_flight_demands) + .with_max_request_payload_size(maximum_message_size) + .with_max_response_payload_size(maximum_message_size); + + let protocol = juliet::protocol::ProtocolBuilder::with_default_channel_config(channel_cfg); + + // TODO: Figure out a good value for buffer sizes. + let io_core = juliet::io::IoCoreBuilder::with_default_buffer_size( + protocol, + max_in_flight_demands.min(20) as usize, + ); + + juliet::rpc::RpcBuilder::new(io_core) +} + +/// Adapter for incoming Juliet requests. +/// +/// At this time the node does not take full advantage of the Juliet RPC capabilities, relying on +/// its older message+ACK based model introduced with `muxink`. In this model, every message is only +/// acknowledged, with no request-response association being done. The ACK indicates that the peer +/// is free to send another message. +/// +/// The [`Ticket`] type is used to track the processing of an incoming message or its resulting +/// operations; it should dropped once the resources for doing so have been spent, but no earlier. +/// +/// Dropping it will cause an "ACK", which in the Juliet transport's case is an empty response, to +/// be sent. Cancellations or responses with actual payloads are not used at this time. +#[derive(Debug)] +pub(crate) struct Ticket(Option>); + +impl Ticket { + #[inline(always)] + pub(super) fn from_rpc_request(incoming_request: IncomingRequest) -> Self { + Ticket(Some(Box::new(incoming_request))) + } + + #[cfg(test)] + #[inline(always)] + pub(crate) fn create_dummy() -> Self { + Ticket(None) + } +} + +impl Drop for Ticket { + #[inline(always)] + fn drop(&mut self) { + // Currently, we simply send a request confirmation in the for of an `ACK`. + if let Some(incoming_request) = self.0.take() { + incoming_request.respond(None); + } + } +} diff --git a/node/src/components/rest_server.rs b/node/src/components/rest_server.rs index 37ea51f426..36d40271a0 100644 --- a/node/src/components/rest_server.rs +++ b/node/src/components/rest_server.rs @@ -27,7 +27,8 @@ use std::{fmt::Debug, time::Instant}; use datasize::DataSize; use futures::{future::BoxFuture, join, FutureExt}; -use tokio::{sync::oneshot, task::JoinHandle}; +use std::net::SocketAddr; +use tokio::task::JoinHandle; use tracing::{debug, error, info, warn}; use casper_json_rpc::CorsOrigin; @@ -49,7 +50,7 @@ use crate::{ }, reactor::{main_reactor::MainEvent, Finalize}, types::{ChainspecInfo, StatusFeed}, - utils::{self, ListeningError}, + utils::{self, DropSwitch, Fuse, ListeningError, ObservableFuse}, NodeRng, }; pub use config::Config; @@ -93,7 +94,9 @@ impl ReactorEventT for REv where pub(crate) struct InnerRestServer { /// When the message is sent, it signals the server loop to exit cleanly. #[data_size(skip)] - shutdown_sender: oneshot::Sender<()>, + shutdown_fuse: DropSwitch, + /// The address the server is listening on. + local_addr: Option, /// The task handle which will only join once the server loop has exited. #[data_size(skip)] server_join_handle: Option>, @@ -131,6 +134,23 @@ impl RestServer { inner_rest: None, } } + + /// Returns the binding address. + /// + /// Only used in testing. If you need to actually retrieve the bind address, add an appropriate + /// request or, as a last resort, make this function return `Option`. + /// + /// # Panics + /// + /// If the bind address is malformed, panics. + #[cfg(test)] + pub(crate) fn bind_address(&self) -> SocketAddr { + self.inner_rest + .as_ref() + .expect("no inner rest server") + .local_addr + .expect("missing bind addr") + } } impl Component for RestServer @@ -169,6 +189,22 @@ where >::set_state(self, state); effects } + Event::BindComplete(local_addr) => { + match self.inner_rest { + Some(ref mut inner_rest) => { + inner_rest.local_addr = Some(local_addr); + info!(%local_addr, "REST server finishing binding"); + >::set_state( + self, + ComponentState::Initialized, + ); + } + None => { + error!("should not have received `BindComplete` event when REST server is disabled") + } + } + Effects::new() + } Event::RestRequest(_) | Event::GetMetricsResult { .. } => { warn!( ?event, @@ -187,6 +223,10 @@ where ); Effects::new() } + Event::BindComplete(_) => { + error!("REST component received BindComplete while initialized"); + Effects::new() + } Event::RestRequest(RestRequest::Status { responder }) => { let node_uptime = self.node_startup_instant.elapsed(); let network_name = self.network_name.clone(); @@ -286,40 +326,24 @@ where effect_builder: EffectBuilder, ) -> Result, Self::Error> { let cfg = &self.config; - let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); + let shutdown_fuse = ObservableFuse::new(); let builder = utils::start_listening(&cfg.address)?; - let server_join_handle = match cfg.cors_origin.as_str() { - "" => Some(tokio::spawn(http_server::run( - builder, - effect_builder, - self.api_version, - shutdown_receiver, - cfg.qps_limit, - ))), - "*" => Some(tokio::spawn(http_server::run_with_cors( - builder, - effect_builder, - self.api_version, - shutdown_receiver, - cfg.qps_limit, - CorsOrigin::Any, - ))), - _ => Some(tokio::spawn(http_server::run_with_cors( - builder, - effect_builder, - self.api_version, - shutdown_receiver, - cfg.qps_limit, - CorsOrigin::Specified(cfg.cors_origin.clone()), - ))), - }; + let server_join_handle = Some(tokio::spawn(http_server::run( + builder, + effect_builder, + self.api_version, + shutdown_fuse.clone(), + cfg.qps_limit, + CorsOrigin::parse_str(&cfg.cors_origin), + ))); let node_startup_instant = self.node_startup_instant; let network_name = self.network_name.clone(); self.inner_rest = Some(InnerRestServer { - shutdown_sender, + shutdown_fuse: DropSwitch::new(shutdown_fuse), + local_addr: None, server_join_handle, node_startup_instant, network_name, @@ -333,7 +357,7 @@ impl Finalize for RestServer { fn finalize(self) -> BoxFuture<'static, ()> { async { if let Some(mut rest_server) = self.inner_rest { - let _ = rest_server.shutdown_sender.send(()); + rest_server.shutdown_fuse.inner().set(); // Wait for the server to exit cleanly. if let Some(join_handle) = rest_server.server_join_handle.take() { diff --git a/node/src/components/rest_server/event.rs b/node/src/components/rest_server/event.rs index cfc9937848..f37595a304 100644 --- a/node/src/components/rest_server/event.rs +++ b/node/src/components/rest_server/event.rs @@ -1,6 +1,7 @@ use std::{ fmt::{self, Display, Formatter}, mem, + net::SocketAddr, }; use derive_more::From; @@ -14,6 +15,8 @@ const_assert!(_REST_EVENT_SIZE < 89); #[derive(Debug, From)] pub(crate) enum Event { Initialize, + /// The background task running the HTTP server has finished binding its port. + BindComplete(SocketAddr), #[from] RestRequest(RestRequest), GetMetricsResult { @@ -26,6 +29,7 @@ impl Display for Event { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { Event::Initialize => write!(formatter, "initialize"), + Event::BindComplete(local_addr) => write!(formatter, "bind complete: {}", local_addr), Event::RestRequest(request) => write!(formatter, "{}", request), Event::GetMetricsResult { text, .. } => match text { Some(txt) => write!(formatter, "get metrics ({} bytes)", txt.len()), diff --git a/node/src/components/rest_server/http_server.rs b/node/src/components/rest_server/http_server.rs index 9899291014..f8d9db9f1a 100644 --- a/node/src/components/rest_server/http_server.rs +++ b/node/src/components/rest_server/http_server.rs @@ -2,7 +2,6 @@ use std::{convert::Infallible, time::Duration}; use futures::{future, TryFutureExt}; use hyper::server::{conn::AddrIncoming, Builder}; -use tokio::sync::oneshot; use tower::builder::ServiceBuilder; use tracing::{info, warn}; use warp::Filter; @@ -11,17 +10,19 @@ use casper_json_rpc::CorsOrigin; use casper_types::ProtocolVersion; use super::{filters, ReactorEventT}; -use crate::effect::EffectBuilder; +use crate::{ + components::rest_server::Event, effect::EffectBuilder, reactor::QueueKind, + utils::ObservableFuse, +}; /// Run the REST HTTP server. -/// -/// A message received on `shutdown_receiver` will cause the server to exit cleanly. pub(super) async fn run( builder: Builder, effect_builder: EffectBuilder, api_version: ProtocolVersion, - shutdown_receiver: oneshot::Receiver<()>, + shutdown_fuse: ObservableFuse, qps_limit: u64, + cors_origin: Option, ) { // REST filters. let rest_status = filters::create_status_filter(effect_builder, api_version); @@ -31,68 +32,23 @@ pub(super) async fn run( filters::create_validator_changes_filter(effect_builder, api_version); let rest_chainspec_filter = filters::create_chainspec_filter(effect_builder, api_version); - let service = warp::service( - rest_status - .or(rest_metrics) - .or(rest_open_rpc) - .or(rest_validator_changes) - .or(rest_chainspec_filter), - ); + let base_filter = rest_status + .or(rest_metrics) + .or(rest_open_rpc) + .or(rest_validator_changes) + .or(rest_chainspec_filter); - // Start the server, passing a oneshot receiver to allow the server to be shut down gracefully. - let make_svc = - hyper::service::make_service_fn(move |_| future::ok::<_, Infallible>(service.clone())); - - let rate_limited_service = ServiceBuilder::new() - .rate_limit(qps_limit, Duration::from_secs(1)) - .service(make_svc); - - let server = builder.serve(rate_limited_service); - info!(address = %server.local_addr(), "started REST server"); + let filter = match cors_origin { + Some(cors_origin) => base_filter + .with(cors_origin.to_cors_builder().build()) + .map(casper_json_rpc::box_reply) + .boxed(), + None => base_filter.map(casper_json_rpc::box_reply).boxed(), + }; - // Shutdown the server gracefully. - let _ = server - .with_graceful_shutdown(async { - shutdown_receiver.await.ok(); - }) - .map_err(|error| { - warn!(%error, "error running REST server"); - }) - .await; -} + let service = warp::service(filter); -/// Run the REST HTTP server with CORS enabled. -/// -/// A message received on `shutdown_receiver` will cause the server to exit cleanly. -pub(super) async fn run_with_cors( - builder: Builder, - effect_builder: EffectBuilder, - api_version: ProtocolVersion, - shutdown_receiver: oneshot::Receiver<()>, - qps_limit: u64, - cors_origin: CorsOrigin, -) { - // REST filters. - let rest_status = filters::create_status_filter(effect_builder, api_version); - let rest_metrics = filters::create_metrics_filter(effect_builder); - let rest_open_rpc = filters::create_rpc_schema_filter(effect_builder); - let rest_validator_changes = - filters::create_validator_changes_filter(effect_builder, api_version); - let rest_chainspec_filter = filters::create_chainspec_filter(effect_builder, api_version); - - let service = warp::service( - rest_status - .or(rest_metrics) - .or(rest_open_rpc) - .or(rest_validator_changes) - .or(rest_chainspec_filter) - .with(match cors_origin { - CorsOrigin::Any => warp::cors().allow_any_origin(), - CorsOrigin::Specified(origin) => warp::cors().allow_origin(origin.as_str()), - }), - ); - - // Start the server, passing a oneshot receiver to allow the server to be shut down gracefully. + // Start the server, passing a fuse to allow the server to be shut down gracefully. let make_svc = hyper::service::make_service_fn(move |_| future::ok::<_, Infallible>(service.clone())); @@ -101,13 +57,17 @@ pub(super) async fn run_with_cors( .service(make_svc); let server = builder.serve(rate_limited_service); + info!(address = %server.local_addr(), "started REST server"); + effect_builder + .into_inner() + .schedule(Event::BindComplete(server.local_addr()), QueueKind::Regular) + .await; + // Shutdown the server gracefully. let _ = server - .with_graceful_shutdown(async { - shutdown_receiver.await.ok(); - }) + .with_graceful_shutdown(shutdown_fuse.wait_owned()) .map_err(|error| { warn!(%error, "error running REST server"); }) diff --git a/node/src/components/rpc_server.rs b/node/src/components/rpc_server.rs index 7c55c816c1..81b06b977c 100644 --- a/node/src/components/rpc_server.rs +++ b/node/src/components/rpc_server.rs @@ -20,6 +20,7 @@ mod speculative_exec_server; use std::{fmt::Debug, time::Instant}; +use casper_json_rpc::CorsOrigin; use datasize::DataSize; use futures::join; use tracing::{error, info, warn}; @@ -217,7 +218,17 @@ where } ComponentState::Initializing => match event { Event::Initialize => { - let (effects, state) = self.bind(self.config.enable_server, effect_builder); + let (effects, mut state) = self.bind(self.config.enable_server, effect_builder); + + if matches!(state, ComponentState::Initializing) { + // Our current code does not support storing the bound port, so we skip the + // second step and go straight to `Initialized`. If new tests are written + // that rely on an initialized RPC server with a port being available, this + // needs to be refactored. Compare with the REST server on how this could be + // done. + state = ComponentState::Initialized; + } + >::set_state(self, state); effects } @@ -453,7 +464,7 @@ where self.api_version, cfg.qps_limit, cfg.max_body_bytes, - cfg.cors_origin.clone(), + CorsOrigin::parse_str(&cfg.cors_origin), )); Some(()) } else { @@ -468,7 +479,7 @@ where self.api_version, cfg.qps_limit, cfg.max_body_bytes, - cfg.cors_origin.clone(), + CorsOrigin::parse_str(&cfg.cors_origin), )); Ok(Effects::new()) diff --git a/node/src/components/rpc_server/http_server.rs b/node/src/components/rpc_server/http_server.rs index bf9ecc28c4..0d49141eb5 100644 --- a/node/src/components/rpc_server/http_server.rs +++ b/node/src/components/rpc_server/http_server.rs @@ -33,7 +33,7 @@ pub(super) async fn run( api_version: ProtocolVersion, qps_limit: u64, max_body_bytes: u32, - cors_origin: String, + cors_origin: Option, ) { let mut handlers = RequestHandlersBuilder::new(); PutDeploy::register_as_handler(effect_builder, api_version, &mut handlers); @@ -58,41 +58,14 @@ pub(super) async fn run( QueryBalance::register_as_handler(effect_builder, api_version, &mut handlers); let handlers = handlers.build(); - match cors_origin.as_str() { - "" => { - super::rpcs::run( - builder, - handlers, - qps_limit, - max_body_bytes, - RPC_API_PATH, - RPC_API_SERVER_NAME, - ) - .await - } - "*" => { - super::rpcs::run_with_cors( - builder, - handlers, - qps_limit, - max_body_bytes, - RPC_API_PATH, - RPC_API_SERVER_NAME, - CorsOrigin::Any, - ) - .await - } - _ => { - super::rpcs::run_with_cors( - builder, - handlers, - qps_limit, - max_body_bytes, - RPC_API_PATH, - RPC_API_SERVER_NAME, - CorsOrigin::Specified(cors_origin), - ) - .await - } - } + super::rpcs::run( + builder, + handlers, + qps_limit, + max_body_bytes, + RPC_API_PATH, + RPC_API_SERVER_NAME, + cors_origin, + ) + .await } diff --git a/node/src/components/rpc_server/rpcs.rs b/node/src/components/rpc_server/rpcs.rs index 9919b8ae7f..442e44cc09 100644 --- a/node/src/components/rpc_server/rpcs.rs +++ b/node/src/components/rpc_server/rpcs.rs @@ -19,7 +19,6 @@ use hyper::server::{conn::AddrIncoming, Builder}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use tokio::sync::oneshot; use tower::ServiceBuilder; use tracing::info; use warp::Filter; @@ -30,7 +29,7 @@ use casper_json_rpc::{ use casper_types::ProtocolVersion; use super::{ReactorEventT, RpcRequest}; -use crate::effect::EffectBuilder; +use crate::{effect::EffectBuilder, utils::ObservableFuse}; pub use common::ErrorData; use docs::DocExample; pub use error_code::ErrorCode; @@ -254,52 +253,6 @@ pub(super) trait RpcWithOptionalParams { ) -> Result; } -/// Start JSON RPC server with CORS enabled in a background. -pub(super) async fn run_with_cors( - builder: Builder, - handlers: RequestHandlers, - qps_limit: u64, - max_body_bytes: u32, - api_path: &'static str, - server_name: &'static str, - cors_header: CorsOrigin, -) { - let make_svc = hyper::service::make_service_fn(move |_| { - let service_routes = casper_json_rpc::route_with_cors( - api_path, - max_body_bytes, - handlers.clone(), - ALLOW_UNKNOWN_FIELDS_IN_JSON_RPC_REQUEST, - &cors_header, - ); - - // Supports content negotiation for gzip responses. This is an interim fix until - // https://github.com/seanmonstar/warp/pull/513 moves forward. - let service_routes_gzip = warp::header::exact(ACCEPT_ENCODING.as_str(), "gzip") - .and(service_routes.clone()) - .with(warp::compression::gzip()); - - let service = warp::service(service_routes_gzip.or(service_routes)); - async move { Ok::<_, Infallible>(service.clone()) } - }); - - let make_svc = ServiceBuilder::new() - .rate_limit(qps_limit, Duration::from_secs(1)) - .service(make_svc); - - let server = builder.serve(make_svc); - info!(address = %server.local_addr(), "started {} server", server_name); - - let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); - let server_with_shutdown = server.with_graceful_shutdown(async { - shutdown_receiver.await.ok(); - }); - - let _ = tokio::spawn(server_with_shutdown).await; - let _ = shutdown_sender.send(()); - info!("{} server shut down", server_name); -} - /// Start JSON RPC server in a background. pub(super) async fn run( builder: Builder, @@ -308,6 +261,7 @@ pub(super) async fn run( max_body_bytes: u32, api_path: &'static str, server_name: &'static str, + cors_header: Option, ) { let make_svc = hyper::service::make_service_fn(move |_| { let service_routes = casper_json_rpc::route( @@ -315,6 +269,7 @@ pub(super) async fn run( max_body_bytes, handlers.clone(), ALLOW_UNKNOWN_FIELDS_IN_JSON_RPC_REQUEST, + cors_header.as_ref(), ); // Supports content negotiation for gzip responses. This is an interim fix until @@ -334,13 +289,10 @@ pub(super) async fn run( let server = builder.serve(make_svc); info!(address = %server.local_addr(), "started {} server", server_name); - let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); - let server_with_shutdown = server.with_graceful_shutdown(async { - shutdown_receiver.await.ok(); - }); + let shutdown_fuse = ObservableFuse::new(); + let server_with_shutdown = server.with_graceful_shutdown(shutdown_fuse.clone().wait_owned()); let _ = tokio::spawn(server_with_shutdown).await; - let _ = shutdown_sender.send(()); info!("{} server shut down", server_name); } diff --git a/node/src/components/rpc_server/speculative_exec_server.rs b/node/src/components/rpc_server/speculative_exec_server.rs index 002f8761ac..02cc239e75 100644 --- a/node/src/components/rpc_server/speculative_exec_server.rs +++ b/node/src/components/rpc_server/speculative_exec_server.rs @@ -21,47 +21,20 @@ pub(super) async fn run( api_version: ProtocolVersion, qps_limit: u64, max_body_bytes: u32, - cors_origin: String, + cors_origin: Option, ) { let mut handlers = RequestHandlersBuilder::new(); SpeculativeExec::register_as_handler(effect_builder, api_version, &mut handlers); let handlers = handlers.build(); - match cors_origin.as_str() { - "" => { - super::rpcs::run( - builder, - handlers, - qps_limit, - max_body_bytes, - SPECULATIVE_EXEC_API_PATH, - SPECULATIVE_EXEC_SERVER_NAME, - ) - .await; - } - "*" => { - super::rpcs::run_with_cors( - builder, - handlers, - qps_limit, - max_body_bytes, - SPECULATIVE_EXEC_API_PATH, - SPECULATIVE_EXEC_SERVER_NAME, - CorsOrigin::Any, - ) - .await - } - _ => { - super::rpcs::run_with_cors( - builder, - handlers, - qps_limit, - max_body_bytes, - SPECULATIVE_EXEC_API_PATH, - SPECULATIVE_EXEC_SERVER_NAME, - CorsOrigin::Specified(cors_origin), - ) - .await - } - } + super::rpcs::run( + builder, + handlers, + qps_limit, + max_body_bytes, + SPECULATIVE_EXEC_API_PATH, + SPECULATIVE_EXEC_SERVER_NAME, + cors_origin, + ) + .await; } diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index bf8f1b34f4..b722e211b9 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -49,7 +49,6 @@ use std::{ io::ErrorKind, mem, path::{Path, PathBuf}, - rc::Rc, sync::Arc, }; @@ -163,7 +162,7 @@ pub struct Storage { root: PathBuf, /// Environment holding LMDB databases. #[data_size(skip)] - env: Rc, + env: Arc, /// The block header database. #[data_size(skip)] block_header_db: Database, @@ -309,7 +308,7 @@ where event: Self::Event, ) -> Effects { let result = match event { - Event::StorageRequest(req) => self.handle_storage_request::(*req), + Event::StorageRequest(req) => self.handle_storage_request(*req), Event::NetRequestIncoming(ref incoming) => { match self.handle_net_request_incoming::(effect_builder, incoming) { Ok(effects) => Ok(effects), @@ -496,7 +495,7 @@ impl Storage { let mut component = Self { root, - env: Rc::new(env), + env: Arc::new(env), block_header_db, block_body_db, block_metadata_db, @@ -782,7 +781,7 @@ impl Storage { } /// Handles a storage request. - fn handle_storage_request( + fn handle_storage_request( &mut self, req: StorageRequest, ) -> Result, FatalStorageError> { @@ -797,7 +796,7 @@ impl Storage { approvals_hashes, responder, } => { - let env = Rc::clone(&self.env); + let env = Arc::clone(&self.env); let mut txn = env.begin_rw_txn()?; let result = self.write_approvals_hashes(&mut txn, &approvals_hashes)?; txn.commit()?; @@ -925,7 +924,7 @@ impl Storage { execution_results, responder, } => { - let env = Rc::clone(&self.env); + let env = Arc::clone(&self.env); let mut txn = env.begin_rw_txn()?; self.write_execution_results(&mut txn, &block_hash, execution_results)?; txn.commit()?; @@ -1271,7 +1270,7 @@ impl Storage { approvals_hashes: &ApprovalsHashes, execution_results: HashMap, ) -> Result { - let env = Rc::clone(&self.env); + let env = Arc::clone(&self.env); let mut txn = env.begin_rw_txn()?; let wrote = self.write_validated_block(&mut txn, block)?; if !wrote { @@ -1438,7 +1437,7 @@ impl Storage { pub fn write_block(&mut self, block: &Block) -> Result { // Validate the block prior to inserting it into the database block.verify()?; - let env = Rc::clone(&self.env); + let env = Arc::clone(&self.env); let mut txn = env.begin_rw_txn()?; let wrote = self.write_validated_block(&mut txn, block)?; if wrote { @@ -1456,7 +1455,7 @@ impl Storage { pub fn write_complete_block(&mut self, block: &Block) -> Result { // Validate the block prior to inserting it into the database block.verify()?; - let env = Rc::clone(&self.env); + let env = Arc::clone(&self.env); let mut txn = env.begin_rw_txn()?; let wrote = self.write_validated_block(&mut txn, block)?; if wrote { @@ -1650,7 +1649,6 @@ impl Storage { .copied() .unwrap_or(EraId::new(0)); for era_id in (0..=last_era.value()) - .into_iter() .rev() .take(count as usize) .map(EraId::new) diff --git a/node/src/components/storage/metrics.rs b/node/src/components/storage/metrics.rs index b6ee022b65..4c0f7f816d 100644 --- a/node/src/components/storage/metrics.rs +++ b/node/src/components/storage/metrics.rs @@ -1,6 +1,6 @@ use prometheus::{self, IntGauge, Registry}; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; const CHAIN_HEIGHT_NAME: &str = "chain_height"; const CHAIN_HEIGHT_HELP: &str = "highest complete block (DEPRECATED)"; @@ -17,38 +17,24 @@ const LOWEST_AVAILABLE_BLOCK_HELP: &str = #[derive(Debug)] pub struct Metrics { // deprecated - replaced by `highest_available_block` - pub(super) chain_height: IntGauge, - pub(super) highest_available_block: IntGauge, - pub(super) lowest_available_block: IntGauge, - registry: Registry, + pub(super) chain_height: RegisteredMetric, + pub(super) highest_available_block: RegisteredMetric, + pub(super) lowest_available_block: RegisteredMetric, } impl Metrics { /// Constructor of metrics which creates and registers metrics objects for use. pub(super) fn new(registry: &Registry) -> Result { - let chain_height = IntGauge::new(CHAIN_HEIGHT_NAME, CHAIN_HEIGHT_HELP)?; + let chain_height = registry.new_int_gauge(CHAIN_HEIGHT_NAME, CHAIN_HEIGHT_HELP)?; let highest_available_block = - IntGauge::new(HIGHEST_AVAILABLE_BLOCK_NAME, HIGHEST_AVAILABLE_BLOCK_HELP)?; + registry.new_int_gauge(HIGHEST_AVAILABLE_BLOCK_NAME, HIGHEST_AVAILABLE_BLOCK_HELP)?; let lowest_available_block = - IntGauge::new(LOWEST_AVAILABLE_BLOCK_NAME, LOWEST_AVAILABLE_BLOCK_HELP)?; - - registry.register(Box::new(chain_height.clone()))?; - registry.register(Box::new(highest_available_block.clone()))?; - registry.register(Box::new(lowest_available_block.clone()))?; + registry.new_int_gauge(LOWEST_AVAILABLE_BLOCK_NAME, LOWEST_AVAILABLE_BLOCK_HELP)?; Ok(Metrics { chain_height, highest_available_block, lowest_available_block, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.chain_height); - unregister_metric!(self.registry, self.highest_available_block); - unregister_metric!(self.registry, self.lowest_available_block); - } -} diff --git a/node/src/components/sync_leaper/metrics.rs b/node/src/components/sync_leaper/metrics.rs index 04443d493a..f64fabda88 100644 --- a/node/src/components/sync_leaper/metrics.rs +++ b/node/src/components/sync_leaper/metrics.rs @@ -1,6 +1,6 @@ use prometheus::{Histogram, IntCounter, Registry}; -use crate::{unregister_metric, utils}; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; const SYNC_LEAP_DURATION_NAME: &str = "sync_leap_duration_seconds"; const SYNC_LEAP_DURATION_HELP: &str = "duration (in sec) to perform a successful sync leap"; @@ -15,15 +15,13 @@ const LINEAR_BUCKET_COUNT: usize = 4; #[derive(Debug)] pub(super) struct Metrics { /// Time duration to perform a sync leap. - pub(super) sync_leap_duration: Histogram, + pub(super) sync_leap_duration: RegisteredMetric, /// Number of successful sync leap responses that were received from peers. - pub(super) sync_leap_fetched_from_peer: IntCounter, + pub(super) sync_leap_fetched_from_peer: RegisteredMetric, /// Number of requests that were rejected by peers. - pub(super) sync_leap_rejected_by_peer: IntCounter, + pub(super) sync_leap_rejected_by_peer: RegisteredMetric, /// Number of requests that couldn't be fetched from peers. - pub(super) sync_leap_cant_fetch: IntCounter, - - registry: Registry, + pub(super) sync_leap_cant_fetch: RegisteredMetric, } impl Metrics { @@ -35,26 +33,21 @@ impl Metrics { LINEAR_BUCKET_COUNT, )?; - let sync_leap_fetched_from_peer = IntCounter::new( + let sync_leap_fetched_from_peer = registry.new_int_counter( "sync_leap_fetched_from_peer_total".to_string(), "number of successful sync leap responses that were received from peers".to_string(), )?; - let sync_leap_rejected_by_peer = IntCounter::new( + let sync_leap_rejected_by_peer = registry.new_int_counter( "sync_leap_rejected_by_peer_total".to_string(), "number of sync leap requests that were rejected by peers".to_string(), )?; - let sync_leap_cant_fetch = IntCounter::new( + let sync_leap_cant_fetch = registry.new_int_counter( "sync_leap_cant_fetch_total".to_string(), "number of sync leap requests that couldn't be fetched from peers".to_string(), )?; - registry.register(Box::new(sync_leap_fetched_from_peer.clone()))?; - registry.register(Box::new(sync_leap_rejected_by_peer.clone()))?; - registry.register(Box::new(sync_leap_cant_fetch.clone()))?; - Ok(Metrics { - sync_leap_duration: utils::register_histogram_metric( - registry, + sync_leap_duration: registry.new_histogram( SYNC_LEAP_DURATION_NAME, SYNC_LEAP_DURATION_HELP, buckets, @@ -62,16 +55,6 @@ impl Metrics { sync_leap_fetched_from_peer, sync_leap_rejected_by_peer, sync_leap_cant_fetch, - registry: registry.clone(), }) } } - -impl Drop for Metrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.sync_leap_duration); - unregister_metric!(self.registry, self.sync_leap_cant_fetch); - unregister_metric!(self.registry, self.sync_leap_fetched_from_peer); - unregister_metric!(self.registry, self.sync_leap_rejected_by_peer); - } -} diff --git a/node/src/dead_metrics.rs b/node/src/dead_metrics.rs new file mode 100644 index 0000000000..0ece6a7451 --- /dev/null +++ b/node/src/dead_metrics.rs @@ -0,0 +1,42 @@ +//! This file contains metrics that have been retired, but are kept around for now to avoid breaking +//! changes to downstream consumers of said metrics. + +use prometheus::{IntCounter, Registry}; + +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; + +/// Metrics that are never updated. +#[derive(Debug)] +#[allow(dead_code)] +pub(super) struct DeadMetrics { + scheduler_queue_network_low_priority_count: RegisteredMetric, + scheduler_queue_network_demands_count: RegisteredMetric, + accumulated_incoming_limiter_delay: RegisteredMetric, + scheduler_queue_network_incoming_count: RegisteredMetric, +} + +impl DeadMetrics { + /// Creates a new instance of the dead metrics. + pub(super) fn new(registry: &Registry) -> Result { + let scheduler_queue_network_low_priority_count = registry.new_int_counter( + "scheduler_queue_network_low_priority_count", + "retired metric", + )?; + + let scheduler_queue_network_demands_count = + registry.new_int_counter("scheduler_queue_network_demands_count", "retired metric")?; + + let accumulated_incoming_limiter_delay = + registry.new_int_counter("accumulated_incoming_limiter_delay", "retired metric")?; + + let scheduler_queue_network_incoming_count = + registry.new_int_counter("scheduler_queue_network_incoming_count", "retired metric")?; + + Ok(DeadMetrics { + scheduler_queue_network_low_priority_count, + scheduler_queue_network_demands_count, + accumulated_incoming_limiter_delay, + scheduler_queue_network_incoming_count, + }) + } +} diff --git a/node/src/effect.rs b/node/src/effect.rs index 2ad42cd7f9..20b96677ea 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -141,7 +141,7 @@ use crate::{ diagnostics_port::StopAtSpec, fetcher::{FetchItem, FetchResult}, gossiper::GossipItem, - network::{blocklist::BlocklistJustification, FromIncoming, NetworkInsights}, + network::{blocklist::BlocklistJustification, FromIncoming, NetworkInsights, Ticket}, upgrade_watcher::NextUpgrade, }, contract_runtime::SpeculativeExecutionState, @@ -154,7 +154,7 @@ use crate::{ FinalitySignatureId, FinalizedApprovals, FinalizedBlock, LegacyDeploy, MetaBlock, MetaBlockState, NodeId, TrieOrChunk, TrieOrChunkId, }, - utils::{fmt_limit::FmtLimit, SharedFlag, Source}, + utils::{fmt_limit::FmtLimit, SharedFuse, Source}, }; use announcements::{ BlockAccumulatorAnnouncement, ConsensusAnnouncement, ContractRuntimeAnnouncement, @@ -214,7 +214,7 @@ pub(crate) struct Responder { /// Sender through which the response ultimately should be sent. sender: Option>, /// Reactor flag indicating shutdown. - is_shutting_down: SharedFlag, + is_shutting_down: SharedFuse, } /// A responder that will automatically send a `None` on drop. @@ -256,10 +256,6 @@ impl AutoClosingResponder { impl Drop for AutoClosingResponder { fn drop(&mut self) { if let Some(sender) = self.0.sender.take() { - debug!( - sending_value = %self.0, - "responding None by dropping auto-close responder" - ); // We still haven't answered, send an answer. if let Err(_unsent_value) = sender.send(None) { debug!( @@ -274,7 +270,7 @@ impl Drop for AutoClosingResponder { impl Responder { /// Creates a new `Responder`. #[inline] - fn new(sender: oneshot::Sender, is_shutting_down: SharedFlag) -> Self { + fn new(sender: oneshot::Sender, is_shutting_down: SharedFuse) -> Self { Responder { sender: Some(sender), is_shutting_down, @@ -288,7 +284,7 @@ impl Responder { #[cfg(test)] #[inline] pub(crate) fn without_shutdown(sender: oneshot::Sender) -> Self { - Responder::new(sender, SharedFlag::global_shared()) + Responder::new(sender, SharedFuse::global_shared()) } } @@ -673,8 +669,20 @@ impl EffectBuilder { /// Sends a network message. /// - /// The message is queued and sent, but no delivery guaranteed. Will return after the message - /// has been buffered in the outgoing kernel buffer and thus is subject to backpressure. + /// The message is queued and sent, without any delivery guarantees. Will return after the + /// message has been buffered by the networking stack and is thus subject to backpressure + /// from the receiving peer. + /// + /// If the message cannot be buffered immediately, `send_message` will wait until there is room + /// in the networking layer's buffer available. This means that messages will be buffered + /// outside the networking component without any limit, when this method is used. The calling + /// component is responsible for ensuring that not too many instances of `send_message` are + /// awaited at any one point in time. + /// + /// If the peer is not reachable, the message will be discarded. + /// + /// See `try_send_message` for a method that does not buffer messages outside networking if + /// buffers are full, but discards them instead. pub(crate) async fn send_message

(self, dest: NodeId, payload: P) where REv: From>, @@ -683,32 +691,45 @@ impl EffectBuilder { |responder| NetworkRequest::SendMessage { dest: Box::new(dest), payload: Box::new(payload), - respond_after_queueing: false, - auto_closing_responder: AutoClosingResponder::from_opt_responder(responder), + message_queued_responder: Some(AutoClosingResponder::from_opt_responder(responder)), }, QueueKind::Network, ) .await; + + // Note: It does not matter to use whether `Some()` (indicating buffering) or `None` + // (indicating a lost message) was returned, since we do not guarantee anything about + // delivery. } - /// Enqueues a network message. + /// Sends a network message with best effort. + /// + /// The message is queued in "fire-and-forget" fashion, there is no guarantee that the peer will + /// receive it. It may also be dropped if the outbound message queue for the specific peer is + /// full as well, instead of backpressure being propagated. + /// + /// Returns immediately. If called at extreme rates, this function may blow up the event queue, + /// since messages are only discarded once they have made their way to a networking component, + /// while this method returns earlier. /// - /// The message is queued in "fire-and-forget" fashion, there is no guarantee that the peer - /// will receive it. Returns as soon as the message is queued inside the networking component. - pub(crate) async fn enqueue_message

(self, dest: NodeId, payload: P) + /// A more heavyweight message sending function is available in `send_message`. + pub(crate) async fn try_send_message

(self, dest: NodeId, payload: P) where REv: From>, { - self.make_request( - |responder| NetworkRequest::SendMessage { - dest: Box::new(dest), - payload: Box::new(payload), - respond_after_queueing: true, - auto_closing_responder: AutoClosingResponder::from_opt_responder(responder), - }, - QueueKind::Network, - ) - .await; + // Note: Since we do not expect any response to our request, we can avoid spawning an extra + // task awaiting the responder. + + self.event_queue + .schedule( + NetworkRequest::SendMessage { + dest: Box::new(dest), + payload: Box::new(payload), + message_queued_responder: None, + }, + QueueKind::Network, + ) + .await } /// Broadcasts a network message to validator peers in the given era. @@ -811,15 +832,29 @@ impl EffectBuilder { } /// Announces an incoming network message. - pub(crate) async fn announce_incoming

(self, sender: NodeId, payload: P) - where - REv: FromIncoming

, - { + pub(crate) async fn announce_incoming

(self, sender: NodeId, payload: P, ticket: Ticket) + where + REv: FromIncoming

+ From> + Send, + P: 'static + Send, + { + // TODO: Remove demands entirely as they are no longer needed with tickets. + let reactor_event = + match >::try_demand_from_incoming(self, sender, payload) { + Ok((rev, demand_has_been_satisfied)) => { + tokio::spawn(async move { + if let Some(answer) = demand_has_been_satisfied.await { + self.send_message(sender, answer).await; + } + + drop(ticket); + }); + rev + } + Err(payload) => >::from_incoming(sender, payload, ticket), + }; + self.event_queue - .schedule( - >::from_incoming(sender, payload), - QueueKind::NetworkIncoming, - ) + .schedule::(reactor_event, QueueKind::MessageIncoming) .await } diff --git a/node/src/effect/incoming.rs b/node/src/effect/incoming.rs index 8f6857b16c..a88cfc6bde 100644 --- a/node/src/effect/incoming.rs +++ b/node/src/effect/incoming.rs @@ -11,18 +11,20 @@ use datasize::DataSize; use serde::Serialize; use crate::{ - components::{consensus, fetcher::Tag, gossiper}, + components::{consensus, fetcher::Tag, gossiper, network::Ticket}, protocol::Message, types::{FinalitySignature, NodeId, TrieOrChunkIdDisplay}, }; use super::AutoClosingResponder; -/// An envelope for an incoming message, attaching a sender address. +/// An envelope for an incoming message, attaching a sender address and a backpressure ticket. #[derive(DataSize, Debug, Serialize)] pub struct MessageIncoming { pub(crate) sender: NodeId, pub(crate) message: Box, + #[serde(skip)] + pub(crate) ticket: Arc, } impl Display for MessageIncoming diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index b454db6afa..95a2c8dad7 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -97,12 +97,9 @@ pub(crate) enum NetworkRequest

{ dest: Box, /// Message payload. payload: Box

, - /// If `true`, the responder will be called early after the message has been queued, not - /// waiting until it has passed to the kernel. - respond_after_queueing: bool, /// Responder to be called when the message has been *buffered for sending*. #[serde(skip_serializing)] - auto_closing_responder: AutoClosingResponder<()>, + message_queued_responder: Option>, }, /// Send a message on the network to validator peers in the given era. ValidatorBroadcast { @@ -143,13 +140,11 @@ impl

NetworkRequest

{ NetworkRequest::SendMessage { dest, payload, - respond_after_queueing, - auto_closing_responder, + message_queued_responder, } => NetworkRequest::SendMessage { dest, payload: Box::new(wrap_payload(*payload)), - respond_after_queueing, - auto_closing_responder, + message_queued_responder, }, NetworkRequest::ValidatorBroadcast { payload, @@ -205,6 +200,7 @@ pub(crate) enum NetworkInfoRequest { }, /// Get up to `count` fully-connected peers in random order. FullyConnectedPeers { + /// Responder to be called with all connected in random order peers. count: usize, /// Responder to be called with the peers. responder: Responder>, diff --git a/node/src/lib.rs b/node/src/lib.rs index b4a5e7b21b..8b4a956cc6 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -26,6 +26,7 @@ pub mod cli; pub(crate) mod components; mod config_migration; mod data_migration; +mod dead_metrics; pub(crate) mod effect; pub mod logging; pub(crate) mod protocol; diff --git a/node/src/logging.rs b/node/src/logging.rs index dbde0f804d..c04fc0a9fa 100644 --- a/node/src/logging.rs +++ b/node/src/logging.rs @@ -72,21 +72,16 @@ impl LoggingConfig { /// Logging output format. /// /// Defaults to "text"". -#[derive(Clone, DataSize, Debug, Deserialize, Serialize)] +#[derive(Clone, Copy, DataSize, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum LoggingFormat { /// Text format. + #[default] Text, /// JSON format. Json, } -impl Default for LoggingFormat { - fn default() -> Self { - LoggingFormat::Text - } -} - /// This is used to implement tracing's `FormatEvent` so that we can customize the way tracing /// events are formatted. pub struct FmtEvent { @@ -265,7 +260,17 @@ where /// See `init_params` for details. #[cfg(test)] pub fn init() -> anyhow::Result<()> { - init_with_config(&Default::default()) + let mut cfg = LoggingConfig::default(); + + // The `NODE_TEST_LOG` environment variable can be used to specify JSON output when testing. + match env::var("NODE_TEST_LOG") { + Ok(s) if s == "json" => { + cfg.format = LoggingFormat::Json; + } + _ => (), + } + + init_with_config(&cfg) } /// A handle for reloading the logger. diff --git a/node/src/protocol.rs b/node/src/protocol.rs index bcf7e635a3..cfc5255b55 100644 --- a/node/src/protocol.rs +++ b/node/src/protocol.rs @@ -17,7 +17,7 @@ use crate::{ consensus, fetcher::{FetchItem, FetchResponse, Tag}, gossiper, - network::{EstimatorWeights, FromIncoming, GossipedAddress, MessageKind, Payload}, + network::{Channel, FromIncoming, GossipedAddress, MessageKind, Payload, Ticket}, }, effect::{ incoming::{ @@ -112,53 +112,44 @@ impl Payload for Message { } #[inline] - fn incoming_resource_estimate(&self, weights: &EstimatorWeights) -> u32 { + fn get_channel(&self) -> Channel { match self { - Message::Consensus(_) => weights.consensus, - Message::ConsensusRequest(_) => weights.consensus, - Message::BlockGossiper(_) => weights.block_gossip, - Message::DeployGossiper(_) => weights.deploy_gossip, - Message::FinalitySignatureGossiper(_) => weights.finality_signature_gossip, - Message::AddressGossiper(_) => weights.address_gossip, - Message::GetRequest { tag, .. } => match tag { - Tag::Deploy => weights.deploy_requests, - Tag::LegacyDeploy => weights.legacy_deploy_requests, - Tag::Block => weights.block_requests, - Tag::BlockHeader => weights.block_header_requests, - Tag::TrieOrChunk => weights.trie_requests, - Tag::FinalitySignature => weights.finality_signature_requests, - Tag::SyncLeap => weights.sync_leap_requests, - Tag::ApprovalsHashes => weights.approvals_hashes_requests, - Tag::BlockExecutionResults => weights.execution_results_requests, + Message::Consensus(_) => Channel::Consensus, + Message::DeployGossiper(_) => Channel::BulkGossip, + Message::AddressGossiper(_) => Channel::BulkGossip, + Message::GetRequest { + tag, + serialized_id: _, + } => match tag { + Tag::Deploy => Channel::DataRequests, + Tag::LegacyDeploy => Channel::SyncDataRequests, + Tag::Block => Channel::SyncDataRequests, + Tag::BlockHeader => Channel::SyncDataRequests, + Tag::TrieOrChunk => Channel::SyncDataRequests, + Tag::FinalitySignature => Channel::DataRequests, + Tag::SyncLeap => Channel::SyncDataRequests, + Tag::ApprovalsHashes => Channel::SyncDataRequests, + Tag::BlockExecutionResults => Channel::SyncDataRequests, }, - Message::GetResponse { tag, .. } => match tag { - Tag::Deploy => weights.deploy_responses, - Tag::LegacyDeploy => weights.legacy_deploy_responses, - Tag::Block => weights.block_responses, - Tag::BlockHeader => weights.block_header_responses, - Tag::TrieOrChunk => weights.trie_responses, - Tag::FinalitySignature => weights.finality_signature_responses, - Tag::SyncLeap => weights.sync_leap_responses, - Tag::ApprovalsHashes => weights.approvals_hashes_responses, - Tag::BlockExecutionResults => weights.execution_results_responses, + Message::GetResponse { + tag, + serialized_item: _, + } => match tag { + // TODO: Verify which responses are for sync data. + Tag::Deploy => Channel::DataResponses, + Tag::LegacyDeploy => Channel::SyncDataResponses, + Tag::Block => Channel::SyncDataResponses, + Tag::BlockHeader => Channel::SyncDataResponses, + Tag::TrieOrChunk => Channel::SyncDataResponses, + Tag::FinalitySignature => Channel::DataResponses, + Tag::SyncLeap => Channel::SyncDataResponses, + Tag::ApprovalsHashes => Channel::SyncDataResponses, + Tag::BlockExecutionResults => Channel::SyncDataResponses, }, - Message::FinalitySignature(_) => weights.finality_signature_broadcasts, - } - } - - fn is_unsafe_for_syncing_peers(&self) -> bool { - match self { - Message::Consensus(_) => false, - Message::ConsensusRequest(_) => false, - Message::BlockGossiper(_) => false, - Message::DeployGossiper(_) => false, - Message::FinalitySignatureGossiper(_) => false, - Message::AddressGossiper(_) => false, - // Trie requests can deadlock between syncing nodes. - Message::GetRequest { tag, .. } if *tag == Tag::TrieOrChunk => true, - Message::GetRequest { .. } => false, - Message::GetResponse { .. } => false, - Message::FinalitySignature(_) => false, + Message::FinalitySignature(_) => Channel::Consensus, + Message::ConsensusRequest(_) => Channel::Consensus, + Message::BlockGossiper(_) => Channel::BulkGossip, + Message::FinalitySignatureGossiper(_) => Channel::BulkGossip, } } } @@ -309,11 +300,13 @@ where + From + From, { - fn from_incoming(sender: NodeId, payload: Message) -> Self { + fn from_incoming(sender: NodeId, payload: Message, ticket: Ticket) -> Self { + let ticket = Arc::new(ticket); match payload { Message::Consensus(message) => ConsensusMessageIncoming { sender, message: Box::new(message), + ticket, } .into(), Message::ConsensusRequest(_message) => { @@ -323,67 +316,86 @@ where Message::BlockGossiper(message) => GossiperIncoming { sender, message: Box::new(message), + ticket, } .into(), Message::DeployGossiper(message) => GossiperIncoming { sender, + message: Box::new(message), + ticket, } .into(), Message::FinalitySignatureGossiper(message) => GossiperIncoming { sender, message: Box::new(message), + ticket, } .into(), Message::AddressGossiper(message) => GossiperIncoming { sender, + message: Box::new(message), + ticket, } .into(), Message::GetRequest { tag, serialized_id } => match tag { Tag::Deploy => NetRequestIncoming { sender, message: Box::new(NetRequest::Deploy(serialized_id)), + ticket, } .into(), Tag::LegacyDeploy => NetRequestIncoming { sender, + message: Box::new(NetRequest::LegacyDeploy(serialized_id)), + ticket, } .into(), Tag::Block => NetRequestIncoming { sender, message: Box::new(NetRequest::Block(serialized_id)), + ticket, } .into(), Tag::BlockHeader => NetRequestIncoming { sender, message: Box::new(NetRequest::BlockHeader(serialized_id)), + ticket, } .into(), Tag::TrieOrChunk => TrieRequestIncoming { sender, message: Box::new(TrieRequest(serialized_id)), + ticket, } .into(), Tag::FinalitySignature => NetRequestIncoming { sender, + message: Box::new(NetRequest::FinalitySignature(serialized_id)), + ticket, } .into(), Tag::SyncLeap => NetRequestIncoming { sender, message: Box::new(NetRequest::SyncLeap(serialized_id)), + ticket, } .into(), Tag::ApprovalsHashes => NetRequestIncoming { sender, + message: Box::new(NetRequest::ApprovalsHashes(serialized_id)), + ticket, } .into(), Tag::BlockExecutionResults => NetRequestIncoming { sender, + message: Box::new(NetRequest::BlockExecutionResults(serialized_id)), + ticket, } .into(), }, @@ -394,52 +406,68 @@ where Tag::Deploy => NetResponseIncoming { sender, message: Box::new(NetResponse::Deploy(serialized_item)), + ticket, } .into(), Tag::LegacyDeploy => NetResponseIncoming { sender, + message: Box::new(NetResponse::LegacyDeploy(serialized_item)), + ticket, } .into(), Tag::Block => NetResponseIncoming { sender, message: Box::new(NetResponse::Block(serialized_item)), + ticket, } .into(), Tag::BlockHeader => NetResponseIncoming { sender, + message: Box::new(NetResponse::BlockHeader(serialized_item)), + ticket, } .into(), Tag::TrieOrChunk => TrieResponseIncoming { sender, + message: Box::new(TrieResponse(serialized_item.to_vec())), + ticket, } .into(), Tag::FinalitySignature => NetResponseIncoming { sender, + message: Box::new(NetResponse::FinalitySignature(serialized_item)), + ticket, } .into(), Tag::SyncLeap => NetResponseIncoming { sender, message: Box::new(NetResponse::SyncLeap(serialized_item)), + ticket, } .into(), Tag::ApprovalsHashes => NetResponseIncoming { sender, message: Box::new(NetResponse::ApprovalsHashes(serialized_item)), + ticket, } .into(), Tag::BlockExecutionResults => NetResponseIncoming { sender, message: Box::new(NetResponse::BlockExecutionResults(serialized_item)), + ticket, } .into(), }, - Message::FinalitySignature(message) => { - FinalitySignatureIncoming { sender, message }.into() + Message::FinalitySignature(message) => FinalitySignatureIncoming { + sender, + message, + ticket, } + .into(), } } diff --git a/node/src/reactor.rs b/node/src/reactor.rs index 119ea7d552..e83cf18708 100644 --- a/node/src/reactor.rs +++ b/node/src/reactor.rs @@ -34,12 +34,10 @@ pub(crate) mod main_reactor; mod queue_kind; use std::{ - any, collections::HashMap, env, fmt::{Debug, Display}, io::Write, - mem, num::NonZeroU64, str::FromStr, sync::{atomic::Ordering, Arc}, @@ -51,7 +49,7 @@ use erased_serde::Serialize as ErasedSerialize; use fake_instant::FakeClock; use futures::{future::BoxFuture, FutureExt}; use once_cell::sync::Lazy; -use prometheus::{self, Histogram, HistogramOpts, IntCounter, IntGauge, Registry}; +use prometheus::{self, Histogram, IntCounter, IntGauge, Registry}; use quanta::{Clock, IntoNanoseconds}; use serde::Serialize; use signal_hook::consts::signal::{SIGINT, SIGQUIT, SIGTERM}; @@ -60,6 +58,8 @@ use tokio::time::{Duration, Instant}; use tracing::{debug_span, error, info, instrument, trace, warn, Span}; use tracing_futures::Instrument; +#[cfg(test)] +use crate::components::ComponentState; #[cfg(test)] use casper_types::testing::TestRng; @@ -84,8 +84,11 @@ use crate::{ ChainspecRawBytes, Deploy, ExitCode, FinalitySignature, LegacyDeploy, NodeId, SyncLeap, TrieOrChunk, }, - unregister_metric, - utils::{self, SharedFlag, WeightedRoundRobin}, + utils::{ + self, + registered_metric::{RegisteredMetric, RegistryExt}, + Fuse, SharedFuse, WeightedRoundRobin, + }, NodeRng, TERMINATION_REQUESTED, }; pub(crate) use queue_kind::QueueKind; @@ -183,7 +186,7 @@ where /// A reference to the scheduler of the event queue. scheduler: &'static Scheduler, /// Flag indicating whether or not the reactor processing this event queue is shutting down. - is_shutting_down: SharedFlag, + is_shutting_down: SharedFuse, } // Implement `Clone` and `Copy` manually, as `derive` will make it depend on `R` and `Ev` otherwise. @@ -199,7 +202,7 @@ impl Copy for EventQueueHandle {} impl EventQueueHandle { /// Creates a new event queue handle. - pub(crate) fn new(scheduler: &'static Scheduler, is_shutting_down: SharedFlag) -> Self { + pub(crate) fn new(scheduler: &'static Scheduler, is_shutting_down: SharedFuse) -> Self { EventQueueHandle { scheduler, is_shutting_down, @@ -211,7 +214,7 @@ impl EventQueueHandle { /// This method is used in tests, where we are never disabling shutdown warnings anyway. #[cfg(test)] pub(crate) fn without_shutdown(scheduler: &'static Scheduler) -> Self { - EventQueueHandle::new(scheduler, SharedFlag::global_shared()) + EventQueueHandle::new(scheduler, SharedFuse::global_shared()) } /// Schedule an event on a specific queue. @@ -244,7 +247,7 @@ impl EventQueueHandle { } /// Returns whether the associated reactor is currently shutting down. - pub(crate) fn shutdown_flag(&self) -> SharedFlag { + pub(crate) fn shutdown_flag(&self) -> SharedFuse { self.is_shutting_down } } @@ -297,6 +300,15 @@ pub(crate) trait Reactor: Sized { /// Instructs the reactor to update performance metrics, if any. fn update_metrics(&mut self, _event_queue_handle: EventQueueHandle) {} + + /// Returns the state of a named components. + /// + /// May return `None` if the component cannot be found, or if the reactor does not support + /// querying component states. + #[cfg(test)] + fn get_component_state(&self, _name: &str) -> Option<&ComponentState> { + None + } } /// A reactor event type. @@ -372,41 +384,37 @@ where clock: Clock, /// Flag indicating the reactor is being shut down. - is_shutting_down: SharedFlag, + is_shutting_down: SharedFuse, } /// Metric data for the Runner #[derive(Debug)] struct RunnerMetrics { /// Total number of events processed. - events: IntCounter, + events: RegisteredMetric, /// Histogram of how long it took to dispatch an event. - event_dispatch_duration: Histogram, + event_dispatch_duration: RegisteredMetric, /// Total allocated RAM in bytes, as reported by stats_alloc. - allocated_ram_bytes: IntGauge, + allocated_ram_bytes: RegisteredMetric, /// Total consumed RAM in bytes, as reported by sys-info. - consumed_ram_bytes: IntGauge, + consumed_ram_bytes: RegisteredMetric, /// Total system RAM in bytes, as reported by sys-info. - total_ram_bytes: IntGauge, - /// Handle to the metrics registry, in case we need to unregister. - registry: Registry, + total_ram_bytes: RegisteredMetric, } impl RunnerMetrics { /// Create and register new runner metrics. fn new(registry: &Registry) -> Result { - let events = IntCounter::new( + let events = registry.new_int_counter( "runner_events", "running total count of events handled by this reactor", )?; // Create an event dispatch histogram, putting extra emphasis on the area between 1-10 us. - let event_dispatch_duration = Histogram::with_opts( - HistogramOpts::new( - "event_dispatch_duration", - "time in nanoseconds to dispatch an event", - ) - .buckets(vec![ + let event_dispatch_duration = registry.new_histogram( + "event_dispatch_duration", + "time in nanoseconds to dispatch an event", + vec![ 100.0, 500.0, 1_000.0, @@ -426,25 +434,19 @@ impl RunnerMetrics { 1_000_000.0, 2_000_000.0, 5_000_000.0, - ]), + ], )?; let allocated_ram_bytes = - IntGauge::new("allocated_ram_bytes", "total allocated ram in bytes")?; + registry.new_int_gauge("allocated_ram_bytes", "total allocated ram in bytes")?; let consumed_ram_bytes = - IntGauge::new("consumed_ram_bytes", "total consumed ram in bytes")?; - let total_ram_bytes = IntGauge::new("total_ram_bytes", "total system ram in bytes")?; - - registry.register(Box::new(events.clone()))?; - registry.register(Box::new(event_dispatch_duration.clone()))?; - registry.register(Box::new(allocated_ram_bytes.clone()))?; - registry.register(Box::new(consumed_ram_bytes.clone()))?; - registry.register(Box::new(total_ram_bytes.clone()))?; + registry.new_int_gauge("consumed_ram_bytes", "total consumed ram in bytes")?; + let total_ram_bytes = + registry.new_int_gauge("total_ram_bytes", "total system ram in bytes")?; Ok(RunnerMetrics { events, event_dispatch_duration, - registry: registry.clone(), allocated_ram_bytes, consumed_ram_bytes, total_ram_bytes, @@ -452,16 +454,6 @@ impl RunnerMetrics { } } -impl Drop for RunnerMetrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.events); - unregister_metric!(self.registry, self.event_dispatch_duration); - unregister_metric!(self.registry, self.allocated_ram_bytes); - unregister_metric!(self.registry, self.consumed_ram_bytes); - unregister_metric!(self.registry, self.total_ram_bytes); - } -} - impl Runner where R: Reactor, @@ -485,18 +477,6 @@ where ) -> Result { adjust_open_files_limit(); - let event_size = mem::size_of::(); - - // Check if the event is of a reasonable size. This only emits a runtime warning at startup - // right now, since storage size of events is not an issue per se, but copying might be - // expensive if events get too large. - if event_size > 16 * mem::size_of::() { - warn!( - %event_size, type_name = ?any::type_name::(), - "large event size, consider reducing it or boxing" - ); - } - let event_queue_dump_threshold = env::var("CL_EVENT_QUEUE_DUMP_THRESHOLD").map_or(None, |s| s.parse::().ok()); @@ -504,7 +484,7 @@ where QueueKind::weights(), event_queue_dump_threshold, )); - let is_shutting_down = SharedFlag::new(); + let is_shutting_down = SharedFuse::new(); let event_queue = EventQueueHandle::new(scheduler, is_shutting_down); let (reactor, initial_effects) = R::new( cfg, diff --git a/node/src/reactor/event_queue_metrics.rs b/node/src/reactor/event_queue_metrics.rs index a9971bff59..cf1cbc5f01 100644 --- a/node/src/reactor/event_queue_metrics.rs +++ b/node/src/reactor/event_queue_metrics.rs @@ -2,22 +2,20 @@ use std::collections::HashMap; use itertools::Itertools; use prometheus::{self, IntGauge, Registry}; -use tracing::{debug, error}; +use tracing::debug; use crate::{ reactor::{EventQueueHandle, QueueKind}, - unregister_metric, + utils::registered_metric::{RegisteredMetric, RegistryExt}, }; /// Metrics for event queue sizes. #[derive(Debug)] pub(super) struct EventQueueMetrics { /// Per queue kind gauges that measure number of event in the queue. - event_queue_gauges: HashMap, + event_queue_gauges: HashMap>, /// Total events count. - event_total: IntGauge, - /// Instance of registry to unregister from when being dropped. - registry: Registry, + event_total: RegisteredMetric, } impl EventQueueMetrics { @@ -26,31 +24,29 @@ impl EventQueueMetrics { registry: Registry, event_queue_handle: EventQueueHandle, ) -> Result { - let mut event_queue_gauges: HashMap = HashMap::new(); + let mut event_queue_gauges = HashMap::new(); for queue_kind in event_queue_handle.event_queues_counts().keys() { let key = format!("scheduler_queue_{}_count", queue_kind.metrics_name()); - let queue_event_counter = IntGauge::new( + let queue_event_counter = registry.new_int_gauge( key, format!( "current number of events in the reactor {} queue", queue_kind.metrics_name() ), )?; - registry.register(Box::new(queue_event_counter.clone()))?; + let result = event_queue_gauges.insert(*queue_kind, queue_event_counter); assert!(result.is_none(), "Map keys should not be overwritten."); } - let event_total = IntGauge::new( + let event_total = registry.new_int_gauge( "scheduler_queue_total_count", "current total number of events in all reactor queues", )?; - registry.register(Box::new(event_total.clone()))?; Ok(EventQueueMetrics { event_queue_gauges, event_total, - registry, }) } @@ -81,16 +77,3 @@ impl EventQueueMetrics { debug!(%total, %event_counts, "Collected new set of event queue sizes metrics.") } } - -impl Drop for EventQueueMetrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.event_total); - self.event_queue_gauges - .iter() - .for_each(|(key, queue_gauge)| { - self.registry - .unregister(Box::new(queue_gauge.clone())) - .unwrap_or_else(|_| error!("unregistering {} failed: was not registered", key)) - }); - } -} diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 878db42657..436e5d7834 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -27,8 +27,6 @@ use tracing::{debug, error, info, warn}; use casper_types::{EraId, PublicKey, TimeDiff, Timestamp, U512}; -#[cfg(test)] -use crate::testing::network::NetworkedReactor; use crate::{ components::{ block_accumulator::{self, BlockAccumulator}, @@ -51,6 +49,7 @@ use crate::{ upgrade_watcher::{self, UpgradeWatcher}, Component, ValidatorBoundComponent, }, + dead_metrics::DeadMetrics, effect::{ announcements::{ BlockAccumulatorAnnouncement, ConsensusAnnouncement, ContractRuntimeAnnouncement, @@ -78,6 +77,11 @@ use crate::{ utils::{Source, WithDir}, NodeRng, }; +#[cfg(test)] +use crate::{ + components::{ComponentState, InitializedComponent}, + testing::network::NetworkedReactor, +}; pub use config::Config; pub(crate) use error::Error; pub(crate) use event::MainEvent; @@ -170,6 +174,9 @@ pub(crate) struct MainReactor { memory_metrics: MemoryMetrics, #[data_size(skip)] event_queue_metrics: EventQueueMetrics, + #[data_size(skip)] + #[allow(dead_code)] + dead_metrics: DeadMetrics, // ambient settings / data / load-bearing config validator_matrix: ValidatorMatrix, @@ -377,9 +384,11 @@ impl reactor::Reactor for MainReactor { self.storage .handle_event(effect_builder, rng, incoming.into()), ), - MainEvent::NetworkPeerProvidingData(NetResponseIncoming { sender, message }) => { - reactor::handle_get_response(self, effect_builder, rng, sender, message) - } + MainEvent::NetworkPeerProvidingData(NetResponseIncoming { + sender, + message, + ticket: _, // TODO: Properly handle ticket. + }) => reactor::handle_get_response(self, effect_builder, rng, sender, message), MainEvent::AddressGossiper(event) => reactor::wrap_effects( MainEvent::AddressGossiper, self.address_gossiper @@ -855,15 +864,17 @@ impl reactor::Reactor for MainReactor { self.contract_runtime .handle_event(effect_builder, rng, demand.into()), ), - MainEvent::TrieResponseIncoming(TrieResponseIncoming { sender, message }) => { - reactor::handle_fetch_response::( - self, - effect_builder, - rng, - sender, - &message.0, - ) - } + MainEvent::TrieResponseIncoming(TrieResponseIncoming { + sender, + message, + ticket: _, // TODO: Sensibly process ticket. + }) => reactor::handle_fetch_response::( + self, + effect_builder, + rng, + sender, + &message.0, + ), // STORAGE MainEvent::Storage(event) => reactor::wrap_effects( @@ -998,6 +1009,7 @@ impl reactor::Reactor for MainReactor { let metrics = Metrics::new(registry.clone()); let memory_metrics = MemoryMetrics::new(registry.clone())?; let event_queue_metrics = EventQueueMetrics::new(registry.clone(), event_queue)?; + let dead_metrics = DeadMetrics::new(registry)?; let protocol_version = chainspec.protocol_config.version; @@ -1184,6 +1196,7 @@ impl reactor::Reactor for MainReactor { metrics, memory_metrics, event_queue_metrics, + dead_metrics, state: ReactorState::Initialize {}, attempts: 0, @@ -1211,6 +1224,27 @@ impl reactor::Reactor for MainReactor { self.event_queue_metrics .record_event_queue_counts(&event_queue_handle) } + + #[cfg(test)] + fn get_component_state(&self, name: &str) -> Option<&ComponentState> { + match name { + "diagnostics_port" => Some( + >::state(&self.diagnostics_port), + ), + "event_stream_server" => Some( + >::state( + &self.event_stream_server, + ), + ), + "rest_server" => Some(>::state( + &self.rest_server, + )), + "rpc_server" => Some(>::state( + &self.rpc_server, + )), + _ => None, + } + } } impl MainReactor { @@ -1235,6 +1269,10 @@ impl MainReactor { self.block_synchronizer .handle_validators(effect_builder, rng), )); + effects.extend(reactor::wrap_effects( + MainEvent::Network, + self.net.handle_validators(effect_builder, rng), + )); effects } diff --git a/node/src/reactor/main_reactor/catch_up.rs b/node/src/reactor/main_reactor/catch_up.rs index 5c12469998..b37d7d380a 100644 --- a/node/src/reactor/main_reactor/catch_up.rs +++ b/node/src/reactor/main_reactor/catch_up.rs @@ -130,12 +130,10 @@ impl MainReactor { // no trusted hash, no local block, might be genesis self.catch_up_check_genesis() } - Err(storage_err) => { - return Either::Right(CatchUpInstruction::Fatal(format!( - "CatchUp: Could not read storage to find highest switch block header: {}", - storage_err - ))); - } + Err(storage_err) => Either::Right(CatchUpInstruction::Fatal(format!( + "CatchUp: Could not read storage to find highest switch block header: {}", + storage_err + ))), } } Err(err) => Either::Right(CatchUpInstruction::Fatal(format!( diff --git a/node/src/reactor/main_reactor/memory_metrics.rs b/node/src/reactor/main_reactor/memory_metrics.rs index 6aafd47436..fd09187b2a 100644 --- a/node/src/reactor/main_reactor/memory_metrics.rs +++ b/node/src/reactor/main_reactor/memory_metrics.rs @@ -1,135 +1,110 @@ use datasize::DataSize; -use prometheus::{self, Histogram, HistogramOpts, IntGauge, Registry}; +use prometheus::{self, Histogram, IntGauge, Registry}; use tracing::debug; use super::MainReactor; -use crate::unregister_metric; +use crate::utils::registered_metric::{RegisteredMetric, RegistryExt}; /// Metrics for estimated heap memory usage for the main reactor. #[derive(Debug)] pub(super) struct MemoryMetrics { - mem_total: IntGauge, - mem_metrics: IntGauge, - mem_net: IntGauge, - mem_address_gossiper: IntGauge, - mem_storage: IntGauge, - mem_contract_runtime: IntGauge, - mem_rpc_server: IntGauge, - mem_rest_server: IntGauge, - mem_event_stream_server: IntGauge, - mem_consensus: IntGauge, - mem_deploy_gossiper: IntGauge, - mem_finality_signature_gossiper: IntGauge, - mem_block_gossiper: IntGauge, - mem_deploy_buffer: IntGauge, - mem_block_validator: IntGauge, - mem_sync_leaper: IntGauge, - mem_deploy_acceptor: IntGauge, - mem_block_synchronizer: IntGauge, - mem_block_accumulator: IntGauge, - mem_fetchers: IntGauge, - mem_diagnostics_port: IntGauge, - mem_upgrade_watcher: IntGauge, + mem_total: RegisteredMetric, + mem_metrics: RegisteredMetric, + mem_net: RegisteredMetric, + mem_address_gossiper: RegisteredMetric, + mem_storage: RegisteredMetric, + mem_contract_runtime: RegisteredMetric, + mem_rpc_server: RegisteredMetric, + mem_rest_server: RegisteredMetric, + mem_event_stream_server: RegisteredMetric, + mem_consensus: RegisteredMetric, + mem_deploy_gossiper: RegisteredMetric, + mem_finality_signature_gossiper: RegisteredMetric, + mem_block_gossiper: RegisteredMetric, + mem_deploy_buffer: RegisteredMetric, + mem_block_validator: RegisteredMetric, + mem_sync_leaper: RegisteredMetric, + mem_deploy_acceptor: RegisteredMetric, + mem_block_synchronizer: RegisteredMetric, + mem_block_accumulator: RegisteredMetric, + mem_fetchers: RegisteredMetric, + mem_diagnostics_port: RegisteredMetric, + mem_upgrade_watcher: RegisteredMetric, /// Histogram detailing how long it took to measure memory usage. - mem_estimator_runtime_s: Histogram, - registry: Registry, + mem_estimator_runtime_s: RegisteredMetric, } impl MemoryMetrics { /// Initializes a new set of memory metrics. pub(super) fn new(registry: Registry) -> Result { - let mem_total = IntGauge::new("mem_total", "total memory usage in bytes")?; - let mem_metrics = IntGauge::new("mem_metrics", "metrics memory usage in bytes")?; - let mem_net = IntGauge::new("mem_net", "network memory usage in bytes")?; - let mem_address_gossiper = IntGauge::new( + let mem_total = registry.new_int_gauge("mem_total", "total memory usage in bytes")?; + let mem_metrics = registry.new_int_gauge("mem_metrics", "metrics memory usage in bytes")?; + let mem_net = registry.new_int_gauge("mem_net", "network memory usage in bytes")?; + let mem_address_gossiper = registry.new_int_gauge( "mem_address_gossiper", "address_gossiper memory usage in bytes", )?; - let mem_storage = IntGauge::new("mem_storage", "storage memory usage in bytes")?; - let mem_contract_runtime = IntGauge::new( + let mem_storage = registry.new_int_gauge("mem_storage", "storage memory usage in bytes")?; + let mem_contract_runtime = registry.new_int_gauge( "mem_contract_runtime", "contract runtime memory usage in bytes", )?; - let mem_rpc_server = IntGauge::new("mem_rpc_server", "rpc server memory usage in bytes")?; + let mem_rpc_server = + registry.new_int_gauge("mem_rpc_server", "rpc server memory usage in bytes")?; let mem_rest_server = - IntGauge::new("mem_rest_server", "rest server memory usage in bytes")?; - let mem_event_stream_server = IntGauge::new( + registry.new_int_gauge("mem_rest_server", "rest server memory usage in bytes")?; + let mem_event_stream_server = registry.new_int_gauge( "mem_event_stream_server", "event stream server memory usage in bytes", )?; - let mem_consensus = IntGauge::new("mem_consensus", "consensus memory usage in bytes")?; - let mem_fetchers = IntGauge::new("mem_fetchers", "combined fetcher memory usage in bytes")?; - let mem_deploy_gossiper = IntGauge::new( + let mem_consensus = + registry.new_int_gauge("mem_consensus", "consensus memory usage in bytes")?; + let mem_fetchers = + registry.new_int_gauge("mem_fetchers", "combined fetcher memory usage in bytes")?; + let mem_deploy_gossiper = registry.new_int_gauge( "mem_deploy_gossiper", "deploy gossiper memory usage in bytes", )?; - let mem_finality_signature_gossiper = IntGauge::new( + let mem_finality_signature_gossiper = registry.new_int_gauge( "mem_finality_signature_gossiper", "finality signature gossiper memory usage in bytes", )?; let mem_block_gossiper = - IntGauge::new("mem_block_gossiper", "block gossiper memory usage in bytes")?; + registry.new_int_gauge("mem_block_gossiper", "block gossiper memory usage in bytes")?; let mem_deploy_buffer = - IntGauge::new("mem_deploy_buffer", "deploy buffer memory usage in bytes")?; - let mem_block_validator = IntGauge::new( + registry.new_int_gauge("mem_deploy_buffer", "deploy buffer memory usage in bytes")?; + let mem_block_validator = registry.new_int_gauge( "mem_block_validator", "block validator memory usage in bytes", )?; let mem_sync_leaper = - IntGauge::new("mem_sync_leaper", "sync leaper memory usage in bytes")?; - let mem_deploy_acceptor = IntGauge::new( + registry.new_int_gauge("mem_sync_leaper", "sync leaper memory usage in bytes")?; + let mem_deploy_acceptor = registry.new_int_gauge( "mem_deploy_acceptor", "deploy acceptor memory usage in bytes", )?; - let mem_block_synchronizer = IntGauge::new( + let mem_block_synchronizer = registry.new_int_gauge( "mem_block_synchronizer", "block synchronizer memory usage in bytes", )?; - let mem_block_accumulator = IntGauge::new( + let mem_block_accumulator = registry.new_int_gauge( "mem_block_accumulator", "block accumulator memory usage in bytes", )?; - let mem_diagnostics_port = IntGauge::new( + let mem_diagnostics_port = registry.new_int_gauge( "mem_diagnostics_port", "diagnostics port memory usage in bytes", )?; - let mem_upgrade_watcher = IntGauge::new( + let mem_upgrade_watcher = registry.new_int_gauge( "mem_upgrade_watcher", "upgrade watcher memory usage in bytes", )?; - let mem_estimator_runtime_s = Histogram::with_opts( - HistogramOpts::new( - "mem_estimator_runtime_s", - "time in seconds to estimate memory usage", - ) - // Create buckets from one nanosecond to eight seconds. - .buckets(prometheus::exponential_buckets(0.000_000_004, 2.0, 32)?), + let mem_estimator_runtime_s = registry.new_histogram( + "mem_estimator_runtime_s", + "time in seconds to estimate memory usage", + prometheus::exponential_buckets(0.000_000_004, 2.0, 32)?, )?; - registry.register(Box::new(mem_total.clone()))?; - registry.register(Box::new(mem_metrics.clone()))?; - registry.register(Box::new(mem_net.clone()))?; - registry.register(Box::new(mem_address_gossiper.clone()))?; - registry.register(Box::new(mem_storage.clone()))?; - registry.register(Box::new(mem_contract_runtime.clone()))?; - registry.register(Box::new(mem_rpc_server.clone()))?; - registry.register(Box::new(mem_rest_server.clone()))?; - registry.register(Box::new(mem_event_stream_server.clone()))?; - registry.register(Box::new(mem_consensus.clone()))?; - registry.register(Box::new(mem_fetchers.clone()))?; - registry.register(Box::new(mem_deploy_gossiper.clone()))?; - registry.register(Box::new(mem_finality_signature_gossiper.clone()))?; - registry.register(Box::new(mem_block_gossiper.clone()))?; - registry.register(Box::new(mem_deploy_buffer.clone()))?; - registry.register(Box::new(mem_block_validator.clone()))?; - registry.register(Box::new(mem_sync_leaper.clone()))?; - registry.register(Box::new(mem_deploy_acceptor.clone()))?; - registry.register(Box::new(mem_block_synchronizer.clone()))?; - registry.register(Box::new(mem_block_accumulator.clone()))?; - registry.register(Box::new(mem_diagnostics_port.clone()))?; - registry.register(Box::new(mem_upgrade_watcher.clone()))?; - registry.register(Box::new(mem_estimator_runtime_s.clone()))?; - Ok(MemoryMetrics { mem_total, mem_metrics, @@ -154,7 +129,6 @@ impl MemoryMetrics { mem_diagnostics_port, mem_upgrade_watcher, mem_estimator_runtime_s, - registry, }) } @@ -261,32 +235,3 @@ impl MemoryMetrics { "Collected new set of memory metrics."); } } - -impl Drop for MemoryMetrics { - fn drop(&mut self) { - unregister_metric!(self.registry, self.mem_total); - unregister_metric!(self.registry, self.mem_metrics); - unregister_metric!(self.registry, self.mem_estimator_runtime_s); - - unregister_metric!(self.registry, self.mem_net); - unregister_metric!(self.registry, self.mem_address_gossiper); - unregister_metric!(self.registry, self.mem_storage); - unregister_metric!(self.registry, self.mem_contract_runtime); - unregister_metric!(self.registry, self.mem_rpc_server); - unregister_metric!(self.registry, self.mem_rest_server); - unregister_metric!(self.registry, self.mem_event_stream_server); - unregister_metric!(self.registry, self.mem_consensus); - unregister_metric!(self.registry, self.mem_fetchers); - unregister_metric!(self.registry, self.mem_deploy_gossiper); - unregister_metric!(self.registry, self.mem_finality_signature_gossiper); - unregister_metric!(self.registry, self.mem_block_gossiper); - unregister_metric!(self.registry, self.mem_deploy_buffer); - unregister_metric!(self.registry, self.mem_block_validator); - unregister_metric!(self.registry, self.mem_sync_leaper); - unregister_metric!(self.registry, self.mem_deploy_acceptor); - unregister_metric!(self.registry, self.mem_block_synchronizer); - unregister_metric!(self.registry, self.mem_block_accumulator); - unregister_metric!(self.registry, self.mem_diagnostics_port); - unregister_metric!(self.registry, self.mem_upgrade_watcher); - } -} diff --git a/node/src/reactor/main_reactor/tests.rs b/node/src/reactor/main_reactor/tests.rs index af81531e88..53dfec64d7 100644 --- a/node/src/reactor/main_reactor/tests.rs +++ b/node/src/reactor/main_reactor/tests.rs @@ -1,4 +1,11 @@ -use std::{collections::BTreeMap, iter, net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; +use std::{ + collections::{BTreeMap, HashSet}, + fs, iter, + net::SocketAddr, + str::FromStr, + sync::Arc, + time::Duration, +}; use either::Either; use num::Zero; @@ -22,6 +29,7 @@ use crate::{ }, gossiper, network, storage, upgrade_watcher::NextUpgrade, + ComponentState, }, effect::{ incoming::ConsensusMessageIncoming, @@ -41,7 +49,7 @@ use crate::{ ActivationPoint, AvailableBlockRange, Block, BlockHash, BlockHeader, BlockPayload, Chainspec, ChainspecRawBytes, Deploy, ExitCode, NodeId, SyncHandling, }, - utils::{External, Loadable, Source, RESOURCES_PATH}, + utils::{extract_metric_names, External, Fuse, Loadable, Source, RESOURCES_PATH}, WithDir, }; @@ -436,6 +444,18 @@ impl TestFixture { .await; } } + + #[inline(always)] + pub fn network_mut(&mut self) -> &mut TestingNetwork> { + &mut self.network + } + + pub async fn run_until_stopped( + self, + rng: TestRng, + ) -> (TestingNetwork>, TestRng) { + self.network.crank_until_stopped(rng).await + } } /// Given a block height and a node id, returns a predicate to check if the lowest available block @@ -1132,6 +1152,122 @@ async fn empty_block_validation_regression() { } } +#[tokio::test] +#[ignore] // Disabled, until the issue with `TestFixture` and multiple `TestRng`s is fixed. +async fn all_metrics_from_1_5_are_present() { + testing::init_logging(); + + let mut rng = crate::new_rng(); + + let mut fixture = TestFixture::new( + InitialStakes::AllEqual { + count: 4, + stake: 100, + }, + None, + ) + .await; + let net = fixture.network_mut(); + + net.settle_on_component_state( + &mut rng, + "rest_server", + &ComponentState::Initialized, + Duration::from_secs(59), + ) + .await; + + // Get the node ID. + let node_id = *net.nodes().keys().next().unwrap(); + + let rest_addr = net.nodes()[&node_id] + .main_reactor() + .rest_server + .bind_address(); + + // We let the entire network run in the background, until our request completes. + let finish_cranking = fixture.run_until_stopped(rng); + + let metrics_response = reqwest::Client::builder() + .build() + .expect("failed to build client") + .get(format!("http://localhost:{}/metrics", rest_addr.port())) + .timeout(Duration::from_secs(2)) + .send() + .await + .expect("request failed") + .error_for_status() + .expect("error response on metrics request") + .text() + .await + .expect("error retrieving text on metrics request"); + + let (_net, _rng) = finish_cranking.await; + + let actual = extract_metric_names(&metrics_response); + let raw_1_5 = fs::read_to_string(RESOURCES_PATH.join("metrics-1.5.txt")) + .expect("could not read 1.5 metrics snapshot"); + let metrics_1_5 = extract_metric_names(&raw_1_5); + + let missing: HashSet<_> = metrics_1_5.difference(&actual).collect(); + assert!( + missing.is_empty(), + "missing 1.5 metrics in current metrics set: {:?}", + missing + ); +} + +#[tokio::test] +#[ignore] // Disabled, until the issue with `TestFixture` and multiple `TestRng`s is fixed. +async fn port_bound_components_report_ready() { + testing::init_logging(); + + let mut rng = crate::new_rng(); + + let mut fixture = TestFixture::new( + InitialStakes::AllEqual { + count: 4, + stake: 100, + }, + None, + ) + .await; + let net = fixture.network_mut(); + + // Ensure all `PortBoundComponent` implementors report readiness eventually. + net.settle_on_component_state( + &mut rng, + "rest_server", + &ComponentState::Initialized, + Duration::from_secs(10), + ) + .await; + + net.settle_on_component_state( + &mut rng, + "rpc_server", + &ComponentState::Initialized, + Duration::from_secs(10), + ) + .await; + + net.settle_on_component_state( + &mut rng, + "event_stream_server", + &ComponentState::Initialized, + Duration::from_secs(10), + ) + .await; + + net.settle_on_component_state( + &mut rng, + "diagnostics_port", + &ComponentState::Initialized, + Duration::from_secs(10), + ) + .await; +} + #[tokio::test] async fn network_should_recover_from_stall() { // Set up a network with three nodes. diff --git a/node/src/reactor/queue_kind.rs b/node/src/reactor/queue_kind.rs index 52e5bdef14..03ac062c0b 100644 --- a/node/src/reactor/queue_kind.rs +++ b/node/src/reactor/queue_kind.rs @@ -4,7 +4,7 @@ //! round-robin manner. This way, events are only competing for time within one queue, non-congested //! queues can always assume to be speedily processed. -use std::{fmt::Display, num::NonZeroUsize}; +use std::num::NonZeroUsize; use enum_iterator::IntoEnumIterator; use serde::Serialize; @@ -12,18 +12,31 @@ use serde::Serialize; /// Scheduling priority. /// /// Priorities are ordered from lowest to highest. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, IntoEnumIterator, PartialOrd, Ord, Serialize)] +#[derive( + Copy, + Clone, + Debug, + strum::Display, + Eq, + PartialEq, + Hash, + IntoEnumIterator, + PartialOrd, + Ord, + Serialize, + Default, +)] pub enum QueueKind { /// Control messages for the runtime itself. Control, - /// Network events that were initiated outside of this node. + /// Incoming message events that were initiated outside of this node. /// - /// Their load may vary and grouping them together in one queue aides DoS protection. - NetworkIncoming, - /// Network events that are low priority. - NetworkLowPriority, - /// Network events demand a resource directly. - NetworkDemand, + /// Their load may vary and grouping them together in one queue aids DoS protection. + MessageIncoming, + /// Incoming messages that are low priority. + MessageLowPriority, + /// Incoming messages from validators. + MessageValidator, /// Network events that were initiated by the local node, such as outgoing messages. Network, /// NetworkInfo events. @@ -37,6 +50,7 @@ pub enum QueueKind { /// Events of unspecified priority. /// /// This is the default queue. + #[default] Regular, /// Gossiper events. Gossip, @@ -57,37 +71,6 @@ pub enum QueueKind { Api, } -impl Display for QueueKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str_value = match self { - QueueKind::Control => "Control", - QueueKind::NetworkIncoming => "NetworkIncoming", - QueueKind::NetworkLowPriority => "NetworkLowPriority", - QueueKind::NetworkDemand => "NetworkDemand", - QueueKind::Network => "Network", - QueueKind::NetworkInfo => "NetworkInfo", - QueueKind::Fetch => "Fetch", - QueueKind::Regular => "Regular", - QueueKind::Gossip => "Gossip", - QueueKind::FromStorage => "FromStorage", - QueueKind::ToStorage => "ToStorage", - QueueKind::ContractRuntime => "ContractRuntime", - QueueKind::SyncGlobalState => "SyncGlobalState", - QueueKind::FinalitySignature => "FinalitySignature", - QueueKind::Consensus => "Consensus", - QueueKind::Validation => "Validation", - QueueKind::Api => "Api", - }; - write!(f, "{}", str_value) - } -} - -impl Default for QueueKind { - fn default() -> Self { - QueueKind::Regular - } -} - impl QueueKind { /// Returns the weight of a specific queue. /// @@ -95,10 +78,10 @@ impl QueueKind { /// each event processing round. fn weight(self) -> NonZeroUsize { NonZeroUsize::new(match self { - QueueKind::NetworkLowPriority => 1, + QueueKind::MessageLowPriority => 1, QueueKind::NetworkInfo => 2, - QueueKind::NetworkDemand => 2, - QueueKind::NetworkIncoming => 8, + QueueKind::MessageIncoming => 4, + QueueKind::MessageValidator => 8, QueueKind::Network => 4, QueueKind::Regular => 4, QueueKind::Fetch => 4, @@ -127,9 +110,9 @@ impl QueueKind { pub(crate) fn metrics_name(&self) -> &str { match self { QueueKind::Control => "control", - QueueKind::NetworkIncoming => "network_incoming", - QueueKind::NetworkDemand => "network_demands", - QueueKind::NetworkLowPriority => "network_low_priority", + QueueKind::MessageIncoming => "message_incoming", + QueueKind::MessageLowPriority => "message_low_priority", + QueueKind::MessageValidator => "message_validator", QueueKind::Network => "network", QueueKind::NetworkInfo => "network_info", QueueKind::SyncGlobalState => "sync_global_state", diff --git a/node/src/testing/condition_check_reactor.rs b/node/src/testing/condition_check_reactor.rs index af2e0a0fb2..017f26ecb8 100644 --- a/node/src/testing/condition_check_reactor.rs +++ b/node/src/testing/condition_check_reactor.rs @@ -103,6 +103,10 @@ impl Reactor for ConditionCheckReactor { } self.reactor.dispatch_event(effect_builder, rng, event) } + + fn get_component_state(&self, name: &str) -> Option<&crate::components::ComponentState> { + self.inner().get_component_state(name) + } } impl Finalize for ConditionCheckReactor { diff --git a/node/src/testing/filter_reactor.rs b/node/src/testing/filter_reactor.rs index 091040bcbf..c9a068cac9 100644 --- a/node/src/testing/filter_reactor.rs +++ b/node/src/testing/filter_reactor.rs @@ -84,6 +84,10 @@ impl Reactor for FilterReactor { Either::Right(event) => self.reactor.dispatch_event(effect_builder, rng, event), } } + + fn get_component_state(&self, name: &str) -> Option<&crate::components::ComponentState> { + self.inner().get_component_state(name) + } } impl Finalize for FilterReactor { diff --git a/node/src/testing/network.rs b/node/src/testing/network.rs index 320acdcd09..4797512743 100644 --- a/node/src/testing/network.rs +++ b/node/src/testing/network.rs @@ -4,7 +4,10 @@ use std::{ collections::{hash_map::Entry, HashMap}, fmt::Debug, mem, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, time::Duration, }; @@ -12,13 +15,14 @@ use fake_instant::FakeClock as Instant; use futures::future::{BoxFuture, FutureExt}; use serde::Serialize; use tokio::time::{self, error::Elapsed}; -use tracing::{debug, error_span}; +use tracing::{debug, error_span, field, Span}; use tracing_futures::Instrument; use casper_types::testing::TestRng; use super::ConditionCheckReactor; use crate::{ + components::ComponentState, effect::{EffectBuilder, Effects}, reactor::{Finalize, Reactor, Runner, TryCrankOutcome}, tls::KeyFingerprint, @@ -30,7 +34,7 @@ use crate::{ /// Type alias for set of nodes inside a network. /// /// Provided as a convenience for writing condition functions for `settle_on` and friends. -pub(crate) type Nodes = HashMap>>; +pub(crate) type Nodes = HashMap>>>; /// A reactor with networking functionality. /// @@ -60,7 +64,9 @@ const POLL_INTERVAL: Duration = Duration::from_millis(10); #[derive(Debug, Default)] pub(crate) struct TestingNetwork { /// Current network. - nodes: HashMap>>, + nodes: HashMap>>>, + /// Mapping of node IDs to spans. + spans: HashMap, } impl TestingNetwork @@ -105,6 +111,7 @@ where pub(crate) fn new() -> Self { TestingNetwork { nodes: HashMap::new(), + spans: HashMap::new(), } } @@ -141,10 +148,17 @@ where chainspec_raw_bytes: Arc, rng: &'b mut NodeRng, ) -> Result<(NodeId, &mut Runner>), R::Error> { - let runner: Runner> = - Runner::new(cfg, chainspec, chainspec_raw_bytes, rng).await?; + let node_idx = self.nodes.len(); + let span = error_span!("node", node_idx, node_id = field::Empty); + let runner: Box>> = Box::new( + Runner::new(cfg, chainspec, chainspec_raw_bytes, rng) + .instrument(span.clone()) + .await?, + ); let node_id = runner.reactor().node_id(); + span.record("node_id", field::display(node_id)); + self.spans.insert(node_id, span.clone()); let node_ref = match self.nodes.entry(node_id) { Entry::Occupied(_) => { @@ -162,7 +176,7 @@ where pub(crate) fn remove_node( &mut self, node_id: &NodeId, - ) -> Option>> { + ) -> Option>>> { self.nodes.remove(node_id) } @@ -170,10 +184,9 @@ where pub(crate) async fn crank(&mut self, node_id: &NodeId, rng: &mut TestRng) -> TryCrankOutcome { let runner = self.nodes.get_mut(node_id).expect("should find node"); let node_id = runner.reactor().node_id(); - runner - .try_crank(rng) - .instrument(error_span!("crank", node_id = %node_id)) - .await + let span = self.spans.get(&node_id).expect("should find span"); + + runner.try_crank(rng).instrument(span.clone()).await } /// Crank only the specified runner until `condition` is true or until `within` has elapsed. @@ -204,11 +217,9 @@ where let mut event_count = 0; for node in self.nodes.values_mut() { let node_id = node.reactor().node_id(); - match node - .try_crank(rng) - .instrument(error_span!("crank", node_id = %node_id)) - .await - { + let span = self.spans.get(&node_id).expect("span disappeared").clone(); + + match node.try_crank(rng).instrument(span).await { TryCrankOutcome::NoEventsToProcess => (), TryCrankOutcome::ProcessedAnEvent => event_count += 1, TryCrankOutcome::ShouldExit(exit_code) => { @@ -346,6 +357,10 @@ where /// Panics if the `condition` is not reached inside of `within`, or if any node returns an exit /// code. /// + /// If the `condition` is not reached inside of `within`, panics. + // Note: `track_caller` will not have an effect until + // is fixed. + // #[track_caller] /// To settle on an exit code, use `settle_on_exit` instead. pub(crate) async fn settle_on(&mut self, rng: &mut TestRng, condition: F, within: Duration) where @@ -361,6 +376,7 @@ where }) } + // #[track_caller] async fn settle_on_indefinitely(&mut self, rng: &mut TestRng, condition: F) where F: Fn(&Nodes) -> bool, @@ -394,6 +410,64 @@ where .unwrap_or_else(|_| panic!("network did not settle on condition within {:?}", within)) } + /// Keeps cranking the network until every reactor's specified component is in the given state. + /// + /// # Panics + /// + /// Panics if any reactor returns `None` on its [`Reactor::get_component_state()`] call. + pub(crate) async fn settle_on_component_state( + &mut self, + rng: &mut TestRng, + name: &str, + state: &ComponentState, + timeout: Duration, + ) { + self.settle_on( + rng, + |net| { + net.values() + .all(|runner| match runner.reactor().get_component_state(name) { + Some(actual_state) => actual_state == state, + None => panic!("unknown or unsupported component: {}", name), + }) + }, + timeout, + ) + .await; + } + + /// Starts a background process that will crank all nodes until stopped. + /// + /// Returns a future that will, once polled, stop all cranking and return the network and the + /// the random number generator. Note that the stop command will be sent as soon as the returned + /// future is polled (awaited), but no sooner. + pub(crate) fn crank_until_stopped( + mut self, + mut rng: TestRng, + ) -> impl futures::Future + where + R: Send + 'static, + { + let stop = Arc::new(AtomicBool::new(false)); + let handle = tokio::spawn({ + let stop = stop.clone(); + async move { + while !stop.load(Ordering::Relaxed) { + if self.crank_all(&mut rng).await == 0 { + time::sleep(POLL_INTERVAL).await; + }; + } + (self, rng) + } + }); + + async move { + // Trigger the background process stop. + stop.store(true, Ordering::Relaxed); + handle.await.expect("failed to join background crank") + } + } + async fn settle_on_exit_indefinitely(&mut self, rng: &mut TestRng, expected: ExitCode) { let mut exited_as_expected = 0; loop { @@ -435,12 +509,14 @@ where } /// Returns the internal map of nodes. - pub(crate) fn nodes(&self) -> &HashMap>> { + pub(crate) fn nodes(&self) -> &HashMap>>> { &self.nodes } /// Returns the internal map of nodes, mutable. - pub(crate) fn nodes_mut(&mut self) -> &mut HashMap>> { + pub(crate) fn nodes_mut( + &mut self, + ) -> &mut HashMap>>> { &mut self.nodes } @@ -448,7 +524,7 @@ where pub(crate) fn runners_mut( &mut self, ) -> impl Iterator>> { - self.nodes.values_mut() + self.nodes.values_mut().map(|bx| &mut **bx) } /// Returns an iterator over all reactors, mutable. diff --git a/node/src/tls.rs b/node/src/tls.rs index cad3f18468..d29be45841 100644 --- a/node/src/tls.rs +++ b/node/src/tls.rs @@ -55,6 +55,8 @@ use rand::{ use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::utils::LockedLineWriter; + // This is inside a private module so that the generated `BigArray` does not form part of this // crate's public API, and hence also doesn't appear in the rustdocs. mod big_array { @@ -320,9 +322,10 @@ pub fn generate_node_cert() -> SslResult<(X509, PKey)> { pub(crate) fn create_tls_acceptor( cert: &X509Ref, private_key: &PKeyRef, + keylog: Option, ) -> SslResult { let mut builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; - set_context_options(&mut builder, cert, private_key)?; + set_context_options(&mut builder, cert, private_key, keylog)?; Ok(builder.build()) } @@ -334,9 +337,10 @@ pub(crate) fn create_tls_acceptor( pub(crate) fn create_tls_connector( cert: &X509Ref, private_key: &PKeyRef, + keylog: Option, ) -> SslResult { let mut builder = SslConnector::builder(SslMethod::tls_client())?; - set_context_options(&mut builder, cert, private_key)?; + set_context_options(&mut builder, cert, private_key, keylog)?; Ok(builder.build()) } @@ -348,6 +352,7 @@ fn set_context_options( ctx: &mut SslContextBuilder, cert: &X509Ref, private_key: &PKeyRef, + keylog: Option, ) -> SslResult<()> { ctx.set_min_proto_version(Some(SslVersion::TLS1_3))?; @@ -361,6 +366,14 @@ fn set_context_options( // handshake has completed. ctx.set_verify_callback(SslVerifyMode::PEER, |_, _| true); + if let Some(writer) = keylog { + ctx.set_keylog_callback(move |_ssl_ref, str| { + let mut line = str.to_owned(); + line.push('\n'); + writer.write_line(&line); + }); + } + Ok(()) } diff --git a/node/src/types/block.rs b/node/src/types/block.rs index 0c89f354f3..6eeafb06cf 100644 --- a/node/src/types/block.rs +++ b/node/src/types/block.rs @@ -1887,7 +1887,7 @@ impl BlockExecutionResultsOrChunk { num_results: usize, ) -> Self { let execution_results: Vec = - (0..num_results).into_iter().map(|_| rng.gen()).collect(); + (0..num_results).map(|_| rng.gen()).collect(); Self { block_hash, diff --git a/node/src/types/validator_matrix.rs b/node/src/types/validator_matrix.rs index 58c139a4ed..adc81a4446 100644 --- a/node/src/types/validator_matrix.rs +++ b/node/src/types/validator_matrix.rs @@ -217,10 +217,6 @@ impl ValidatorMatrix { self.finality_threshold_fraction } - pub(crate) fn is_empty(&self) -> bool { - self.read_inner().is_empty() - } - /// Returns whether `pub_key` is the ID of a validator in this era, or `None` if the validator /// information for that era is missing. pub(crate) fn is_validator_in_era( @@ -251,6 +247,9 @@ impl ValidatorMatrix { } /// Determine if the active validator is in a current or upcoming set of active validators. + /// + /// The set is not guaranteed to be minimal, as it will include validators up to `auction_delay + /// + 1` back eras from the highest era known. #[inline] pub(crate) fn is_active_or_upcoming_validator(&self, public_key: &PublicKey) -> bool { // This function is potentially expensive and could be memoized, with the cache being @@ -262,6 +261,21 @@ impl ValidatorMatrix { .any(|validator_weights| validator_weights.is_validator(public_key)) } + /// Return the set of active or upcoming validators. + /// + /// The set is not guaranteed to be minimal, as it will include validators up to `auction_delay + /// + 1` back eras from the highest era known. + #[inline] + pub(crate) fn active_or_upcoming_validators(&self) -> HashSet { + self.read_inner() + .values() + .rev() + .take(self.auction_delay as usize + 1) + .flat_map(|validator_weights| validator_weights.validator_public_keys()) + .cloned() + .collect() + } + pub(crate) fn create_finality_signature( &self, block_header: &BlockHeader, @@ -543,7 +557,6 @@ mod tests { let mut era_validator_weights = vec![validator_matrix.validator_weights(0.into()).unwrap()]; era_validator_weights.extend( (1..MAX_VALIDATOR_MATRIX_ENTRIES as u64) - .into_iter() .map(EraId::from) .map(empty_era_validator_weights), ); @@ -636,7 +649,6 @@ mod tests { let mut era_validator_weights = vec![validator_matrix.validator_weights(0.into()).unwrap()]; era_validator_weights.extend( (1..=MAX_VALIDATOR_MATRIX_ENTRIES as u64) - .into_iter() .map(EraId::from) .map(empty_era_validator_weights), ); @@ -653,12 +665,7 @@ mod tests { } // Register eras [7, 8, 9]. - era_validator_weights.extend( - (7..=9) - .into_iter() - .map(EraId::from) - .map(empty_era_validator_weights), - ); + era_validator_weights.extend((7..=9).map(EraId::from).map(empty_era_validator_weights)); for evw in era_validator_weights.iter().rev().take(3).cloned() { assert!( validator_matrix.register_era_validator_weights(evw), diff --git a/node/src/utils.rs b/node/src/utils.rs index 7ca3085f04..9a0f3bb161 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -6,7 +6,9 @@ mod display_error; pub(crate) mod ds; mod external; pub(crate) mod fmt_limit; +mod fuse; pub(crate) mod opt_display; +pub(crate) mod registered_metric; #[cfg(target_os = "linux")] pub(crate) mod rlimit; pub(crate) mod round_robin; @@ -18,22 +20,21 @@ use std::{ any, cell::RefCell, fmt::{self, Debug, Display, Formatter}, - io, + fs::File, + io::{self, Write}, net::{SocketAddr, ToSocketAddrs}, ops::{Add, BitXorAssign, Div}, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{Arc, Mutex}, time::{Duration, Instant, SystemTime}, }; use datasize::DataSize; +use fs2::FileExt; +use futures::future::Either; use hyper::server::{conn::AddrIncoming, Builder, Server}; -#[cfg(test)] -use once_cell::sync::Lazy; -use prometheus::{self, Histogram, HistogramOpts, Registry}; + +use prometheus::{self, IntGauge}; use serde::Serialize; use thiserror::Error; use tracing::{error, warn}; @@ -44,7 +45,10 @@ pub(crate) use display_error::display_error; #[cfg(test)] pub(crate) use external::RESOURCES_PATH; pub use external::{External, LoadError, Loadable}; +pub(crate) use fuse::{DropSwitch, Fuse, ObservableFuse, SharedFuse}; pub(crate) use round_robin::WeightedRoundRobin; +#[cfg(test)] +pub(crate) use tests::extract_metric_names; /// DNS resolution error. #[derive(Debug, Error)] @@ -156,42 +160,30 @@ pub(crate) fn leak(value: T) -> &'static T { Box::leak(Box::new(value)) } -/// A flag shared across multiple subsystem. -#[derive(Copy, Clone, DataSize, Debug)] -pub(crate) struct SharedFlag(&'static AtomicBool); - -impl SharedFlag { - /// Creates a new shared flag. - /// - /// The flag is initially not set. - pub(crate) fn new() -> Self { - SharedFlag(leak(AtomicBool::new(false))) - } - - /// Checks whether the flag is set. - pub(crate) fn is_set(self) -> bool { - self.0.load(Ordering::SeqCst) - } - - /// Set the flag. - pub(crate) fn set(self) { - self.0.store(true, Ordering::SeqCst) - } - - /// Returns a shared instance of the flag for testing. - /// - /// The returned flag should **never** have `set` be called upon it. - #[cfg(test)] - pub(crate) fn global_shared() -> Self { - static SHARED_FLAG: Lazy = Lazy::new(SharedFlag::new); +/// An "unlimited semaphore". +/// +/// Upon construction, `TokenizedCount` increases a given `IntGauge` by one for metrics purposed. +/// +/// Once it is dropped, the underlying gauge will be decreased by one. +#[derive(Debug)] +pub(crate) struct TokenizedCount { + /// The gauge modified on construction/drop. + gauge: Option, +} - *SHARED_FLAG +impl TokenizedCount { + /// Create a new tokenized count, increasing the given gauge. + pub(crate) fn new(gauge: IntGauge) -> Self { + gauge.inc(); + TokenizedCount { gauge: Some(gauge) } } } -impl Default for SharedFlag { - fn default() -> Self { - Self::new() +impl Drop for TokenizedCount { + fn drop(&mut self) { + if let Some(gauge) = self.gauge.take() { + gauge.dec(); + } } } @@ -335,34 +327,6 @@ where (numerator + denominator / T::from(2)) / denominator } -/// Creates a prometheus Histogram and registers it. -pub(crate) fn register_histogram_metric( - registry: &Registry, - metric_name: &str, - metric_help: &str, - buckets: Vec, -) -> Result { - let histogram_opts = HistogramOpts::new(metric_name, metric_help).buckets(buckets); - let histogram = Histogram::with_opts(histogram_opts)?; - registry.register(Box::new(histogram.clone()))?; - Ok(histogram) -} - -/// Unregisters a metric from the Prometheus registry. -#[macro_export] -macro_rules! unregister_metric { - ($registry:expr, $metric:expr) => { - $registry - .unregister(Box::new($metric.clone())) - .unwrap_or_else(|_| { - tracing::error!( - "unregistering {} failed: was not registered", - stringify!($metric) - ) - }); - }; -} - /// XORs two byte sequences. /// /// # Panics @@ -416,6 +380,47 @@ pub(crate) async fn wait_for_arc_drop( false } +/// A thread-safe wrapper around a file that writes chunks. +/// +/// A chunk can (but needn't) be a line. The writer guarantees it will be written to the wrapped +/// file, even if other threads are attempting to write chunks at the same time. +#[derive(Clone)] +pub(crate) struct LockedLineWriter(Arc>); + +impl LockedLineWriter { + /// Creates a new `LockedLineWriter`. + /// + /// This function does not panic - if any error occurs, it will be logged and ignored. + pub(crate) fn new(file: File) -> Self { + LockedLineWriter(Arc::new(Mutex::new(file))) + } + + /// Writes a chunk to the wrapped file. + pub(crate) fn write_line(&self, line: &str) { + match self.0.lock() { + Ok(mut guard) => { + // Acquire a lock on the file. This ensures we do not garble output when multiple + // nodes are writing to the same file. + if let Err(err) = guard.lock_exclusive() { + warn!(%line, %err, "could not acquire file lock, not writing line"); + return; + } + + if let Err(err) = guard.write_all(line.as_bytes()) { + warn!(%line, %err, "could not finish writing line"); + } + + if let Err(err) = guard.unlock() { + warn!(%err, "failed to release file lock in locked line writer, ignored"); + } + } + Err(_) => { + error!(%line, "line writer lock poisoned, lost line"); + } + } + } +} + /// An anchor for converting an `Instant` into a wall-clock (`SystemTime`) time. #[derive(Copy, Clone, Debug)] pub(crate) struct TimeAnchor { @@ -447,13 +452,50 @@ impl TimeAnchor { } } +/// Discard secondary data from a value. +pub(crate) trait Peel { + /// What is left after discarding the wrapping. + type Inner; + + /// Discard "uninteresting" data. + fn peel(self) -> Self::Inner; +} + +impl Peel for Either<(A, G), (B, F)> { + type Inner = Either; + + fn peel(self) -> Self::Inner { + match self { + Either::Left((v, _)) => Either::Left(v), + Either::Right((v, _)) => Either::Right(v), + } + } +} + #[cfg(test)] mod tests { - use std::{sync::Arc, time::Duration}; + use std::{collections::HashSet, sync::Arc, time::Duration}; + + use prometheus::IntGauge; + + use super::{wait_for_arc_drop, xor, TokenizedCount}; - use crate::utils::SharedFlag; + /// Extracts the names of all metrics contained in a prometheus-formatted metrics snapshot. - use super::{wait_for_arc_drop, xor}; + pub(crate) fn extract_metric_names(raw: &str) -> HashSet<&str> { + raw.lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + None + } else { + let (full_id, _) = trimmed.split_once(' ')?; + let id = full_id.split_once('{').map(|v| v.0).unwrap_or(full_id); + Some(id) + } + }) + .collect() + } #[test] fn xor_works() { @@ -511,20 +553,51 @@ mod tests { } #[test] - fn shared_flag_sanity_check() { - let flag = SharedFlag::new(); - let copied = flag; - - assert!(!flag.is_set()); - assert!(!copied.is_set()); - assert!(!flag.is_set()); - assert!(!copied.is_set()); - - flag.set(); + fn tokenized_count_sanity_check() { + let gauge = IntGauge::new("sanity_gauge", "tokenized count test gauge") + .expect("failed to construct IntGauge in test"); + + gauge.inc(); + gauge.inc(); + assert_eq!(gauge.get(), 2); + + let ticket1 = TokenizedCount::new(gauge.clone()); + let ticket2 = TokenizedCount::new(gauge.clone()); + + assert_eq!(gauge.get(), 4); + drop(ticket2); + assert_eq!(gauge.get(), 3); + drop(ticket1); + assert_eq!(gauge.get(), 2); + } - assert!(flag.is_set()); - assert!(copied.is_set()); - assert!(flag.is_set()); - assert!(copied.is_set()); + #[test] + fn can_parse_metrics() { + let sample = r#" + chain_height 0 + # HELP consensus_current_era the current era in consensus + # TYPE consensus_current_era gauge + consensus_current_era 0 + # HELP consumed_ram_bytes total consumed ram in bytes + # TYPE consumed_ram_bytes gauge + consumed_ram_bytes 0 + # HELP contract_runtime_apply_commit time in seconds to commit the execution effects of a contract + # TYPE contract_runtime_apply_commit histogram + contract_runtime_apply_commit_bucket{le="0.01"} 0 + contract_runtime_apply_commit_bucket{le="0.02"} 0 + contract_runtime_apply_commit_bucket{le="0.04"} 0 + contract_runtime_apply_commit_bucket{le="0.08"} 0 + contract_runtime_apply_commit_bucket{le="0.16"} 0 + "#; + + let extracted = extract_metric_names(sample); + + let mut expected = HashSet::new(); + expected.insert("chain_height"); + expected.insert("consensus_current_era"); + expected.insert("consumed_ram_bytes"); + expected.insert("contract_runtime_apply_commit_bucket"); + + assert_eq!(extracted, expected); } } diff --git a/node/src/utils/external.rs b/node/src/utils/external.rs index 278ae2767e..e5a112056f 100644 --- a/node/src/utils/external.rs +++ b/node/src/utils/external.rs @@ -43,12 +43,13 @@ pub static RESOURCES_PATH: Lazy = /// An `External` also always provides a default, which will always result in an error when `load` /// is called. Should the underlying type `T` implement `Default`, the `with_default` can be /// used instead. -#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize, Default)] #[serde(untagged)] pub enum External { /// Value that should be loaded from an external path. Path(PathBuf), /// The value has not been specified, but a default has been requested. + #[default] #[serde(skip)] Missing, } @@ -93,22 +94,17 @@ pub trait Loadable: Sized { /// Load a test-only instance from the local path. #[cfg(test)] fn from_resources>(rel_path: P) -> Self { - Self::from_path(RESOURCES_PATH.join(rel_path.as_ref())).unwrap_or_else(|error| { + let full_path = RESOURCES_PATH.join(rel_path.as_ref()); + Self::from_path(&full_path).unwrap_or_else(|error| { panic!( "could not load resources from {}: {}", - rel_path.as_ref().display(), + full_path.display(), error ) }) } } -impl Default for External { - fn default() -> Self { - External::Missing - } -} - fn display_res_path(result: &Result) -> String { result .as_ref() diff --git a/node/src/utils/fmt_limit.rs b/node/src/utils/fmt_limit.rs index ae8ec19f44..c11f4c6129 100644 --- a/node/src/utils/fmt_limit.rs +++ b/node/src/utils/fmt_limit.rs @@ -103,7 +103,7 @@ mod tests { #[test] fn limit_debug_works() { - let collection: Vec<_> = (0..5).into_iter().collect(); + let collection: Vec<_> = (0..5).collect(); // Sanity check. assert_eq!(format!("{:?}", collection), "[0, 1, 2, 3, 4]"); diff --git a/node/src/utils/fuse.rs b/node/src/utils/fuse.rs new file mode 100644 index 0000000000..0974585dff --- /dev/null +++ b/node/src/utils/fuse.rs @@ -0,0 +1,218 @@ +/// Fuses of various kind. +/// +/// A fuse is a boolean flag that can only be set once, but checked any number of times. +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use datasize::DataSize; +use tokio::sync::Notify; + +use super::leak; + +/// A one-time settable boolean flag. +pub(crate) trait Fuse { + /// Trigger the fuse. + fn set(&self); +} + +/// A set-once-only flag shared across multiple subsystems. +#[derive(Copy, Clone, DataSize, Debug)] +pub(crate) struct SharedFuse(&'static AtomicBool); + +impl SharedFuse { + /// Creates a new shared fuse. + /// + /// The fuse is initially not set. + pub(crate) fn new() -> Self { + SharedFuse(leak(AtomicBool::new(false))) + } + + /// Checks whether the fuse is set. + pub(crate) fn is_set(self) -> bool { + self.0.load(Ordering::SeqCst) + } + + /// Returns a shared instance of the fuse for testing. + /// + /// The returned fuse should **never** have `set` be called upon it, since there is only once + /// instance globally. + #[cfg(test)] + pub(crate) fn global_shared() -> Self { + use once_cell::sync::Lazy; + + static SHARED_FUSE: Lazy = Lazy::new(SharedFuse::new); + + *SHARED_FUSE + } +} + +impl Fuse for SharedFuse { + fn set(&self) { + self.0.store(true, Ordering::SeqCst) + } +} + +impl Default for SharedFuse { + fn default() -> Self { + Self::new() + } +} + +/// A shared fuse that can be observed for change. +/// +/// It is similar to a condition var, except it can only bet set once and will immediately return +/// if it was previously set. +#[derive(DataSize, Clone, Debug)] +pub(crate) struct ObservableFuse(Arc); + +impl ObservableFuse { + /// Creates a new sticky fuse. + /// + /// The fuse will start out as not set. + pub(crate) fn new() -> Self { + ObservableFuse(Arc::new(ObservableFuseInner { + fuse: AtomicBool::new(false), + notify: Notify::new(), + })) + } +} + +/// Inner implementation of the `ObservableFuse`. +#[derive(DataSize, Debug)] +struct ObservableFuseInner { + /// The fuse to trigger. + #[data_size(skip)] + fuse: AtomicBool, + /// Notification that the fuse has been triggered. + #[data_size(skip)] + notify: Notify, +} + +impl ObservableFuse { + /// Waits for the fuse to be triggered. + /// + /// If the fuse is already set, returns immediately, otherwise waits for the notification. + /// + /// The future returned by this function is safe to cancel. + pub(crate) async fn wait(&self) { + // Note: We will catch all notifications from the point on where `notified()` is called, so + // we first construct the future, then check the fuse. Any notification sent while we + // were loading will be caught in the `notified.await`. + let notified = self.0.notify.notified(); + + if self.0.fuse.load(Ordering::SeqCst) { + return; + } + + notified.await; + } + + /// Owned wait function. + /// + /// Like wait, but owns `self`, thus it can be called and passed around with a static lifetime. + pub(crate) async fn wait_owned(self) { + self.wait().await; + } +} + +impl Fuse for ObservableFuse { + fn set(&self) { + self.0.fuse.store(true, Ordering::SeqCst); + self.0.notify.notify_waiters(); + } +} + +/// A wrapper for a fuse that will cause it to be set when dropped. +// Note: Do not implement/derive `Clone` for `DropSwitch`, as this is a massive footgun. Creating a +// new instance explicitly is safer, as it avoid unintentially trigger the entire switch from +// after having created it on the stack and passed on a clone instance. +#[derive(DataSize, Debug)] +pub(crate) struct DropSwitch(T) +where + T: Fuse; + +impl DropSwitch +where + T: Fuse, +{ + /// Creates a new drop switch around a fuse. + pub(crate) fn new(fuse: T) -> Self { + DropSwitch(fuse) + } + + /// Access the wrapped fuse. + pub(crate) fn inner(&self) -> &T { + &self.0 + } +} + +impl Drop for DropSwitch +where + T: Fuse, +{ + fn drop(&mut self) { + self.0.set() + } +} + +#[cfg(test)] +mod tests { + use futures::FutureExt; + + use crate::utils::Fuse; + + use super::{DropSwitch, ObservableFuse, SharedFuse}; + + #[test] + fn shared_fuse_sanity_check() { + let fuse = SharedFuse::new(); + let copied = fuse; + + assert!(!fuse.is_set()); + assert!(!copied.is_set()); + assert!(!fuse.is_set()); + assert!(!copied.is_set()); + + fuse.set(); + + assert!(fuse.is_set()); + assert!(copied.is_set()); + assert!(fuse.is_set()); + assert!(copied.is_set()); + } + + #[test] + fn observable_fuse_sanity_check() { + let fuse = ObservableFuse::new(); + assert!(fuse.wait().now_or_never().is_none()); + + fuse.set(); + + // Should finish immediately due to the fuse being set. + assert!(fuse.wait().now_or_never().is_some()); + } + + #[test] + fn observable_fuse_drop_switch_check() { + let fuse = ObservableFuse::new(); + assert!(fuse.wait().now_or_never().is_none()); + + let drop_switch = DropSwitch::new(fuse.clone()); + assert!(fuse.wait().now_or_never().is_none()); + + drop(drop_switch); + assert!(fuse.wait().now_or_never().is_some()); + } + + #[test] + fn observable_fuse_race_condition_check() { + let fuse = ObservableFuse::new(); + assert!(fuse.wait().now_or_never().is_none()); + + let waiting = fuse.wait(); + fuse.set(); + assert!(waiting.now_or_never().is_some()); + } +} diff --git a/node/src/utils/registered_metric.rs b/node/src/utils/registered_metric.rs new file mode 100644 index 0000000000..8a5cb7f448 --- /dev/null +++ b/node/src/utils/registered_metric.rs @@ -0,0 +1,204 @@ +//! Self registering and deregistering metrics support. + +use prometheus::{ + core::{Atomic, Collector, GenericCounter, GenericGauge}, + Counter, Gauge, Histogram, HistogramOpts, HistogramTimer, IntCounter, IntGauge, Registry, +}; + +/// A metric wrapper that will deregister the metric from a given registry on drop. +#[derive(Debug)] +pub(crate) struct RegisteredMetric +where + T: Collector + 'static, +{ + metric: Option>, + registry: Registry, +} + +impl RegisteredMetric +where + T: Collector + 'static, +{ + /// Creates a new self-deregistering metric. + pub(crate) fn new(registry: Registry, metric: T) -> Result + where + T: Clone, + { + let boxed_metric = Box::new(metric); + registry.register(boxed_metric.clone())?; + + Ok(RegisteredMetric { + metric: Some(boxed_metric), + registry, + }) + } + + /// Returns a reference to the wrapped metric. + #[inline] + pub(crate) fn inner(&self) -> &T { + self.metric.as_ref().expect("metric disappeared") + } +} + +impl

RegisteredMetric> +where + P: Atomic, +{ + /// Increments the counter. + #[inline] + pub(crate) fn inc(&self) { + self.inner().inc() + } + + /// Increments the counter by set amount. + #[inline] + pub(crate) fn inc_by(&self, v: P::T) { + self.inner().inc_by(v) + } +} + +impl

RegisteredMetric> +where + P: Atomic, +{ + /// Decrements the gauge. + #[inline] + pub(crate) fn dec(&self) { + self.inner().dec() + } + + /// Returns the gauge value. + #[cfg(test)] + #[inline] + pub(crate) fn get(&self) -> P::T { + self.inner().get() + } + + /// Increments the gauge. + #[inline] + pub(crate) fn inc(&self) { + self.inner().inc() + } + + /// Sets the gauge value. + #[inline] + pub(crate) fn set(&self, v: P::T) { + self.inner().set(v) + } +} + +impl RegisteredMetric { + /// Observes a given value. + #[inline] + pub(crate) fn observe(&self, v: f64) { + self.inner().observe(v) + } + + /// Creates a new histogram timer. + #[inline] + pub(crate) fn start_timer(&self) -> HistogramTimer { + self.inner().start_timer() + } +} + +impl Drop for RegisteredMetric +where + T: Collector + 'static, +{ + fn drop(&mut self) { + if let Some(boxed_metric) = self.metric.take() { + let desc = boxed_metric + .desc() + .first() + .map(|desc| desc.fq_name.clone()) + .unwrap_or_default(); + self.registry.unregister(boxed_metric).unwrap_or_else(|_| { + tracing::error!("unregistering {} failed: was not registered", desc) + }) + } + } +} + +/// Extension trait for [`Registry`] instances. +pub(crate) trait RegistryExt { + /// Creates a new [`Counter`] registered to this registry. + fn new_counter, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error>; + + /// Creates a new [`Histogram`] registered to this registry. + fn new_histogram, S2: Into>( + &self, + name: S1, + help: S2, + buckets: Vec, + ) -> Result, prometheus::Error>; + + /// Creates a new [`Gauge`] registered to this registry. + fn new_gauge, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error>; + + /// Creates a new [`IntCounter`] registered to this registry. + fn new_int_counter, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error>; + + /// Creates a new [`IntGauge`] registered to this registry. + fn new_int_gauge, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error>; +} + +impl RegistryExt for Registry { + fn new_counter, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error> { + RegisteredMetric::new(self.clone(), Counter::new(name, help)?) + } + + fn new_gauge, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error> { + RegisteredMetric::new(self.clone(), Gauge::new(name, help)?) + } + + fn new_histogram, S2: Into>( + &self, + name: S1, + help: S2, + buckets: Vec, + ) -> Result, prometheus::Error> { + let histogram_opts = HistogramOpts::new(name, help).buckets(buckets); + + RegisteredMetric::new(self.clone(), Histogram::with_opts(histogram_opts)?) + } + + fn new_int_counter, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error> { + RegisteredMetric::new(self.clone(), IntCounter::new(name, help)?) + } + + fn new_int_gauge, S2: Into>( + &self, + name: S1, + help: S2, + ) -> Result, prometheus::Error> { + RegisteredMetric::new(self.clone(), IntGauge::new(name, help)?) + } +} diff --git a/node/src/utils/specimen.rs b/node/src/utils/specimen.rs index c59122c5e1..9e8c706e72 100644 --- a/node/src/utils/specimen.rs +++ b/node/src/utils/specimen.rs @@ -419,8 +419,8 @@ where // 1. The required seed bytes for Ed25519 and Secp256k1 are both the same length of // 32 bytes. // 2. While Secp256k1 does not allow the most trivial seed bytes of 0x00..0001, a - // a hash function output seems to satisfy it, and our current hashing scheme - // also output 32 bytes. + // hash function output seems to satisfy it, and our current hashing scheme also + // output 32 bytes. let seed_bytes = Digest::hash(seed.to_be_bytes()).value(); match variant { diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 0c04a156af..d7d1b98265 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -227,7 +227,7 @@ provision_contract_user_group_uref = { cost = 200, arguments = [0, 0, 0, 0, 0] } put_key = { cost = 38_000, arguments = [0, 1_100, 0, 0] } read_host_buffer = { cost = 3_500, arguments = [0, 310, 0] } read_value = { cost = 6_000, arguments = [0, 0, 0] } -read_value_local = { cost = 5_500, arguments = [0, 590, 0] } +dictionary_get = { cost = 5_500, arguments = [0, 590, 0] } remove_associated_key = { cost = 4_200, arguments = [0, 0] } remove_contract_user_group = { cost = 200, arguments = [0, 0, 0, 0] } remove_contract_user_group_urefs = { cost = 200, arguments = [0, 0, 0, 0, 0, 0] } @@ -240,7 +240,7 @@ transfer_from_purse_to_purse = { cost = 82_000, arguments = [0, 0, 0, 0, 0, 0, 0 transfer_to_account = { cost = 2_500_000_000, arguments = [0, 0, 0, 0, 0, 0, 0] } update_associated_key = { cost = 4_200, arguments = [0, 0, 0] } write = { cost = 14_000, arguments = [0, 0, 0, 980] } -write_local = { cost = 9_500, arguments = [0, 1_800, 0, 520] } +dictionary_put = { cost = 9_500, arguments = [0, 1_800, 0, 520] } [system_costs] wasmless_transfer_cost = 100_000_000 diff --git a/resources/local/config.toml b/resources/local/config.toml index 3bbba3de07..c7630c77b1 100644 --- a/resources/local/config.toml +++ b/resources/local/config.toml @@ -178,6 +178,15 @@ bind_address = '0.0.0.0:34553' # one connection. known_addresses = ['127.0.0.1:34553'] +# TLS keylog location +# +# If set, the node will write all keys generated during all TLS connections to the given file path. +# This option is intended for debugging only, do NOT enable this on production systems. +# +# The specified location will be appended to, even across node restarts, so it may grow large if +# unattended. +# keylog_path = "/path/to/keylog" + # Minimum number of fully-connected peers to consider network component initialized. min_peers_for_initialization = 3 @@ -246,44 +255,6 @@ blocklist_retain_duration = '1min' # secret_key = "local_node.pem" # ca_certificate = "ca_cert.pem" -# Weights for impact estimation of incoming messages, used in combination with -# `max_incoming_message_rate_non_validators`. -# -# Any weight set to 0 means that the category of traffic is exempt from throttling. -[network.estimator_weights] -consensus = 0 -block_gossip = 1 -deploy_gossip = 0 -finality_signature_gossip = 1 -address_gossip = 0 -finality_signature_broadcasts = 0 -deploy_requests = 1 -deploy_responses = 0 -legacy_deploy_requests = 1 -legacy_deploy_responses = 0 -block_requests = 1 -block_responses = 0 -block_header_requests = 1 -block_header_responses = 0 -trie_requests = 1 -trie_responses = 0 -finality_signature_requests = 1 -finality_signature_responses = 0 -sync_leap_requests = 1 -sync_leap_responses = 0 -approvals_hashes_requests = 1 -approvals_hashes_responses = 0 -execution_results_requests = 1 -execution_results_responses = 0 - -# Identity of a node -# -# When this section is not specified, an identity will be generated when the node process starts with a self-signed certifcate. -# This option makes sense for some private chains where for security reasons joining new nodes is restricted. -# [network.identity] -# tls_certificate = "local_node_cert.pem" -# secret_key = "local_node.pem" -# ca_certificate = "ca_cert.pem" # ================================================== # Configuration options for the JSON-RPC HTTP server diff --git a/resources/metrics-1.5.txt b/resources/metrics-1.5.txt new file mode 100644 index 0000000000..7c7525443f --- /dev/null +++ b/resources/metrics-1.5.txt @@ -0,0 +1,808 @@ +# HELP accumulated_incoming_limiter_delay seconds spent delaying incoming traffic from non-validators due to limiter, in seconds. +# TYPE accumulated_incoming_limiter_delay counter +accumulated_incoming_limiter_delay 0 +# HELP accumulated_outgoing_limiter_delay seconds spent delaying outgoing traffic to non-validators due to limiter, in seconds +# TYPE accumulated_outgoing_limiter_delay counter +accumulated_outgoing_limiter_delay 0 +# HELP address_gossiper_items_received number of items received by the address_gossiper +# TYPE address_gossiper_items_received counter +address_gossiper_items_received 3 +# HELP address_gossiper_table_items_current number of items in the gossip table of address_gossiper in state current +# TYPE address_gossiper_table_items_current gauge +address_gossiper_table_items_current 0 +# HELP address_gossiper_table_items_finished number of items in the gossip table of address_gossiper in state finished +# TYPE address_gossiper_table_items_finished gauge +address_gossiper_table_items_finished 1 +# HELP address_gossiper_times_gossiped number of times the address_gossiper sent gossip requests to peers +# TYPE address_gossiper_times_gossiped counter +address_gossiper_times_gossiped 0 +# HELP address_gossiper_times_ran_out_of_peers number of times the address_gossiper ran out of peers and had to pause +# TYPE address_gossiper_times_ran_out_of_peers counter +address_gossiper_times_ran_out_of_peers 3 +# HELP allocated_ram_bytes total allocated ram in bytes +# TYPE allocated_ram_bytes gauge +allocated_ram_bytes 0 +# HELP amount_of_blocks the number of blocks finalized so far +# TYPE amount_of_blocks gauge +amount_of_blocks 0 +# HELP approvals_hashes_fetch_total number of approvals_hashes all fetch requests made +# TYPE approvals_hashes_fetch_total counter +approvals_hashes_fetch_total 0 +# HELP approvals_hashes_found_in_storage number of fetch requests that found approvals_hashes in local storage +# TYPE approvals_hashes_found_in_storage counter +approvals_hashes_found_in_storage 0 +# HELP approvals_hashes_found_on_peer number of fetch requests that fetched approvals_hashes from peer +# TYPE approvals_hashes_found_on_peer counter +approvals_hashes_found_on_peer 0 +# HELP approvals_hashes_timeouts number of approvals_hashes fetch requests that timed out +# TYPE approvals_hashes_timeouts counter +approvals_hashes_timeouts 0 +# HELP block_accumulator_block_acceptors number of block acceptors in the Block Accumulator +# TYPE block_accumulator_block_acceptors gauge +block_accumulator_block_acceptors 0 +# HELP block_accumulator_known_child_blocks number of blocks received by the Block Accumulator for which we know the hash of the child block +# TYPE block_accumulator_known_child_blocks gauge +block_accumulator_known_child_blocks 0 +# HELP block_execution_results_or_chunk_fetcher_fetch_total number of block_execution_results_or_chunk_fetcher all fetch requests made +# TYPE block_execution_results_or_chunk_fetcher_fetch_total counter +block_execution_results_or_chunk_fetcher_fetch_total 0 +# HELP block_execution_results_or_chunk_fetcher_found_in_storage number of fetch requests that found block_execution_results_or_chunk_fetcher in local storage +# TYPE block_execution_results_or_chunk_fetcher_found_in_storage counter +block_execution_results_or_chunk_fetcher_found_in_storage 0 +# HELP block_execution_results_or_chunk_fetcher_found_on_peer number of fetch requests that fetched block_execution_results_or_chunk_fetcher from peer +# TYPE block_execution_results_or_chunk_fetcher_found_on_peer counter +block_execution_results_or_chunk_fetcher_found_on_peer 0 +# HELP block_execution_results_or_chunk_fetcher_timeouts number of block_execution_results_or_chunk_fetcher fetch requests that timed out +# TYPE block_execution_results_or_chunk_fetcher_timeouts counter +block_execution_results_or_chunk_fetcher_timeouts 0 +# HELP block_fetch_total number of block all fetch requests made +# TYPE block_fetch_total counter +block_fetch_total 0 +# HELP block_found_in_storage number of fetch requests that found block in local storage +# TYPE block_found_in_storage counter +block_found_in_storage 0 +# HELP block_found_on_peer number of fetch requests that fetched block from peer +# TYPE block_found_on_peer counter +block_found_on_peer 0 +# HELP block_gossiper_items_received number of items received by the block_gossiper +# TYPE block_gossiper_items_received counter +block_gossiper_items_received 0 +# HELP block_gossiper_table_items_current number of items in the gossip table of block_gossiper in state current +# TYPE block_gossiper_table_items_current gauge +block_gossiper_table_items_current 0 +# HELP block_gossiper_table_items_finished number of items in the gossip table of block_gossiper in state finished +# TYPE block_gossiper_table_items_finished gauge +block_gossiper_table_items_finished 0 +# HELP block_gossiper_times_gossiped number of times the block_gossiper sent gossip requests to peers +# TYPE block_gossiper_times_gossiped counter +block_gossiper_times_gossiped 0 +# HELP block_gossiper_times_ran_out_of_peers number of times the block_gossiper ran out of peers and had to pause +# TYPE block_gossiper_times_ran_out_of_peers counter +block_gossiper_times_ran_out_of_peers 0 +# HELP block_header_fetch_total number of block_header all fetch requests made +# TYPE block_header_fetch_total counter +block_header_fetch_total 0 +# HELP block_header_found_in_storage number of fetch requests that found block_header in local storage +# TYPE block_header_found_in_storage counter +block_header_found_in_storage 0 +# HELP block_header_found_on_peer number of fetch requests that fetched block_header from peer +# TYPE block_header_found_on_peer counter +block_header_found_on_peer 0 +# HELP block_header_timeouts number of block_header fetch requests that timed out +# TYPE block_header_timeouts counter +block_header_timeouts 0 +# HELP block_timeouts number of block fetch requests that timed out +# TYPE block_timeouts counter +block_timeouts 0 +# HELP chain_height highest complete block (DEPRECATED) +# TYPE chain_height gauge +chain_height 0 +# HELP consensus_current_era the current era in consensus +# TYPE consensus_current_era gauge +consensus_current_era 0 +# HELP consumed_ram_bytes total consumed ram in bytes +# TYPE consumed_ram_bytes gauge +consumed_ram_bytes 0 +# HELP contract_runtime_apply_commit time in seconds to commit the execution effects of a contract +# TYPE contract_runtime_apply_commit histogram +contract_runtime_apply_commit_bucket{le="0.01"} 0 +contract_runtime_apply_commit_bucket{le="0.02"} 0 +contract_runtime_apply_commit_bucket{le="0.04"} 0 +contract_runtime_apply_commit_bucket{le="0.08"} 0 +contract_runtime_apply_commit_bucket{le="0.16"} 0 +contract_runtime_apply_commit_bucket{le="0.32"} 0 +contract_runtime_apply_commit_bucket{le="0.64"} 0 +contract_runtime_apply_commit_bucket{le="1.28"} 0 +contract_runtime_apply_commit_bucket{le="2.56"} 0 +contract_runtime_apply_commit_bucket{le="5.12"} 0 +contract_runtime_apply_commit_bucket{le="+Inf"} 0 +contract_runtime_apply_commit_sum 0 +contract_runtime_apply_commit_count 0 +# HELP contract_runtime_commit_step time in seconds to commit the step at era end +# TYPE contract_runtime_commit_step histogram +contract_runtime_commit_step_bucket{le="0.01"} 0 +contract_runtime_commit_step_bucket{le="0.02"} 0 +contract_runtime_commit_step_bucket{le="0.04"} 0 +contract_runtime_commit_step_bucket{le="0.08"} 0 +contract_runtime_commit_step_bucket{le="0.16"} 0 +contract_runtime_commit_step_bucket{le="0.32"} 0 +contract_runtime_commit_step_bucket{le="0.64"} 0 +contract_runtime_commit_step_bucket{le="1.28"} 0 +contract_runtime_commit_step_bucket{le="2.56"} 0 +contract_runtime_commit_step_bucket{le="5.12"} 0 +contract_runtime_commit_step_bucket{le="+Inf"} 0 +contract_runtime_commit_step_sum 0 +contract_runtime_commit_step_count 0 +# HELP contract_runtime_commit_upgrade time in seconds to commit an upgrade +# TYPE contract_runtime_commit_upgrade histogram +contract_runtime_commit_upgrade_bucket{le="0.01"} 0 +contract_runtime_commit_upgrade_bucket{le="0.02"} 0 +contract_runtime_commit_upgrade_bucket{le="0.04"} 0 +contract_runtime_commit_upgrade_bucket{le="0.08"} 0 +contract_runtime_commit_upgrade_bucket{le="0.16"} 0 +contract_runtime_commit_upgrade_bucket{le="0.32"} 0 +contract_runtime_commit_upgrade_bucket{le="0.64"} 0 +contract_runtime_commit_upgrade_bucket{le="1.28"} 0 +contract_runtime_commit_upgrade_bucket{le="2.56"} 0 +contract_runtime_commit_upgrade_bucket{le="5.12"} 0 +contract_runtime_commit_upgrade_bucket{le="+Inf"} 0 +contract_runtime_commit_upgrade_sum 0 +contract_runtime_commit_upgrade_count 0 +# HELP contract_runtime_execute_block time in seconds to execute all deploys in a block +# TYPE contract_runtime_execute_block histogram +contract_runtime_execute_block_bucket{le="0.01"} 0 +contract_runtime_execute_block_bucket{le="0.02"} 0 +contract_runtime_execute_block_bucket{le="0.04"} 0 +contract_runtime_execute_block_bucket{le="0.08"} 0 +contract_runtime_execute_block_bucket{le="0.16"} 0 +contract_runtime_execute_block_bucket{le="0.32"} 0 +contract_runtime_execute_block_bucket{le="0.64"} 0 +contract_runtime_execute_block_bucket{le="1.28"} 0 +contract_runtime_execute_block_bucket{le="2.56"} 0 +contract_runtime_execute_block_bucket{le="5.12"} 0 +contract_runtime_execute_block_bucket{le="+Inf"} 0 +contract_runtime_execute_block_sum 0 +contract_runtime_execute_block_count 0 +# HELP contract_runtime_get_balance time in seconds to get the balance of a purse from global state +# TYPE contract_runtime_get_balance histogram +contract_runtime_get_balance_bucket{le="0.01"} 0 +contract_runtime_get_balance_bucket{le="0.02"} 0 +contract_runtime_get_balance_bucket{le="0.04"} 0 +contract_runtime_get_balance_bucket{le="0.08"} 0 +contract_runtime_get_balance_bucket{le="0.16"} 0 +contract_runtime_get_balance_bucket{le="0.32"} 0 +contract_runtime_get_balance_bucket{le="0.64"} 0 +contract_runtime_get_balance_bucket{le="1.28"} 0 +contract_runtime_get_balance_bucket{le="2.56"} 0 +contract_runtime_get_balance_bucket{le="5.12"} 0 +contract_runtime_get_balance_bucket{le="+Inf"} 0 +contract_runtime_get_balance_sum 0 +contract_runtime_get_balance_count 0 +# HELP contract_runtime_get_bids time in seconds to get bids from global state +# TYPE contract_runtime_get_bids histogram +contract_runtime_get_bids_bucket{le="0.01"} 0 +contract_runtime_get_bids_bucket{le="0.02"} 0 +contract_runtime_get_bids_bucket{le="0.04"} 0 +contract_runtime_get_bids_bucket{le="0.08"} 0 +contract_runtime_get_bids_bucket{le="0.16"} 0 +contract_runtime_get_bids_bucket{le="0.32"} 0 +contract_runtime_get_bids_bucket{le="0.64"} 0 +contract_runtime_get_bids_bucket{le="1.28"} 0 +contract_runtime_get_bids_bucket{le="2.56"} 0 +contract_runtime_get_bids_bucket{le="5.12"} 0 +contract_runtime_get_bids_bucket{le="+Inf"} 0 +contract_runtime_get_bids_sum 0 +contract_runtime_get_bids_count 0 +# HELP contract_runtime_get_era_validators time in seconds to get validators for a given era from global state +# TYPE contract_runtime_get_era_validators histogram +contract_runtime_get_era_validators_bucket{le="0.01"} 0 +contract_runtime_get_era_validators_bucket{le="0.02"} 0 +contract_runtime_get_era_validators_bucket{le="0.04"} 0 +contract_runtime_get_era_validators_bucket{le="0.08"} 0 +contract_runtime_get_era_validators_bucket{le="0.16"} 0 +contract_runtime_get_era_validators_bucket{le="0.32"} 0 +contract_runtime_get_era_validators_bucket{le="0.64"} 0 +contract_runtime_get_era_validators_bucket{le="1.28"} 0 +contract_runtime_get_era_validators_bucket{le="2.56"} 0 +contract_runtime_get_era_validators_bucket{le="5.12"} 0 +contract_runtime_get_era_validators_bucket{le="+Inf"} 0 +contract_runtime_get_era_validators_sum 0 +contract_runtime_get_era_validators_count 0 +# HELP contract_runtime_get_trie time in seconds to get a trie +# TYPE contract_runtime_get_trie histogram +contract_runtime_get_trie_bucket{le="0.001"} 0 +contract_runtime_get_trie_bucket{le="0.002"} 0 +contract_runtime_get_trie_bucket{le="0.004"} 0 +contract_runtime_get_trie_bucket{le="0.008"} 0 +contract_runtime_get_trie_bucket{le="0.016"} 0 +contract_runtime_get_trie_bucket{le="0.032"} 0 +contract_runtime_get_trie_bucket{le="0.064"} 0 +contract_runtime_get_trie_bucket{le="0.128"} 0 +contract_runtime_get_trie_bucket{le="0.256"} 0 +contract_runtime_get_trie_bucket{le="0.512"} 0 +contract_runtime_get_trie_bucket{le="+Inf"} 0 +contract_runtime_get_trie_sum 0 +contract_runtime_get_trie_count 0 +# HELP contract_runtime_latest_commit_step duration in seconds of latest commit step at era end +# TYPE contract_runtime_latest_commit_step gauge +contract_runtime_latest_commit_step 0 +# HELP contract_runtime_put_trie time in seconds to put a trie +# TYPE contract_runtime_put_trie histogram +contract_runtime_put_trie_bucket{le="0.001"} 0 +contract_runtime_put_trie_bucket{le="0.002"} 0 +contract_runtime_put_trie_bucket{le="0.004"} 0 +contract_runtime_put_trie_bucket{le="0.008"} 0 +contract_runtime_put_trie_bucket{le="0.016"} 0 +contract_runtime_put_trie_bucket{le="0.032"} 0 +contract_runtime_put_trie_bucket{le="0.064"} 0 +contract_runtime_put_trie_bucket{le="0.128"} 0 +contract_runtime_put_trie_bucket{le="0.256"} 0 +contract_runtime_put_trie_bucket{le="0.512"} 0 +contract_runtime_put_trie_bucket{le="+Inf"} 0 +contract_runtime_put_trie_sum 0 +contract_runtime_put_trie_count 0 +# HELP contract_runtime_run_execute time in seconds to execute but not commit a contract +# TYPE contract_runtime_run_execute histogram +contract_runtime_run_execute_bucket{le="0.01"} 0 +contract_runtime_run_execute_bucket{le="0.02"} 0 +contract_runtime_run_execute_bucket{le="0.04"} 0 +contract_runtime_run_execute_bucket{le="0.08"} 0 +contract_runtime_run_execute_bucket{le="0.16"} 0 +contract_runtime_run_execute_bucket{le="0.32"} 0 +contract_runtime_run_execute_bucket{le="0.64"} 0 +contract_runtime_run_execute_bucket{le="1.28"} 0 +contract_runtime_run_execute_bucket{le="2.56"} 0 +contract_runtime_run_execute_bucket{le="5.12"} 0 +contract_runtime_run_execute_bucket{le="+Inf"} 0 +contract_runtime_run_execute_sum 0 +contract_runtime_run_execute_count 0 +# HELP contract_runtime_run_query time in seconds to run a query in global state +# TYPE contract_runtime_run_query histogram +contract_runtime_run_query_bucket{le="0.01"} 0 +contract_runtime_run_query_bucket{le="0.02"} 0 +contract_runtime_run_query_bucket{le="0.04"} 0 +contract_runtime_run_query_bucket{le="0.08"} 0 +contract_runtime_run_query_bucket{le="0.16"} 0 +contract_runtime_run_query_bucket{le="0.32"} 0 +contract_runtime_run_query_bucket{le="0.64"} 0 +contract_runtime_run_query_bucket{le="1.28"} 0 +contract_runtime_run_query_bucket{le="2.56"} 0 +contract_runtime_run_query_bucket{le="5.12"} 0 +contract_runtime_run_query_bucket{le="+Inf"} 0 +contract_runtime_run_query_sum 0 +contract_runtime_run_query_count 0 +# HELP deploy_acceptor_accepted_deploy time in seconds to accept a deploy in the deploy acceptor +# TYPE deploy_acceptor_accepted_deploy histogram +deploy_acceptor_accepted_deploy_bucket{le="10"} 0 +deploy_acceptor_accepted_deploy_bucket{le="20"} 0 +deploy_acceptor_accepted_deploy_bucket{le="40"} 0 +deploy_acceptor_accepted_deploy_bucket{le="80"} 0 +deploy_acceptor_accepted_deploy_bucket{le="160"} 0 +deploy_acceptor_accepted_deploy_bucket{le="320"} 0 +deploy_acceptor_accepted_deploy_bucket{le="640"} 0 +deploy_acceptor_accepted_deploy_bucket{le="1280"} 0 +deploy_acceptor_accepted_deploy_bucket{le="2560"} 0 +deploy_acceptor_accepted_deploy_bucket{le="5120"} 0 +deploy_acceptor_accepted_deploy_bucket{le="+Inf"} 0 +deploy_acceptor_accepted_deploy_sum 0 +deploy_acceptor_accepted_deploy_count 0 +# HELP deploy_acceptor_rejected_deploy time in seconds to reject a deploy in the deploy acceptor +# TYPE deploy_acceptor_rejected_deploy histogram +deploy_acceptor_rejected_deploy_bucket{le="10"} 0 +deploy_acceptor_rejected_deploy_bucket{le="20"} 0 +deploy_acceptor_rejected_deploy_bucket{le="40"} 0 +deploy_acceptor_rejected_deploy_bucket{le="80"} 0 +deploy_acceptor_rejected_deploy_bucket{le="160"} 0 +deploy_acceptor_rejected_deploy_bucket{le="320"} 0 +deploy_acceptor_rejected_deploy_bucket{le="640"} 0 +deploy_acceptor_rejected_deploy_bucket{le="1280"} 0 +deploy_acceptor_rejected_deploy_bucket{le="2560"} 0 +deploy_acceptor_rejected_deploy_bucket{le="5120"} 0 +deploy_acceptor_rejected_deploy_bucket{le="+Inf"} 0 +deploy_acceptor_rejected_deploy_sum 0 +deploy_acceptor_rejected_deploy_count 0 +# HELP deploy_buffer_dead_deploys number of deploys that should not be included in future proposals. +# TYPE deploy_buffer_dead_deploys gauge +deploy_buffer_dead_deploys 0 +# HELP deploy_buffer_held_deploys number of deploys included in in-flight proposed blocks. +# TYPE deploy_buffer_held_deploys gauge +deploy_buffer_held_deploys 0 +# HELP deploy_buffer_total_deploys total number of deploys contained in the deploy buffer. +# TYPE deploy_buffer_total_deploys gauge +deploy_buffer_total_deploys 0 +# HELP deploy_fetch_total number of deploy all fetch requests made +# TYPE deploy_fetch_total counter +deploy_fetch_total 0 +# HELP deploy_found_in_storage number of fetch requests that found deploy in local storage +# TYPE deploy_found_in_storage counter +deploy_found_in_storage 0 +# HELP deploy_found_on_peer number of fetch requests that fetched deploy from peer +# TYPE deploy_found_on_peer counter +deploy_found_on_peer 0 +# HELP deploy_gossiper_items_received number of items received by the deploy_gossiper +# TYPE deploy_gossiper_items_received counter +deploy_gossiper_items_received 0 +# HELP deploy_gossiper_table_items_current number of items in the gossip table of deploy_gossiper in state current +# TYPE deploy_gossiper_table_items_current gauge +deploy_gossiper_table_items_current 0 +# HELP deploy_gossiper_table_items_finished number of items in the gossip table of deploy_gossiper in state finished +# TYPE deploy_gossiper_table_items_finished gauge +deploy_gossiper_table_items_finished 0 +# HELP deploy_gossiper_times_gossiped number of times the deploy_gossiper sent gossip requests to peers +# TYPE deploy_gossiper_times_gossiped counter +deploy_gossiper_times_gossiped 0 +# HELP deploy_gossiper_times_ran_out_of_peers number of times the deploy_gossiper ran out of peers and had to pause +# TYPE deploy_gossiper_times_ran_out_of_peers counter +deploy_gossiper_times_ran_out_of_peers 0 +# HELP deploy_timeouts number of deploy fetch requests that timed out +# TYPE deploy_timeouts counter +deploy_timeouts 0 +# HELP event_dispatch_duration time in nanoseconds to dispatch an event +# TYPE event_dispatch_duration histogram +event_dispatch_duration_bucket{le="100"} 0 +event_dispatch_duration_bucket{le="500"} 0 +event_dispatch_duration_bucket{le="1000"} 0 +event_dispatch_duration_bucket{le="5000"} 4 +event_dispatch_duration_bucket{le="10000"} 4 +event_dispatch_duration_bucket{le="20000"} 4 +event_dispatch_duration_bucket{le="50000"} 9 +event_dispatch_duration_bucket{le="100000"} 20 +event_dispatch_duration_bucket{le="200000"} 45 +event_dispatch_duration_bucket{le="300000"} 78 +event_dispatch_duration_bucket{le="400000"} 126 +event_dispatch_duration_bucket{le="500000"} 200 +event_dispatch_duration_bucket{le="600000"} 247 +event_dispatch_duration_bucket{le="700000"} 271 +event_dispatch_duration_bucket{le="800000"} 274 +event_dispatch_duration_bucket{le="900000"} 276 +event_dispatch_duration_bucket{le="1000000"} 281 +event_dispatch_duration_bucket{le="2000000"} 305 +event_dispatch_duration_bucket{le="5000000"} 315 +event_dispatch_duration_bucket{le="+Inf"} 316 +event_dispatch_duration_sum 183686355 +event_dispatch_duration_count 316 +# HELP execution_queue_size number of blocks that are currently enqueued and waiting for execution +# TYPE execution_queue_size gauge +execution_queue_size 0 +# HELP finality_signature_fetcher_fetch_total number of finality_signature_fetcher all fetch requests made +# TYPE finality_signature_fetcher_fetch_total counter +finality_signature_fetcher_fetch_total 0 +# HELP finality_signature_fetcher_found_in_storage number of fetch requests that found finality_signature_fetcher in local storage +# TYPE finality_signature_fetcher_found_in_storage counter +finality_signature_fetcher_found_in_storage 0 +# HELP finality_signature_fetcher_found_on_peer number of fetch requests that fetched finality_signature_fetcher from peer +# TYPE finality_signature_fetcher_found_on_peer counter +finality_signature_fetcher_found_on_peer 0 +# HELP finality_signature_fetcher_timeouts number of finality_signature_fetcher fetch requests that timed out +# TYPE finality_signature_fetcher_timeouts counter +finality_signature_fetcher_timeouts 0 +# HELP finality_signature_gossiper_items_received number of items received by the finality_signature_gossiper +# TYPE finality_signature_gossiper_items_received counter +finality_signature_gossiper_items_received 0 +# HELP finality_signature_gossiper_table_items_current number of items in the gossip table of finality_signature_gossiper in state current +# TYPE finality_signature_gossiper_table_items_current gauge +finality_signature_gossiper_table_items_current 0 +# HELP finality_signature_gossiper_table_items_finished number of items in the gossip table of finality_signature_gossiper in state finished +# TYPE finality_signature_gossiper_table_items_finished gauge +finality_signature_gossiper_table_items_finished 0 +# HELP finality_signature_gossiper_times_gossiped number of times the finality_signature_gossiper sent gossip requests to peers +# TYPE finality_signature_gossiper_times_gossiped counter +finality_signature_gossiper_times_gossiped 0 +# HELP finality_signature_gossiper_times_ran_out_of_peers number of times the finality_signature_gossiper ran out of peers and had to pause +# TYPE finality_signature_gossiper_times_ran_out_of_peers counter +finality_signature_gossiper_times_ran_out_of_peers 0 +# HELP finalization_time the amount of time, in milliseconds, between proposal and finalization of the latest finalized block +# TYPE finalization_time gauge +finalization_time 0 +# HELP forward_block_sync_duration_seconds duration (in sec) to synchronize a forward block +# TYPE forward_block_sync_duration_seconds histogram +forward_block_sync_duration_seconds_bucket{le="0.05"} 0 +forward_block_sync_duration_seconds_bucket{le="0.08750000000000001"} 0 +forward_block_sync_duration_seconds_bucket{le="0.153125"} 0 +forward_block_sync_duration_seconds_bucket{le="0.26796875000000003"} 0 +forward_block_sync_duration_seconds_bucket{le="0.46894531250000004"} 0 +forward_block_sync_duration_seconds_bucket{le="0.8206542968750001"} 0 +forward_block_sync_duration_seconds_bucket{le="1.4361450195312502"} 0 +forward_block_sync_duration_seconds_bucket{le="2.513253784179688"} 0 +forward_block_sync_duration_seconds_bucket{le="4.398194122314454"} 0 +forward_block_sync_duration_seconds_bucket{le="7.696839714050294"} 0 +forward_block_sync_duration_seconds_bucket{le="+Inf"} 0 +forward_block_sync_duration_seconds_sum 0 +forward_block_sync_duration_seconds_count 0 +# HELP highest_available_block_height highest height of the available block range (the highest contiguous chain of complete blocks) +# TYPE highest_available_block_height gauge +highest_available_block_height 0 +# HELP historical_block_sync_duration_seconds duration (in sec) to synchronize a historical block +# TYPE historical_block_sync_duration_seconds histogram +historical_block_sync_duration_seconds_bucket{le="0.05"} 0 +historical_block_sync_duration_seconds_bucket{le="0.08750000000000001"} 0 +historical_block_sync_duration_seconds_bucket{le="0.153125"} 0 +historical_block_sync_duration_seconds_bucket{le="0.26796875000000003"} 0 +historical_block_sync_duration_seconds_bucket{le="0.46894531250000004"} 0 +historical_block_sync_duration_seconds_bucket{le="0.8206542968750001"} 0 +historical_block_sync_duration_seconds_bucket{le="1.4361450195312502"} 0 +historical_block_sync_duration_seconds_bucket{le="2.513253784179688"} 0 +historical_block_sync_duration_seconds_bucket{le="4.398194122314454"} 0 +historical_block_sync_duration_seconds_bucket{le="7.696839714050294"} 0 +historical_block_sync_duration_seconds_bucket{le="+Inf"} 0 +historical_block_sync_duration_seconds_sum 0 +historical_block_sync_duration_seconds_count 0 +# HELP legacy_deploy_fetch_total number of legacy_deploy all fetch requests made +# TYPE legacy_deploy_fetch_total counter +legacy_deploy_fetch_total 0 +# HELP legacy_deploy_found_in_storage number of fetch requests that found legacy_deploy in local storage +# TYPE legacy_deploy_found_in_storage counter +legacy_deploy_found_in_storage 0 +# HELP legacy_deploy_found_on_peer number of fetch requests that fetched legacy_deploy from peer +# TYPE legacy_deploy_found_on_peer counter +legacy_deploy_found_on_peer 0 +# HELP legacy_deploy_timeouts number of legacy_deploy fetch requests that timed out +# TYPE legacy_deploy_timeouts counter +legacy_deploy_timeouts 0 +# HELP lowest_available_block_height lowest height of the available block range (the highest contiguous chain of complete blocks) +# TYPE lowest_available_block_height gauge +lowest_available_block_height 0 +# HELP mem_address_gossiper address_gossiper memory usage in bytes +# TYPE mem_address_gossiper gauge +mem_address_gossiper 0 +# HELP mem_block_accumulator block accumulator memory usage in bytes +# TYPE mem_block_accumulator gauge +mem_block_accumulator 0 +# HELP mem_block_gossiper block gossiper memory usage in bytes +# TYPE mem_block_gossiper gauge +mem_block_gossiper 0 +# HELP mem_block_synchronizer block synchronizer memory usage in bytes +# TYPE mem_block_synchronizer gauge +mem_block_synchronizer 0 +# HELP mem_block_validator block validator memory usage in bytes +# TYPE mem_block_validator gauge +mem_block_validator 0 +# HELP mem_consensus consensus memory usage in bytes +# TYPE mem_consensus gauge +mem_consensus 0 +# HELP mem_contract_runtime contract runtime memory usage in bytes +# TYPE mem_contract_runtime gauge +mem_contract_runtime 0 +# HELP mem_deploy_acceptor deploy acceptor memory usage in bytes +# TYPE mem_deploy_acceptor gauge +mem_deploy_acceptor 0 +# HELP mem_deploy_buffer deploy buffer memory usage in bytes +# TYPE mem_deploy_buffer gauge +mem_deploy_buffer 0 +# HELP mem_deploy_gossiper deploy gossiper memory usage in bytes +# TYPE mem_deploy_gossiper gauge +mem_deploy_gossiper 0 +# HELP mem_diagnostics_port diagnostics port memory usage in bytes +# TYPE mem_diagnostics_port gauge +mem_diagnostics_port 0 +# HELP mem_estimator_runtime_s time in seconds to estimate memory usage +# TYPE mem_estimator_runtime_s histogram +mem_estimator_runtime_s_bucket{le="0.000000004"} 0 +mem_estimator_runtime_s_bucket{le="0.000000008"} 0 +mem_estimator_runtime_s_bucket{le="0.000000016"} 0 +mem_estimator_runtime_s_bucket{le="0.000000032"} 0 +mem_estimator_runtime_s_bucket{le="0.000000064"} 0 +mem_estimator_runtime_s_bucket{le="0.000000128"} 0 +mem_estimator_runtime_s_bucket{le="0.000000256"} 0 +mem_estimator_runtime_s_bucket{le="0.000000512"} 0 +mem_estimator_runtime_s_bucket{le="0.000001024"} 0 +mem_estimator_runtime_s_bucket{le="0.000002048"} 0 +mem_estimator_runtime_s_bucket{le="0.000004096"} 0 +mem_estimator_runtime_s_bucket{le="0.000008192"} 0 +mem_estimator_runtime_s_bucket{le="0.000016384"} 0 +mem_estimator_runtime_s_bucket{le="0.000032768"} 0 +mem_estimator_runtime_s_bucket{le="0.000065536"} 0 +mem_estimator_runtime_s_bucket{le="0.000131072"} 0 +mem_estimator_runtime_s_bucket{le="0.000262144"} 0 +mem_estimator_runtime_s_bucket{le="0.000524288"} 0 +mem_estimator_runtime_s_bucket{le="0.001048576"} 0 +mem_estimator_runtime_s_bucket{le="0.002097152"} 0 +mem_estimator_runtime_s_bucket{le="0.004194304"} 0 +mem_estimator_runtime_s_bucket{le="0.008388608"} 0 +mem_estimator_runtime_s_bucket{le="0.016777216"} 0 +mem_estimator_runtime_s_bucket{le="0.033554432"} 0 +mem_estimator_runtime_s_bucket{le="0.067108864"} 0 +mem_estimator_runtime_s_bucket{le="0.134217728"} 0 +mem_estimator_runtime_s_bucket{le="0.268435456"} 0 +mem_estimator_runtime_s_bucket{le="0.536870912"} 0 +mem_estimator_runtime_s_bucket{le="1.073741824"} 0 +mem_estimator_runtime_s_bucket{le="2.147483648"} 0 +mem_estimator_runtime_s_bucket{le="4.294967296"} 0 +mem_estimator_runtime_s_bucket{le="8.589934592"} 0 +mem_estimator_runtime_s_bucket{le="+Inf"} 0 +mem_estimator_runtime_s_sum 0 +mem_estimator_runtime_s_count 0 +# HELP mem_event_stream_server event stream server memory usage in bytes +# TYPE mem_event_stream_server gauge +mem_event_stream_server 0 +# HELP mem_fetchers combined fetcher memory usage in bytes +# TYPE mem_fetchers gauge +mem_fetchers 0 +# HELP mem_finality_signature_gossiper finality signature gossiper memory usage in bytes +# TYPE mem_finality_signature_gossiper gauge +mem_finality_signature_gossiper 0 +# HELP mem_metrics metrics memory usage in bytes +# TYPE mem_metrics gauge +mem_metrics 0 +# HELP mem_net network memory usage in bytes +# TYPE mem_net gauge +mem_net 0 +# HELP mem_rest_server rest server memory usage in bytes +# TYPE mem_rest_server gauge +mem_rest_server 0 +# HELP mem_rpc_server rpc server memory usage in bytes +# TYPE mem_rpc_server gauge +mem_rpc_server 0 +# HELP mem_storage storage memory usage in bytes +# TYPE mem_storage gauge +mem_storage 0 +# HELP mem_sync_leaper sync leaper memory usage in bytes +# TYPE mem_sync_leaper gauge +mem_sync_leaper 0 +# HELP mem_total total memory usage in bytes +# TYPE mem_total gauge +mem_total 0 +# HELP mem_upgrade_watcher upgrade watcher memory usage in bytes +# TYPE mem_upgrade_watcher gauge +mem_upgrade_watcher 0 +# HELP net_broadcast_requests number of broadcasting requests +# TYPE net_broadcast_requests counter +net_broadcast_requests 0 +# HELP net_direct_message_requests number of requests to send a message directly to a peer +# TYPE net_direct_message_requests counter +net_direct_message_requests 0 +# HELP net_in_bytes_address_gossip volume in bytes of incoming messages with address gossiper payload +# TYPE net_in_bytes_address_gossip counter +net_in_bytes_address_gossip 0 +# HELP net_in_bytes_block_gossip volume in bytes of incoming messages with block gossiper payload +# TYPE net_in_bytes_block_gossip counter +net_in_bytes_block_gossip 0 +# HELP net_in_bytes_block_transfer volume in bytes of incoming messages with block request/response payload +# TYPE net_in_bytes_block_transfer counter +net_in_bytes_block_transfer 0 +# HELP net_in_bytes_consensus volume in bytes of incoming messages with consensus payload +# TYPE net_in_bytes_consensus counter +net_in_bytes_consensus 0 +# HELP net_in_bytes_deploy_gossip volume in bytes of incoming messages with deploy gossiper payload +# TYPE net_in_bytes_deploy_gossip counter +net_in_bytes_deploy_gossip 0 +# HELP net_in_bytes_deploy_transfer volume in bytes of incoming messages with deploy request/response payload +# TYPE net_in_bytes_deploy_transfer counter +net_in_bytes_deploy_transfer 0 +# HELP net_in_bytes_finality_signature_gossip volume in bytes of incoming messages with finality signature gossiper payload +# TYPE net_in_bytes_finality_signature_gossip counter +net_in_bytes_finality_signature_gossip 0 +# HELP net_in_bytes_other volume in bytes of incoming messages with other payload +# TYPE net_in_bytes_other counter +net_in_bytes_other 0 +# HELP net_in_bytes_protocol volume in bytes of incoming messages that are protocol overhead +# TYPE net_in_bytes_protocol counter +net_in_bytes_protocol 0 +# HELP net_in_bytes_trie_transfer volume in bytes of incoming messages with trie payloads +# TYPE net_in_bytes_trie_transfer counter +net_in_bytes_trie_transfer 0 +# HELP net_in_count_address_gossip count of incoming messages with address gossiper payload +# TYPE net_in_count_address_gossip counter +net_in_count_address_gossip 0 +# HELP net_in_count_block_gossip count of incoming messages with block gossiper payload +# TYPE net_in_count_block_gossip counter +net_in_count_block_gossip 0 +# HELP net_in_count_block_transfer count of incoming messages with block request/response payload +# TYPE net_in_count_block_transfer counter +net_in_count_block_transfer 0 +# HELP net_in_count_consensus count of incoming messages with consensus payload +# TYPE net_in_count_consensus counter +net_in_count_consensus 0 +# HELP net_in_count_deploy_gossip count of incoming messages with deploy gossiper payload +# TYPE net_in_count_deploy_gossip counter +net_in_count_deploy_gossip 0 +# HELP net_in_count_deploy_transfer count of incoming messages with deploy request/response payload +# TYPE net_in_count_deploy_transfer counter +net_in_count_deploy_transfer 0 +# HELP net_in_count_finality_signature_gossip count of incoming messages with finality signature gossiper payload +# TYPE net_in_count_finality_signature_gossip counter +net_in_count_finality_signature_gossip 0 +# HELP net_in_count_other count of incoming messages with other payload +# TYPE net_in_count_other counter +net_in_count_other 0 +# HELP net_in_count_protocol count of incoming messages that are protocol overhead +# TYPE net_in_count_protocol counter +net_in_count_protocol 0 +# HELP net_in_count_trie_transfer count of incoming messages with trie payloads +# TYPE net_in_count_trie_transfer counter +net_in_count_trie_transfer 0 +# HELP net_out_bytes_address_gossip volume in bytes of outgoing messages with address gossiper payload +# TYPE net_out_bytes_address_gossip counter +net_out_bytes_address_gossip 0 +# HELP net_out_bytes_block_gossip volume in bytes of outgoing messages with block gossiper payload +# TYPE net_out_bytes_block_gossip counter +net_out_bytes_block_gossip 0 +# HELP net_out_bytes_block_transfer volume in bytes of outgoing messages with block request/response payload +# TYPE net_out_bytes_block_transfer counter +net_out_bytes_block_transfer 0 +# HELP net_out_bytes_consensus volume in bytes of outgoing messages with consensus payload +# TYPE net_out_bytes_consensus counter +net_out_bytes_consensus 0 +# HELP net_out_bytes_deploy_gossip volume in bytes of outgoing messages with deploy gossiper payload +# TYPE net_out_bytes_deploy_gossip counter +net_out_bytes_deploy_gossip 0 +# HELP net_out_bytes_deploy_transfer volume in bytes of outgoing messages with deploy request/response payload +# TYPE net_out_bytes_deploy_transfer counter +net_out_bytes_deploy_transfer 0 +# HELP net_out_bytes_finality_signature_gossip volume in bytes of outgoing messages with finality signature gossiper payload +# TYPE net_out_bytes_finality_signature_gossip counter +net_out_bytes_finality_signature_gossip 0 +# HELP net_out_bytes_other volume in bytes of outgoing messages with other payload +# TYPE net_out_bytes_other counter +net_out_bytes_other 0 +# HELP net_out_bytes_protocol volume in bytes of outgoing messages that are protocol overhead +# TYPE net_out_bytes_protocol counter +net_out_bytes_protocol 0 +# HELP net_out_bytes_trie_transfer volume in bytes of outgoing messages with trie payloads +# TYPE net_out_bytes_trie_transfer counter +net_out_bytes_trie_transfer 0 +# HELP net_out_count_address_gossip count of outgoing messages with address gossiper payload +# TYPE net_out_count_address_gossip counter +net_out_count_address_gossip 0 +# HELP net_out_count_block_gossip count of outgoing messages with block gossiper payload +# TYPE net_out_count_block_gossip counter +net_out_count_block_gossip 0 +# HELP net_out_count_block_transfer count of outgoing messages with block request/response payload +# TYPE net_out_count_block_transfer counter +net_out_count_block_transfer 0 +# HELP net_out_count_consensus count of outgoing messages with consensus payload +# TYPE net_out_count_consensus counter +net_out_count_consensus 0 +# HELP net_out_count_deploy_gossip count of outgoing messages with deploy gossiper payload +# TYPE net_out_count_deploy_gossip counter +net_out_count_deploy_gossip 0 +# HELP net_out_count_deploy_transfer count of outgoing messages with deploy request/response payload +# TYPE net_out_count_deploy_transfer counter +net_out_count_deploy_transfer 0 +# HELP net_out_count_finality_signature_gossip count of outgoing messages with finality signature gossiper payload +# TYPE net_out_count_finality_signature_gossip counter +net_out_count_finality_signature_gossip 0 +# HELP net_out_count_other count of outgoing messages with other payload +# TYPE net_out_count_other counter +net_out_count_other 0 +# HELP net_out_count_protocol count of outgoing messages that are protocol overhead +# TYPE net_out_count_protocol counter +net_out_count_protocol 0 +# HELP net_out_count_trie_transfer count of outgoing messages with trie payloads +# TYPE net_out_count_trie_transfer counter +net_out_count_trie_transfer 0 +# HELP net_queued_direct_messages number of messages waiting to be sent out +# TYPE net_queued_direct_messages gauge +net_queued_direct_messages 0 +# HELP out_state_blocked number of connections in the blocked state +# TYPE out_state_blocked gauge +out_state_blocked 2 +# HELP out_state_connected number of connections in the connected state +# TYPE out_state_connected gauge +out_state_connected 0 +# HELP out_state_connecting number of connections in the connecting state +# TYPE out_state_connecting gauge +out_state_connecting 0 +# HELP out_state_loopback number of connections in the loopback state +# TYPE out_state_loopback gauge +out_state_loopback 1 +# HELP out_state_waiting number of connections in the waiting state +# TYPE out_state_waiting gauge +out_state_waiting 0 +# HELP peers number of connected peers +# TYPE peers gauge +peers 0 +# HELP requests_for_trie_accepted number of trie requests accepted for processing +# TYPE requests_for_trie_accepted counter +requests_for_trie_accepted 0 +# HELP requests_for_trie_finished number of trie requests finished, successful or not +# TYPE requests_for_trie_finished counter +requests_for_trie_finished 0 +# HELP runner_events running total count of events handled by this reactor +# TYPE runner_events counter +runner_events 317 +# HELP scheduler_queue_api_count current number of events in the reactor api queue +# TYPE scheduler_queue_api_count gauge +scheduler_queue_api_count 0 +# HELP scheduler_queue_consensus_count current number of events in the reactor consensus queue +# TYPE scheduler_queue_consensus_count gauge +scheduler_queue_consensus_count 0 +# HELP scheduler_queue_contract_runtime_count current number of events in the reactor contract_runtime queue +# TYPE scheduler_queue_contract_runtime_count gauge +scheduler_queue_contract_runtime_count 0 +# HELP scheduler_queue_control_count current number of events in the reactor control queue +# TYPE scheduler_queue_control_count gauge +scheduler_queue_control_count 0 +# HELP scheduler_queue_fetch_count current number of events in the reactor fetch queue +# TYPE scheduler_queue_fetch_count gauge +scheduler_queue_fetch_count 0 +# HELP scheduler_queue_finality_signature_count current number of events in the reactor finality_signature queue +# TYPE scheduler_queue_finality_signature_count gauge +scheduler_queue_finality_signature_count 0 +# HELP scheduler_queue_from_storage_count current number of events in the reactor from_storage queue +# TYPE scheduler_queue_from_storage_count gauge +scheduler_queue_from_storage_count 0 +# HELP scheduler_queue_gossip_count current number of events in the reactor gossip queue +# TYPE scheduler_queue_gossip_count gauge +scheduler_queue_gossip_count 0 +# HELP scheduler_queue_network_count current number of events in the reactor network queue +# TYPE scheduler_queue_network_count gauge +scheduler_queue_network_count 0 +# HELP scheduler_queue_network_demands_count current number of events in the reactor network_demands queue +# TYPE scheduler_queue_network_demands_count gauge +scheduler_queue_network_demands_count 0 +# HELP scheduler_queue_network_incoming_count current number of events in the reactor network_incoming queue +# TYPE scheduler_queue_network_incoming_count gauge +scheduler_queue_network_incoming_count 0 +# HELP scheduler_queue_network_info_count current number of events in the reactor network_info queue +# TYPE scheduler_queue_network_info_count gauge +scheduler_queue_network_info_count 0 +# HELP scheduler_queue_network_low_priority_count current number of events in the reactor network_low_priority queue +# TYPE scheduler_queue_network_low_priority_count gauge +scheduler_queue_network_low_priority_count 0 +# HELP scheduler_queue_regular_count current number of events in the reactor regular queue +# TYPE scheduler_queue_regular_count gauge +scheduler_queue_regular_count 0 +# HELP scheduler_queue_sync_global_state_count current number of events in the reactor sync_global_state queue +# TYPE scheduler_queue_sync_global_state_count gauge +scheduler_queue_sync_global_state_count 0 +# HELP scheduler_queue_to_storage_count current number of events in the reactor to_storage queue +# TYPE scheduler_queue_to_storage_count gauge +scheduler_queue_to_storage_count 0 +# HELP scheduler_queue_total_count current total number of events in all reactor queues +# TYPE scheduler_queue_total_count gauge +scheduler_queue_total_count 0 +# HELP scheduler_queue_validation_count current number of events in the reactor validation queue +# TYPE scheduler_queue_validation_count gauge +scheduler_queue_validation_count 0 +# HELP sync_leap_cant_fetch_total number of sync leap requests that couldn't be fetched from peers +# TYPE sync_leap_cant_fetch_total counter +sync_leap_cant_fetch_total 0 +# HELP sync_leap_duration_seconds duration (in sec) to perform a successful sync leap +# TYPE sync_leap_duration_seconds histogram +sync_leap_duration_seconds_bucket{le="1"} 0 +sync_leap_duration_seconds_bucket{le="2"} 0 +sync_leap_duration_seconds_bucket{le="3"} 0 +sync_leap_duration_seconds_bucket{le="4"} 0 +sync_leap_duration_seconds_bucket{le="+Inf"} 0 +sync_leap_duration_seconds_sum 0 +sync_leap_duration_seconds_count 0 +# HELP sync_leap_fetched_from_peer_total number of successful sync leap responses that were received from peers +# TYPE sync_leap_fetched_from_peer_total counter +sync_leap_fetched_from_peer_total 0 +# HELP sync_leap_fetcher_fetch_total number of sync_leap_fetcher all fetch requests made +# TYPE sync_leap_fetcher_fetch_total counter +sync_leap_fetcher_fetch_total 0 +# HELP sync_leap_fetcher_found_in_storage number of fetch requests that found sync_leap_fetcher in local storage +# TYPE sync_leap_fetcher_found_in_storage counter +sync_leap_fetcher_found_in_storage 0 +# HELP sync_leap_fetcher_found_on_peer number of fetch requests that fetched sync_leap_fetcher from peer +# TYPE sync_leap_fetcher_found_on_peer counter +sync_leap_fetcher_found_on_peer 0 +# HELP sync_leap_fetcher_timeouts number of sync_leap_fetcher fetch requests that timed out +# TYPE sync_leap_fetcher_timeouts counter +sync_leap_fetcher_timeouts 0 +# HELP sync_leap_rejected_by_peer_total number of sync leap requests that were rejected by peers +# TYPE sync_leap_rejected_by_peer_total counter +sync_leap_rejected_by_peer_total 0 +# HELP time_of_last_block_payload timestamp of the most recently accepted block payload +# TYPE time_of_last_block_payload gauge +time_of_last_block_payload 0 +# HELP time_of_last_finalized_block timestamp of the most recently finalized block +# TYPE time_of_last_finalized_block gauge +time_of_last_finalized_block 0 +# HELP total_ram_bytes total system ram in bytes +# TYPE total_ram_bytes gauge +total_ram_bytes 0 +# HELP trie_or_chunk_fetch_total number of trie_or_chunk all fetch requests made +# TYPE trie_or_chunk_fetch_total counter +trie_or_chunk_fetch_total 0 +# HELP trie_or_chunk_found_in_storage number of fetch requests that found trie_or_chunk in local storage +# TYPE trie_or_chunk_found_in_storage counter +trie_or_chunk_found_in_storage 0 +# HELP trie_or_chunk_found_on_peer number of fetch requests that fetched trie_or_chunk from peer +# TYPE trie_or_chunk_found_on_peer counter +trie_or_chunk_found_on_peer 0 +# HELP trie_or_chunk_timeouts number of trie_or_chunk fetch requests that timed out +# TYPE trie_or_chunk_timeouts counter +trie_or_chunk_timeouts 0 diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index 5827dcffc8..9b8d9999af 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -237,7 +237,7 @@ provision_contract_user_group_uref = { cost = 200, arguments = [0, 0, 0, 0, 0] } put_key = { cost = 38_000, arguments = [0, 1_100, 0, 0] } read_host_buffer = { cost = 3_500, arguments = [0, 310, 0] } read_value = { cost = 6_000, arguments = [0, 0, 0] } -read_value_local = { cost = 5_500, arguments = [0, 590, 0] } +dictionary_get = { cost = 5_500, arguments = [0, 590, 0] } remove_associated_key = { cost = 4_200, arguments = [0, 0] } remove_contract_user_group = { cost = 200, arguments = [0, 0, 0, 0] } remove_contract_user_group_urefs = { cost = 200, arguments = [0, 0, 0, 0, 0, 0] } @@ -250,7 +250,7 @@ transfer_from_purse_to_purse = { cost = 82_000, arguments = [0, 0, 0, 0, 0, 0, 0 transfer_to_account = { cost = 2_500_000_000, arguments = [0, 0, 0, 0, 0, 0, 0] } update_associated_key = { cost = 4_200, arguments = [0, 0, 0] } write = { cost = 14_000, arguments = [0, 0, 0, 980] } -write_local = { cost = 9_500, arguments = [0, 1_800, 0, 520] } +dictionary_put = { cost = 9_500, arguments = [0, 1_800, 0, 520] } enable_contract_version = { cost = 200, arguments = [0, 0, 0, 0] } [system_costs] diff --git a/resources/production/config-example.toml b/resources/production/config-example.toml index ef55205017..596400b5ce 100644 --- a/resources/production/config-example.toml +++ b/resources/production/config-example.toml @@ -178,6 +178,15 @@ bind_address = '0.0.0.0:35000' # one connection. known_addresses = ['168.119.137.143:35000','47.251.14.254:35000','47.242.53.164:35000','46.101.61.107:35000','47.88.87.63:35000','35.152.42.229:35000','206.189.47.102:35000','134.209.243.124:35000','148.251.190.103:35000','167.172.32.44:35000','165.22.252.48:35000','18.219.70.138:35000','3.225.191.9:35000','3.221.194.62:35000','101.36.120.117:35000','54.151.24.120:35000','148.251.135.60:35000','18.188.103.230:35000','54.215.53.35:35000','88.99.95.7:35000','99.81.225.72:35000','52.207.122.179:35000','3.135.134.105:35000','62.171.135.101:35000','139.162.132.144:35000','63.33.251.206:35000','135.181.165.110:35000','135.181.134.57:35000','94.130.107.198:35000','54.180.220.20:35000','188.40.83.254:35000','157.90.131.121:35000','134.209.110.11:35000','168.119.69.6:35000','45.76.251.225:35000','168.119.209.31:35000','31.7.207.16:35000','209.145.60.74:35000','54.252.66.23:35000','134.209.16.172:35000','178.238.235.196:35000','18.217.20.213:35000','3.14.161.135:35000','3.12.207.193:35000','3.12.207.193:35000'] +# TLS keylog location +# +# If set, the node will write all keys generated during all TLS connections to the given file path. +# This option is intended for debugging only, do NOT enable this on production systems. +# +# The specified location will be appended to, even across node restarts, so it may grow large if +# unattended. +# keylog_path = "/path/to/keylog" + # Minimum number of fully-connected peers to consider network component initialized. min_peers_for_initialization = 3 @@ -237,45 +246,6 @@ tarpit_chance = 0.2 # How long peers remain blocked after they get blocklisted. blocklist_retain_duration = '10min' -# Identity of a node -# -# When this section is not specified, an identity will be generated when the node process starts with a self-signed certifcate. -# This option makes sense for some private chains where for security reasons joining new nodes is restricted. -# [network.identity] -# tls_certificate = "node_cert.pem" -# secret_key = "node.pem" -# ca_certificate = "ca_cert.pem" - -# Weights for impact estimation of incoming messages, used in combination with -# `max_incoming_message_rate_non_validators`. -# -# Any weight set to 0 means that the category of traffic is exempt from throttling. -[network.estimator_weights] -consensus = 0 -block_gossip = 1 -deploy_gossip = 0 -finality_signature_gossip = 1 -address_gossip = 0 -finality_signature_broadcasts = 0 -deploy_requests = 1 -deploy_responses = 0 -legacy_deploy_requests = 1 -legacy_deploy_responses = 0 -block_requests = 1 -block_responses = 0 -block_header_requests = 1 -block_header_responses = 0 -trie_requests = 1 -trie_responses = 0 -finality_signature_requests = 1 -finality_signature_responses = 0 -sync_leap_requests = 1 -sync_leap_responses = 0 -approvals_hashes_requests = 1 -approvals_hashes_responses = 0 -execution_results_requests = 1 -execution_results_responses = 0 - # Identity of a node # # When this section is not specified, an identity will be generated when the node process starts with a self-signed certifcate. diff --git a/resources/test/rpc_schema_hashing.json b/resources/test/rpc_schema_hashing.json index d2de416a20..e5d9e1de41 100644 --- a/resources/test/rpc_schema_hashing.json +++ b/resources/test/rpc_schema_hashing.json @@ -2466,7 +2466,8 @@ "Read", "Write", "Add", - "NoOp" + "NoOp", + "Delete" ] }, "TransformEntry": { @@ -2501,7 +2502,8 @@ "Identity", "WriteContractWasm", "WriteContract", - "WriteContractPackage" + "WriteContractPackage", + "Prune" ] }, { diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index 8c77ad830e..f375df2f57 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -1217,7 +1217,8 @@ "Read", "Write", "Add", - "NoOp" + "NoOp", + "Delete" ] }, "TransformEntry": { @@ -1252,7 +1253,8 @@ "Identity", "WriteContractWasm", "WriteContract", - "WriteContractPackage" + "WriteContractPackage", + "Prune" ] }, { @@ -2032,4 +2034,4 @@ } } } -} +} \ No newline at end of file diff --git a/resources/test/valid/0_9_0/chainspec.toml b/resources/test/valid/0_9_0/chainspec.toml index 0eb317cba5..3ae6fad6a9 100644 --- a/resources/test/valid/0_9_0/chainspec.toml +++ b/resources/test/valid/0_9_0/chainspec.toml @@ -125,7 +125,7 @@ provision_contract_user_group_uref = { cost = 124, arguments = [0,1,2,3,4] } put_key = { cost = 125, arguments = [0, 1, 2, 3] } read_host_buffer = { cost = 126, arguments = [0, 1, 2] } read_value = { cost = 127, arguments = [0, 1, 0] } -read_value_local = { cost = 128, arguments = [0, 1, 0] } +dictionary_get = { cost = 128, arguments = [0, 1, 0] } remove_associated_key = { cost = 129, arguments = [0, 1] } remove_contract_user_group = { cost = 130, arguments = [0, 1, 2, 3] } remove_contract_user_group_urefs = { cost = 131, arguments = [0,1,2,3,4,5] } @@ -138,7 +138,7 @@ transfer_from_purse_to_purse = { cost = 137, arguments = [0, 1, 2, 3, 4, 5, 6, 7 transfer_to_account = { cost = 138, arguments = [0, 1, 2, 3, 4, 5, 6] } update_associated_key = { cost = 139, arguments = [0, 1, 2] } write = { cost = 140, arguments = [0, 1, 0, 2] } -write_local = { cost = 141, arguments = [0, 1, 2, 3] } +dictionary_put = { cost = 141, arguments = [0, 1, 2, 3] } enable_contract_version = { cost = 142, arguments = [0, 1, 2, 3] } [system_costs] diff --git a/resources/test/valid/0_9_0_unordered/chainspec.toml b/resources/test/valid/0_9_0_unordered/chainspec.toml index b124e80098..848ac2526a 100644 --- a/resources/test/valid/0_9_0_unordered/chainspec.toml +++ b/resources/test/valid/0_9_0_unordered/chainspec.toml @@ -123,7 +123,7 @@ provision_contract_user_group_uref = { cost = 124, arguments = [0,1,2,3,4] } put_key = { cost = 125, arguments = [0, 1, 2, 3] } read_host_buffer = { cost = 126, arguments = [0, 1, 2] } read_value = { cost = 127, arguments = [0, 1, 0] } -read_value_local = { cost = 128, arguments = [0, 1, 0] } +dictionary_get = { cost = 128, arguments = [0, 1, 0] } remove_associated_key = { cost = 129, arguments = [0, 1] } remove_contract_user_group = { cost = 130, arguments = [0, 1, 2, 3] } remove_contract_user_group_urefs = { cost = 131, arguments = [0,1,2,3,4,5] } @@ -136,7 +136,7 @@ transfer_from_purse_to_purse = { cost = 137, arguments = [0, 1, 2, 3, 4, 5, 6, 7 transfer_to_account = { cost = 138, arguments = [0, 1, 2, 3, 4, 5, 6] } update_associated_key = { cost = 139, arguments = [0, 1, 2] } write = { cost = 140, arguments = [0, 1, 0, 2] } -write_local = { cost = 141, arguments = [0, 1, 2, 3] } +dictionary_put = { cost = 141, arguments = [0, 1, 2, 3] } enable_contract_version = { cost = 142, arguments = [0, 1, 2, 3] } [system_costs] diff --git a/resources/test/valid/1_0_0/chainspec.toml b/resources/test/valid/1_0_0/chainspec.toml index 68057e902b..72c1f68f95 100644 --- a/resources/test/valid/1_0_0/chainspec.toml +++ b/resources/test/valid/1_0_0/chainspec.toml @@ -126,7 +126,7 @@ put_key = { cost = 125, arguments = [0, 1, 2, 3] } random_bytes = { cost = 123, arguments = [0, 1] } read_host_buffer = { cost = 126, arguments = [0, 1, 2] } read_value = { cost = 127, arguments = [0, 1, 0] } -read_value_local = { cost = 128, arguments = [0, 1, 0] } +dictionary_get = { cost = 128, arguments = [0, 1, 0] } remove_associated_key = { cost = 129, arguments = [0, 1] } remove_contract_user_group = { cost = 130, arguments = [0, 1, 2, 3] } remove_contract_user_group_urefs = { cost = 131, arguments = [0,1,2,3,4,5] } @@ -139,7 +139,7 @@ transfer_from_purse_to_purse = { cost = 137, arguments = [0, 1, 2, 3, 4, 5, 6, 7 transfer_to_account = { cost = 138, arguments = [0, 1, 2, 3, 4, 5, 6] } update_associated_key = { cost = 139, arguments = [0, 1, 2] } write = { cost = 140, arguments = [0, 1, 0, 2] } -write_local = { cost = 141, arguments = [0, 1, 2, 3] } +dictionary_put = { cost = 141, arguments = [0, 1, 2, 3] } enable_contract_version = { cost = 142, arguments = [0, 1, 2, 3] } [system_costs] diff --git a/types/src/api_error.rs b/types/src/api_error.rs index 985be9be3b..eb1da1a1e8 100644 --- a/types/src/api_error.rs +++ b/types/src/api_error.rs @@ -655,22 +655,22 @@ impl Debug for ApiError { ApiError::AuctionError(value) => write!( f, "ApiError::AuctionError({:?})", - auction::Error::try_from(*value).map_err(|_err| fmt::Error::default())? + auction::Error::try_from(*value).map_err(|_err| fmt::Error)? )?, ApiError::ContractHeader(value) => write!( f, "ApiError::ContractHeader({:?})", - contracts::Error::try_from(*value).map_err(|_err| fmt::Error::default())? + contracts::Error::try_from(*value).map_err(|_err| fmt::Error)? )?, ApiError::Mint(value) => write!( f, "ApiError::Mint({:?})", - mint::Error::try_from(*value).map_err(|_err| fmt::Error::default())? + mint::Error::try_from(*value).map_err(|_err| fmt::Error)? )?, ApiError::HandlePayment(value) => write!( f, "ApiError::HandlePayment({:?})", - handle_payment::Error::try_from(*value).map_err(|_err| fmt::Error::default())? + handle_payment::Error::try_from(*value).map_err(|_err| fmt::Error)? )?, ApiError::User(value) => write!(f, "ApiError::User({})", value)?, } diff --git a/types/src/era_id.rs b/types/src/era_id.rs index 71e8390a92..9fe3d98c3c 100644 --- a/types/src/era_id.rs +++ b/types/src/era_id.rs @@ -220,7 +220,7 @@ mod tests { assert_eq!(window.len(), auction_delay as usize + 1); assert_eq!(window.get(0), Some(¤t_era)); assert_eq!( - window.iter().rev().next(), + window.iter().next_back(), Some(&(current_era + auction_delay)) ); } diff --git a/types/src/execution_result.rs b/types/src/execution_result.rs index cc73d9ec91..87788fc94c 100644 --- a/types/src/execution_result.rs +++ b/types/src/execution_result.rs @@ -63,6 +63,7 @@ enum OpTag { Write = 1, Add = 2, NoOp = 3, + Delete = 4, } impl TryFrom for OpTag { @@ -95,6 +96,7 @@ enum TransformTag { AddKeys = 16, Failure = 17, WriteUnbonding = 18, + Prune = 19, } impl TryFrom for TransformTag { @@ -438,6 +440,8 @@ pub enum OpKind { Add, /// An operation which has no effect. NoOp, + /// A delete operation. + Delete, } impl OpKind { @@ -447,6 +451,7 @@ impl OpKind { OpKind::Write => OpTag::Write, OpKind::Add => OpTag::Add, OpKind::NoOp => OpTag::NoOp, + OpKind::Delete => OpTag::Delete, } } } @@ -471,6 +476,7 @@ impl FromBytes for OpKind { OpTag::Write => Ok((OpKind::Write, remainder)), OpTag::Add => Ok((OpKind::Add, remainder)), OpTag::NoOp => Ok((OpKind::NoOp, remainder)), + OpTag::Delete => Ok((OpKind::Delete, remainder)), } } } @@ -554,6 +560,8 @@ pub enum Transform { Failure(String), /// Writes the given Unbonding to global state. WriteUnbonding(Vec), + /// Prunes a key. + Prune, } impl Transform { @@ -578,6 +586,7 @@ impl Transform { Transform::AddKeys(_) => TransformTag::AddKeys, Transform::Failure(_) => TransformTag::Failure, Transform::WriteUnbonding(_) => TransformTag::WriteUnbonding, + Transform::Prune => TransformTag::Prune, } } } @@ -638,6 +647,7 @@ impl ToBytes for Transform { Transform::WriteUnbonding(value) => { buffer.extend(value.to_bytes()?); } + Transform::Prune => {} } Ok(buffer) } @@ -663,6 +673,7 @@ impl ToBytes for Transform { Transform::WriteBid(value) => value.serialized_length(), Transform::WriteWithdraw(value) => value.serialized_length(), Transform::WriteUnbonding(value) => value.serialized_length(), + Transform::Prune => 0, }; U8_SERIALIZED_LENGTH + body_len } @@ -738,6 +749,7 @@ impl FromBytes for Transform { as FromBytes>::from_bytes(remainder)?; Ok((Transform::WriteUnbonding(unbonding_purses), remainder)) } + TransformTag::Prune => Ok((Transform::Prune, remainder)), } } } @@ -745,7 +757,7 @@ impl FromBytes for Transform { impl Distribution for Standard { fn sample(&self, rng: &mut R) -> Transform { // TODO - include WriteDeployInfo and WriteTransfer as options - match rng.gen_range(0..13) { + match rng.gen_range(0..14) { 0 => Transform::Identity, 1 => Transform::WriteCLValue(CLValue::from_t(true).unwrap()), 2 => Transform::WriteAccount(AccountHash::new(rng.gen())), @@ -768,6 +780,7 @@ impl Distribution for Standard { Transform::AddKeys(named_keys) } 12 => Transform::Failure(rng.gen::().to_string()), + 13 => Transform::Prune, _ => unreachable!(), } } diff --git a/types/src/key.rs b/types/src/key.rs index f092a74a3b..addede0246 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -575,6 +575,16 @@ impl Key { } false } + + /// Returns a reference to the inner [`AccountHash`] if `self` is of type + /// [`Key::Withdraw`], otherwise returns `None`. + pub fn as_withdraw(&self) -> Option<&AccountHash> { + if let Self::Withdraw(v) = self { + Some(v) + } else { + None + } + } } impl Display for Key { diff --git a/utils/global-state-update-gen/src/generic.rs b/utils/global-state-update-gen/src/generic.rs index 466831dd39..e8e52acde3 100644 --- a/utils/global-state-update-gen/src/generic.rs +++ b/utils/global-state-update-gen/src/generic.rs @@ -277,7 +277,7 @@ pub fn add_and_remove_bids( validators_diff.removed.clone() }; - for (pub_key, seigniorage_recipient) in new_snapshot.values().rev().next().unwrap() { + for (pub_key, seigniorage_recipient) in new_snapshot.values().next_back().unwrap() { create_or_update_bid(state, pub_key, seigniorage_recipient, slash); } diff --git a/utils/nctl/sh/assets/compile.sh b/utils/nctl/sh/assets/compile.sh index ed61e8f5b0..82077a3363 100644 --- a/utils/nctl/sh/assets/compile.sh +++ b/utils/nctl/sh/assets/compile.sh @@ -6,6 +6,11 @@ # NCTL - path to nctl home directory. ######################################## +if [ "$NCTL_SKIP_COMPILATION" = "true" ]; then + echo "skipping nctl-compile as requested"; + return; +fi + unset OPTIND #clean OPTIND envvar, otherwise getopts can break. COMPILE_MODE="release" #default compile mode to release. diff --git a/utils/nctl/sh/assets/setup_shared.sh b/utils/nctl/sh/assets/setup_shared.sh index 905da4fa53..47cdfeecaf 100644 --- a/utils/nctl/sh/assets/setup_shared.sh +++ b/utils/nctl/sh/assets/setup_shared.sh @@ -411,6 +411,10 @@ function setup_asset_node_configs() SPECULATIVE_EXEC_ADDR=$(grep 'speculative_exec_server' $PATH_TO_CONFIG_FILE || true) # Set node configuration settings. + # Note: To dump TLS keys, add + # "cfg['network']['keylog_path']='$PATH_TO_NET/tlskeys';" + # -- but beware, this will break older nodes configurations. + # TODO: Write conditional include of this configuration setting. SCRIPT=( "import toml;" "cfg=toml.load('$PATH_TO_CONFIG_FILE');" diff --git a/utils/nctl/sh/staging/build.sh b/utils/nctl/sh/staging/build.sh index 3ffd002985..2fac9e4164 100644 --- a/utils/nctl/sh/staging/build.sh +++ b/utils/nctl/sh/staging/build.sh @@ -45,6 +45,12 @@ function _main() ####################################### function set_stage_binaries() { + # Allow for external overriding of binary staging step if necessary. + if [ ! -z $NCTL_OVERRIDE_STAGE_BINARIES ]; then + $NCTL_OVERRIDE_STAGE_BINARIES + return + fi; + local PATH_TO_NODE_SOURCE=${1} local PATH_TO_CLIENT_SOURCE=${2} diff --git a/utils/nctl/sh/staging/set_remote.sh b/utils/nctl/sh/staging/set_remote.sh index b78afdfa2f..be1f490e9e 100644 --- a/utils/nctl/sh/staging/set_remote.sh +++ b/utils/nctl/sh/staging/set_remote.sh @@ -53,6 +53,13 @@ function _main() curl -O "$_BASE_URL/v$PROTOCOL_VERSION/$REMOTE_FILE" > /dev/null 2>&1 fi done + + # Allow external hook for patching the downloaded binaries. + if [ ! -z "${NCTL_PATCH_REMOTE_CMD}" ]; then + $NCTL_PATCH_REMOTE_CMD ./casper-node + $NCTL_PATCH_REMOTE_CMD ./global-state-update-gen + fi + chmod +x ./casper-node chmod +x ./global-state-update-gen if [ "${#PROTOCOL_VERSION}" = '3' ]; then