diff --git a/Cargo.lock b/Cargo.lock index f4ff9b68..0869a1ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.1.6", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] @@ -40,6 +40,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -103,7 +112,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -139,7 +148,7 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -168,7 +177,7 @@ dependencies = [ "itoa", "serde", "serde_json", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -181,7 +190,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -204,7 +213,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "serde", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -228,7 +237,7 @@ dependencies = [ "serde", "serde_with", "sha2 0.10.9", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -281,7 +290,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] @@ -308,7 +317,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -340,7 +349,7 @@ dependencies = [ "rand 0.8.5", "serde_json", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", "url", ] @@ -358,7 +367,7 @@ dependencies = [ "derive_more", "foldhash 0.1.5", "hashbrown 0.15.4", - "indexmap 2.12.0", + "indexmap 2.14.2", "itoa", "k256", "keccak-asm", @@ -407,7 +416,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tracing", "url", @@ -455,7 +464,7 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -544,7 +553,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -570,7 +579,7 @@ dependencies = [ "either", "elliptic-curve 0.13.8", "k256", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -586,7 +595,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -600,7 +609,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -613,11 +622,11 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.12.0", + "indexmap 2.14.2", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "syn-solidity", "tiny-keccak", ] @@ -636,7 +645,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.104", + "syn 2.0.119", "syn-solidity", ] @@ -647,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10db1bd7baa35bc8d4a1b07efbf734e73e5ba09f2580fb8cee3483a36087ceb2" dependencies = [ "serde", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -678,7 +687,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tower 0.5.2", "tracing", @@ -750,7 +759,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -826,9 +835,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ark-ff" @@ -954,12 +963,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" @@ -989,7 +992,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -1000,7 +1003,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -1017,7 +1020,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -1079,11 +1082,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.2", + "axum-core 0.5.6", "bytes", "form_urlencoded", "futures-util", @@ -1098,8 +1101,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -1133,9 +1135,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -1144,7 +1146,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -1202,7 +1203,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -1256,15 +1257,15 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" dependencies = [ - "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures 0.3.0", "zeroize", ] @@ -1288,9 +1289,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe" dependencies = [ "cc", "glob", @@ -1318,9 +1319,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -1375,9 +1376,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1385,17 +1386,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.1" @@ -1403,21 +1393,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", + "cipher", "cpufeatures 0.3.0", "rand_core 0.10.1", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", - "zeroize", ] [[package]] @@ -1464,13 +1454,13 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.6", + "block-buffer 0.12.1", + "crypto-common 0.2.2", "inout", - "zeroize", ] [[package]] @@ -1515,7 +1505,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -1533,12 +1523,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11ed919bd3bae4af5ab56372b627dfc32622aba6cec36906e8ab46746037c9d" - [[package]] name = "cmov" version = "0.5.4" @@ -1563,9 +1547,9 @@ dependencies = [ [[package]] name = "commonware-actor" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beac6262223b45c6843ab70b245b526b28836a8257b1759a509ba5fab24fa4a6" +checksum = "e96ef599ff4c2ec1608c9b5b3527ae76be6b48cfc71dc9c89910105febc3ec91" dependencies = [ "cfg-if", "commonware-macros", @@ -1577,9 +1561,9 @@ dependencies = [ [[package]] name = "commonware-broadcast" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2d7e56dc894ad3f55dbe5726468915492ae8e4650ad2a45c1bcfd138c2037f" +checksum = "f11e2cda843c7a2d3480ea7fa2fe6d9a7b4f897dfcef395c9b85e5bceb2db7f8" dependencies = [ "commonware-actor", "commonware-codec", @@ -1588,15 +1572,15 @@ dependencies = [ "commonware-p2p", "commonware-runtime", "commonware-utils", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] [[package]] name = "commonware-codec" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8eb7efe071cea13e8b23a0a7f584398870c401d2b2aabfb6b9ab3634ebcba3a" +checksum = "4d1e1bd11366f81e304c737c098eb0c075cef59e21daff44c3227952c2f75b49" dependencies = [ "bytes", "cfg-if", @@ -1604,26 +1588,26 @@ dependencies = [ "commonware-macros", "paste", "rand_chacha 0.10.0", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] name = "commonware-codec-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a876bec347344381948901d0f5ec2e9443ebc8dbab99a63bc2db78c2fc2efbdd" +checksum = "54ccca449c618a38da9b867c128ec1e7be55cfe0b69b8814c25daac229ac6cc9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "commonware-coding" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409e9997c1bb3273b9570c26ddef4f252327c9bf18968d4439a83cfd1d3e5661" +checksum = "07145b2f348a8d3faa058dca684a5a37e0cede9f53ef123421c2227d40477afb" dependencies = [ "bytes", "commonware-codec", @@ -1635,14 +1619,14 @@ dependencies = [ "commonware-utils", "num-rational", "rayon", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] name = "commonware-consensus" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf15e3402e26f062695d4dd5e4b5967f601dc7303d06b9fafdb20a6485a3a0a" +checksum = "e2af83244a9e1982617148e577a6f692b22f4bf727976cddf978d4afcd73a4e7" dependencies = [ "bytes", "cfg-if", @@ -1665,15 +1649,15 @@ dependencies = [ "rand 0.10.2", "rand_core 0.10.1", "rayon", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] [[package]] name = "commonware-cryptography" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b842f31e1aaa1af284a1f9dc4f1234761f72d07e70d53cf3f1521b0870ade094" +checksum = "df6b593210847c596c6657f2fb033c57ac0ef2f7ce68559c0e79040a34b65b7e" dependencies = [ "ahash", "anyhow", @@ -1689,15 +1673,15 @@ dependencies = [ "commonware-math", "commonware-parallel", "commonware-utils", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "crc-fast", - "ctutils 0.3.1", + "ctutils", "curve25519-dalek 5.0.0", "ecdsa 0.17.0", "fixedbitset", "getrandom 0.2.16", "getrandom 0.4.3", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-rational", "num-traits", "once_cell", @@ -1706,16 +1690,16 @@ dependencies = [ "rand_chacha 0.10.0", "rand_core 0.10.1", "sha2 0.11.0", - "thiserror 2.0.12", + "thiserror 2.0.20", "x25519-dalek", "zeroize", ] [[package]] name = "commonware-formatting" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab01b7f2798e29b0a4f7b47da5b7f3c106881578af3e69e8d2ee9d89d7c1d4fd" +checksum = "7cdfc3a5a45f024571d9abce2bdb5a52081f9f017e61b80e2bff89856361ceee" dependencies = [ "commonware-macros", "const-hex", @@ -1723,9 +1707,9 @@ dependencies = [ [[package]] name = "commonware-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e37a17d5c99ec6711098c6614af1023fd32926aa0414ec0f0613f6bb940278" +checksum = "3058bc70420515d6e92d99510ec2b2a2e1d029a6aced44d243c05ce8d60c6493" dependencies = [ "commonware-macros-impl", "tokio", @@ -1733,22 +1717,22 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d44bceee874226efca172f0e9ec9ec4eb486788ba6be1d98710f3f159b807af" +checksum = "bbcf77c39d7486af7394b1d1c4d268d08a5763036b813a2e6aba2ee825b5bd45" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", - "toml", + "syn 2.0.119", + "toml 1.1.5+spec-1.1.0", ] [[package]] name = "commonware-math" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "593fa0c5aa8bd8d350724b9b1ad4e85072845aa85a06b39ca4598998bbec1e00" +checksum = "e0227b6d4c3e264894c92c682e532128dd22c21bf835c86a6ec6ba9cb15e3f87" dependencies = [ "bytes", "commonware-codec", @@ -1760,9 +1744,9 @@ dependencies = [ [[package]] name = "commonware-p2p" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f974fcd27c0dad7ae73caeaf00c8330fd29c60f5ff1f679ccc1a51c8ed95bc" +checksum = "38bf74a0456b74a0f84e4ab23a8765227d9c52cdef63f4968670c96467b3c63a" dependencies = [ "commonware-actor", "commonware-codec", @@ -1781,15 +1765,15 @@ dependencies = [ "rand 0.10.2", "rand_core 0.10.1", "rand_distr", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] [[package]] name = "commonware-parallel" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7109817274c671f1fb0ba17cf8aecfbb70672d29379284feeac43fa1b1dc0201" +checksum = "10bae95b6d977699c212691a996805e241936d8f958c73a1ebff0190376d1260" dependencies = [ "cfg-if", "commonware-macros", @@ -1800,9 +1784,9 @@ dependencies = [ [[package]] name = "commonware-resolver" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a497cc9e9a2cd205d96474d0d40c7731f29085cd467adbe9ff0f94719659582" +checksum = "64cde1899be3cb1ff031f144fce7236aab504e6f56378235f16bd0469e9545e3" dependencies = [ "bytes", "commonware-actor", @@ -1816,18 +1800,18 @@ dependencies = [ "futures", "rand 0.10.2", "rand_core 0.10.1", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] [[package]] name = "commonware-runtime" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a84bc6cefb099b1ffce3e04705a1e61e91d895b9f09b9e1be14c8ec5823c181" +checksum = "fba4d9bbc36e8d5df471666c5a3f2fa2599f1e860bc61c05c1879ec5643701d9" dependencies = [ "ahash", - "axum 0.8.4", + "axum 0.8.9", "bytes", "cfg-if", "commonware-codec", @@ -1847,13 +1831,13 @@ dependencies = [ "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", - "prometheus-client 0.24.0", + "prometheus-client 0.25.1", "rand 0.10.2", "rand_core 0.10.1", "rayon", "sha2 0.11.0", "sysinfo", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tracing", "tracing-opentelemetry", @@ -1862,21 +1846,21 @@ dependencies = [ [[package]] name = "commonware-runtime-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f7b39ef9fd9f1df4fe4445c9a5ec5ba012d90d1602b12a8b8626a8563ab02c" +checksum = "d478d4299c93e9f9a80064efdacd69ac0fe8324a9dc44264edbcd0d47c5d6eb3" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "commonware-storage" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac18e2b696ab7bbecf1059bc3b8d39fc0af002180906d65c7df3112fd9385de" +checksum = "15ec50ba1efad76170c65d8c53baefc49da20eed02df9eedb3aa0754f2ff1d53" dependencies = [ "ahash", "anyhow", @@ -1891,17 +1875,17 @@ dependencies = [ "commonware-utils", "futures", "futures-util", - "hashbrown 0.16.1", - "thiserror 2.0.12", + "hashbrown 0.17.1", + "thiserror 2.0.20", "tracing", "zstd", ] [[package]] name = "commonware-stream" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8890b69e3fa209bc9e87d199ff60ca5a21c88322d32914847511887e6dc5364b" +checksum = "e4448b7e1d6a0d7a3fc9521be6ddebba0e3bde321da63e414233b6c53bd39e77" dependencies = [ "chacha20poly1305", "commonware-codec", @@ -1912,20 +1896,19 @@ dependencies = [ "commonware-utils", "futures", "rand_core 0.10.1", - "thiserror 2.0.12", + "thiserror 2.0.20", "x25519-dalek", "zeroize", ] [[package]] name = "commonware-utils" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a484dd46b738d9b5ba892312425e71e6afaf4320e388d74449bd9ca0ea14cd76" +checksum = "a611d1e0f4285e396deab2e76cf0554f05fee53e9c3d69a53374ce069b88ab44" dependencies = [ "ahash", "bytes", - "cfg-if", "commonware-codec", "commonware-formatting", "commonware-macros", @@ -1933,7 +1916,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "getrandom 0.4.3", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-bigint", "num-integer", "num-rational", @@ -1941,7 +1924,7 @@ dependencies = [ "parking_lot", "pin-project", "rand 0.10.2", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tracing", "zeroize", @@ -2032,9 +2015,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation" @@ -2122,10 +2105,11 @@ dependencies = [ [[package]] name = "criterion" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", @@ -2134,6 +2118,7 @@ dependencies = [ "itertools 0.13.0", "num-traits", "oorandom", + "page_size", "plotters", "rayon", "regex", @@ -2145,9 +2130,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", @@ -2183,18 +2168,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -2221,7 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", - "ctutils 0.4.2", + "ctutils", "getrandom 0.4.3", "hybrid-array", "num-traits", @@ -2237,7 +2222,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -2252,22 +2236,13 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "ctutils" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c67c81499f542d1dd38c6a2a2fe825f4dd4bca5162965dd2eea0c8119873d3c" -dependencies = [ - "cmov 0.4.3", -] - [[package]] name = "ctutils" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "cmov 0.5.4", + "cmov", "subtle", ] @@ -2310,7 +2285,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2344,7 +2319,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2359,7 +2334,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2370,7 +2345,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2381,14 +2356,14 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -2456,7 +2431,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "unicode-xid", ] @@ -2490,7 +2465,7 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", - "ctutils 0.4.2", + "ctutils", ] [[package]] @@ -2535,6 +2510,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2543,7 +2528,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2602,9 +2587,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -2722,7 +2707,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -2885,9 +2870,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -2900,9 +2885,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -2910,15 +2895,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -2927,32 +2912,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -2966,9 +2951,9 @@ dependencies = [ [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2978,7 +2963,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -3149,7 +3133,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -3201,6 +3185,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3570,7 +3563,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -3592,23 +3585,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -3748,7 +3741,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "soketto", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-util", @@ -3775,7 +3768,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tower 0.5.2", "tracing", @@ -3801,7 +3794,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tokio-stream", "tower 0.5.2", @@ -3826,7 +3819,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tower 0.5.2", "url", @@ -3842,7 +3835,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -3864,7 +3857,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-util", @@ -3891,7 +3884,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-util", @@ -3908,7 +3901,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -3920,7 +3913,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -4005,9 +3998,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -4123,7 +4116,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4172,7 +4165,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4182,7 +4175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.14.2", "metrics", "metrics-util", "quanta", @@ -4306,9 +4299,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -4378,7 +4371,7 @@ checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4394,6 +4387,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4401,6 +4403,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", ] [[package]] @@ -4413,11 +4433,22 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -4446,15 +4477,9 @@ dependencies = [ "alloy-serde", "derive_more", "serde", - "thiserror 2.0.12", + "thiserror 2.0.20", ] -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl" version = "0.10.73" @@ -4478,7 +4503,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4509,7 +4534,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.12", + "thiserror 2.0.20", "tracing", ] @@ -4539,7 +4564,7 @@ dependencies = [ "opentelemetry_sdk", "prost 0.14.1", "reqwest 0.13.4", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -4566,7 +4591,7 @@ dependencies = [ "percent-encoding", "portable-atomic", "rand 0.9.1", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tokio-stream", ] @@ -4590,6 +4615,16 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -4615,7 +4650,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4670,28 +4705,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.20", "ucd-trie", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4762,12 +4797,11 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", + "cpufeatures 0.3.0", "universal-hash", ] @@ -4841,9 +4875,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -4867,7 +4901,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -4916,7 +4950,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -4940,7 +4974,19 @@ dependencies = [ "dtoa", "itoa", "parking_lot", - "prometheus-client-derive-encode 0.5.0", + "prometheus-client-derive-encode 0.5.1", +] + +[[package]] +name = "prometheus-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1784dc11a05f5a8f57c4a62915686be140f24f70301290b997e317241fa9ffc2" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode 0.5.1", ] [[package]] @@ -4951,18 +4997,18 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "prometheus-client-derive-encode" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" +checksum = "01e34894696ff94f64a20c2c373a6440903e9c2789a303d68ec6e6f953f890e4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 3.0.5", ] [[package]] @@ -5015,7 +5061,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -5028,7 +5074,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -5095,7 +5141,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.6.3", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -5116,7 +5162,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -5192,7 +5238,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.1", + "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -5291,9 +5337,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -5301,9 +5347,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -5343,7 +5389,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.20", ] [[package]] @@ -5363,7 +5409,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -5489,7 +5535,7 @@ dependencies = [ "futures-util", "metrics", "reth-metrics", - "thiserror 2.0.12", + "thiserror 2.0.20", "tokio", "tracing", "tracing-futures", @@ -5818,7 +5864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct 1.0.0", - "ctutils 0.4.2", + "ctutils", "der 0.8.1", "hybrid-array", "subtle", @@ -5939,7 +5985,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -5967,9 +6013,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -5996,7 +6042,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.14.2", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -6015,7 +6061,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -6143,7 +6189,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.20", "time", ] @@ -6276,7 +6322,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -6350,7 +6396,7 @@ dependencies = [ "tikv-jemalloc-ctl", "tokio", "tokio-util", - "toml", + "toml 0.9.8", "tower 0.5.2", "tracing", "tracing-appender", @@ -6556,7 +6602,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tokio", - "toml", + "toml 0.9.8", "tracing", "url", ] @@ -6574,9 +6620,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -6592,7 +6649,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -6612,21 +6669,22 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "sysinfo" -version = "0.37.2" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows 0.61.3", + "objc2-open-directory", + "windows 0.62.2", ] [[package]] @@ -6680,11 +6738,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.20", ] [[package]] @@ -6695,18 +6753,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 3.0.5", ] [[package]] @@ -6825,9 +6883,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6849,7 +6907,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -6904,13 +6962,28 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.2", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +dependencies = [ + "indexmap 2.14.2", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.4", ] [[package]] @@ -6922,32 +6995,41 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap 2.12.0", - "toml_datetime", + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" @@ -7065,7 +7147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 2.0.12", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -7078,7 +7160,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -7209,12 +7291,12 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.6", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -7335,7 +7417,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -7370,7 +7452,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7494,24 +7576,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core 0.62.2", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -7540,14 +7621,27 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", "windows-threading", ] @@ -7559,7 +7653,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -7570,7 +7664,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -7581,7 +7675,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -7592,7 +7686,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -7609,12 +7703,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -7646,6 +7740,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -7665,6 +7768,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7783,11 +7895,11 @@ dependencies = [ [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] @@ -7979,6 +8091,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -8045,7 +8166,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "synstructure", ] @@ -8066,7 +8187,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -8086,28 +8207,28 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] @@ -8140,7 +8261,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 916cc6bd..da977648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,20 +15,20 @@ summit-finalizer = {path = "finalizer"} summit-rpc = {path = "rpc"} summit-orchestrator = {path = "orchestrator"} -commonware-actor = "2026.7.0" -commonware-formatting = "2026.7.0" -commonware-consensus = "2026.7.0" -commonware-cryptography = "2026.7.0" -commonware-storage = "2026.7.0" -commonware-runtime = "2026.7.0" -commonware-codec = "2026.7.0" -commonware-p2p = "2026.7.0" -commonware-broadcast = "2026.7.0" -commonware-utils = "2026.7.0" -commonware-resolver = "2026.7.0" -commonware-macros = "2026.7.0" -commonware-math = "2026.7.0" -commonware-parallel = "2026.7.0" +commonware-actor = "2026.9.0" +commonware-formatting = "2026.9.0" +commonware-consensus = "2026.9.0" +commonware-cryptography = "2026.9.0" +commonware-storage = "2026.9.0" +commonware-runtime = "2026.9.0" +commonware-codec = "2026.9.0" +commonware-p2p = "2026.9.0" +commonware-broadcast = "2026.9.0" +commonware-utils = "2026.9.0" +commonware-resolver = "2026.9.0" +commonware-macros = "2026.9.0" +commonware-math = "2026.9.0" +commonware-parallel = "2026.9.0" alloy-consensus = "1.0.12" alloy-eips = { version = "1.0.19", features = ["ssz"] } diff --git a/application/src/actor.rs b/application/src/actor.rs index c937cb83..0e72fcad 100644 --- a/application/src/actor.rs +++ b/application/src/actor.rs @@ -467,13 +467,11 @@ impl< let requester = try_join(parent_request, block_request); select! { result = requester => { - // The syncer drops (cancels) a block subscription for a - // round older than its last_processed_round, so a stale - // verify path can see the request canceled. Treat that as - // a terminal "cannot verify" (vote false) rather than an - // invariant violation. + // Local subscriptions survive resolver-floor denial. + // Shutdown or a closed subscription can still cancel + // this work; it is not evidence of an invalid peer. let Ok((parent, block)) = result else { - warn!(?round, "verify aborted: block subscription canceled (likely stale round)"); + warn!(?round, "verify aborted: block subscription canceled"); let _ = response.send(false); return; }; diff --git a/application/src/ingress.rs b/application/src/ingress.rs index 1c5c1cb4..77cd28d7 100644 --- a/application/src/ingress.rs +++ b/application/src/ingress.rs @@ -165,7 +165,7 @@ mod tests { let (tx, mut rx) = mpsc::channel(4); let mut mailbox = Mailbox::::new(tx); - let digest = Sha256::hash(b"proposal"); + let digest = Sha256::hash(&[b"proposal"]); let round = Round::new(Epoch::new(3), View::new(7)); let peers = vec![test_public_key(1), test_public_key(2)]; diff --git a/docs/deposits-and-withdrawals.md b/docs/deposits-and-withdrawals.md index 39b4e3f3..5d752721 100644 --- a/docs/deposits-and-withdrawals.md +++ b/docs/deposits-and-withdrawals.md @@ -5,7 +5,8 @@ - Potential validators have to deposit at least **MINIMUM_STAKE** to join the network. - If a potential validator makes an initial deposit with *amount* < **MINIMUM_STAKE**, then the validator account is still created, but it won't be set to active. - Top-up deposits are allowed. -- Once a processed deposit brings an inactive validator's balance to at least **MINIMUM_STAKE**, activation is scheduled **VALIDATOR_NUM_WARM_UP_EPOCHS** later. Deposit processing occurs near epoch end and is subject to **MAX_DEPOSITS_PER_EPOCH**, so activation may be delayed from submission. +- Once a processed deposit brings an inactive validator's balance to at least **MINIMUM_STAKE**, activation is scheduled **VALIDATOR_NUM_WARM_UP_EPOCHS** later, provided fewer than **MAX_VALIDATOR_COUNT** validators are active or joining. Deposit processing occurs near epoch end and is subject to **MAX_DEPOSITS_PER_EPOCH**, so activation may be delayed from submission. +- If **MAX_VALIDATOR_COUNT** is already occupied by active and joining validators, a valid deposit is still credited but the validator remains inactive and no activation is scheduled. - Deposit requests with invalid signatures will be refunded as a withdrawal. K% of the deposited amount (**INVALID_DEPOSIT_TAX**, default 5%) is sent to the treasury address (the zero address by default, which effectively burns it). This prevents invalid deposits from becoming a DDOS vector. - if the deposit's keys are malformed, it is refunded with the same K% tax applied to invalid signatures. - If the deposit's consensus (BLS) key does not match the key already on the account (or is already used by another validator), the deposit is refunded with the same K% tax applied to invalid signatures. @@ -22,6 +23,33 @@ - An invalid deposit may create separate refund and treasury withdrawals. Each withdrawal consumes one slot under the payout cap. - Requests included in the last block of epoch E remain buffered until the penultimate block of E+1. An accepted exit then occurs at the end of E+1, with payout scheduled for epoch **E + 1 + VALIDATOR_WITHDRAWAL_NUM_EPOCHS**, subject to the withdrawal cap. +## Validator Count Limits +- **MAX_VALIDATOR_COUNT** (`MaxValidatorCount`, configured in genesis as `max_validator_count`) defaults to **128** and accepts values from **1 to 4,096**. It limits admission of **Active + Joining** validators, not the total number of stored accounts. Joining validators reserve a slot throughout warm-up. +- At epoch processing, the final proposed **MINIMUM_VALIDATOR_COUNT** and **MAX_VALIDATOR_COUNT** are evaluated together, using the last valid request for each. If minimum exceeds maximum, all updates to those two parameters in the pending batch are discarded before withdrawal or deposit decisions; the existing pair remains in effect. Unrelated parameter updates are retained. Valid paired changes are independent of request ordering. +- A proposed maximum below the current **Active + Joining** count is rejected before withdrawals or deposits are processed. Equality is allowed. All pending maximum-count updates are discarded on rejection, retaining the existing maximum rather than falling back to an earlier request; other updates remain subject to minimum/maximum consistency validation. Same-batch exits or joining cancellations cannot make the reduction valid; submit a later update after membership has decreased. No validators are evicted and no activation reservations are canceled by a cap change. +- Raising the maximum does not automatically activate funded inactive accounts. Another valid deposit must trigger admission, and any pending withdrawal still prevents reactivation. +- Withdrawals are processed before queued deposits are credited. An accepted active full exit or joining-validator cancellation frees an admission slot for that batch; a rejected withdrawal or active partial withdrawal does not. Deposits compete for available slots in deposit-queue order. +- A withdrawal for an account first created by a deposit in that same batch is dropped because the account does not yet exist. The deposit is still credited; a later authorized withdrawal can reclaim it. Cap-blocked inactive accounts can withdraw without the active-validator minimum-balance floor. +- Stored state must have minimum no greater than maximum. Membership can temporarily exceed the stored maximum while an accepted queued increase is already being used for admission, before boundary application. Checkpoint recovery and network sizing must account for that pending increase. + +### Network Capacity and Restarts + +Summit allocates its fixed P2P queue capacity at startup using the validator and observer limits from recovered consensus state. Accepted pending increases are included; pending reductions do not reduce the startup allocation. The capacity includes one extra slot for the local identity when it is outside the authorized peer sets: + +```text +peer capacity = startup validator limit × (1 + startup observers per validator) + 1 +``` + +With the defaults of **128 validators** and **16 observers per validator**, this is **2,177 identities per peer set**. Authorized observer identities count even when they are offline. + +**Increasing `MaxValidatorCount` does not resize a running node's queues or automatically restart Summit.** To use a higher maximum that requires more peer capacity: + +1. Ensure the raised value is durably recorded in each affected node's consensus state, either as an effective parameter or an accepted pending increase. +2. Coordinate restarts of the affected Summit instances so they allocate capacity using that value. +3. Restart before larger validator/observer sets exceed the previous allocation. Restarting against unchanged protocol state does not increase capacity. + +A decrease, or an increase already covered by the startup allocation, does not require a restart for capacity reasons. The same capacity constraint applies when increasing the observers-per-validator parameter. If a registered peer set exceeds a node's fixed capacity, the node fails an assertion; this is an operational limit, not a node-local consensus rule rejecting the protocol parameter update. + ## Validator Balance - All active validators must have a balance of at least **MINIMUM_STAKE**. - There is no upper limit on validator balance, however, there is no advantage (such as higher chance of becoming a leader) in having a balance that exceeds **MINIMUM_STAKE**. diff --git a/docs/ssz-merklization.md b/docs/ssz-merklization.md index a50bae04..f05b44d9 100644 --- a/docs/ssz-merklization.md +++ b/docs/ssz-merklization.md @@ -36,7 +36,7 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f ### Top-Level Tree -32 leaf slots (depth 5), 28 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 28–31 are unused (zero-filled). +32 leaf slots (depth 5), 29 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 29–31 are unused (zero-filled). | Leaf Index | Field | Type | |------------|-------|------| @@ -67,6 +67,8 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f | 24 | `minimum_validator_count` | Scalar | | 25 | `pending_active_validator_exits` | Scalar | | 26 | `invalid_deposit_tax` | Scalar | +| 27 | `max_pending_withdrawals_per_validator` | Scalar | +| 28 | `max_validator_count` | Scalar | ### Collection Subtrees @@ -200,6 +202,7 @@ Single top-level leaf write + rehash of the 5-level path to root. | `set_max_deposits_per_epoch()` | `ssz_tree.set_max_deposits_per_epoch()` | | `set_max_withdrawals_per_epoch()` | `ssz_tree.set_max_withdrawals_per_epoch()` | | `set_observers_per_validator()` | `ssz_tree.set_observers_per_validator()` | +| `set_max_validator_count()` | `ssz_tree.set_max_validator_count()` | | `set_minimum_validator_count()` | `ssz_tree.set_minimum_validator_count()` | | `increment_pending_active_validator_exits()` / `reset_pending_active_validator_exits()` | `ssz_tree.set_pending_active_validator_exits()` | | `set_next_withdrawal_index()` | `ssz_tree.set_next_withdrawal_index()` | diff --git a/example_genesis.toml b/example_genesis.toml index 6eafe525..600e97ad 100644 --- a/example_genesis.toml +++ b/example_genesis.toml @@ -26,7 +26,8 @@ allowed_timestamp_future_ms = 10000 treasury_address = "0x0000000000000000000000000000000000000000" max_deposits_per_epoch = 3 max_withdrawals_per_epoch = 16 -observers_per_validator = 5 +observers_per_validator = 16 +max_validator_count = 128 minimum_validator_count = 3 invalid_deposit_tax = 0 diff --git a/finalizer/benches/consensus_state_write.rs b/finalizer/benches/consensus_state_write.rs index 175084f1..c7f651ab 100644 --- a/finalizer/benches/consensus_state_write.rs +++ b/finalizer/benches/consensus_state_write.rs @@ -85,6 +85,7 @@ fn main() { log: commonware_storage::journal::contiguous::variable::Config { partition: "bench-log".to_string(), write_buffer: NZUsize!(64 * 1024), + replay_buffer: NZUsize!(64 * 1024), compression: None, codec_config: ((), ()), items_per_section: NZU64!(4), @@ -96,6 +97,7 @@ fn main() { }, translator: EightCap, init_cache_size: Some(NZUsize!(1024)), + init_buffer: NZUsize!(64 * 1024), }; let mut db = diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index 156ebf3e..2d40e121 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -1,5 +1,5 @@ use crate::config::ProtocolConsts; -use crate::db::{Config as StateConfig, FinalizerState}; +use crate::db::FinalizerState; use crate::{FinalizerConfig, FinalizerMailbox, FinalizerMessage}; use alloy_rpc_types_engine::ForkchoiceState; use anyhow::{Result, anyhow}; @@ -17,9 +17,7 @@ use commonware_runtime::telemetry::metrics::{Gauge, MetricsExt as _}; use commonware_runtime::{ BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage, spawn_cell, }; -use commonware_storage::translator::EightCap; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; -use commonware_utils::{NZU64, NZUsize}; use futures::channel::{mpsc, oneshot}; use futures::{FutureExt, StreamExt as _, select_biased}; #[cfg(feature = "prom")] @@ -27,7 +25,6 @@ use metrics::{counter, histogram}; use rand::Rng; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::marker::PhantomData; -use std::num::NonZero; use std::time::{Duration, Instant}; use summit_orchestrator::Message; use summit_syncer::{FaultEvidence, Update}; @@ -51,8 +48,6 @@ use summit_types::{EngineClient, consensus_state::ConsensusState}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, trace, warn}; -const WRITE_BUFFER: NonZero = NZUsize!(1024 * 1024); - type FinalizerScheme = bls12381_multisig::Scheme; type StateQueryResponse = ConsensusStateResponse>; type StateQueryMessage = ( @@ -313,18 +308,7 @@ impl< let (tx, rx) = mpsc::channel(cfg.mailbox_size); let (updates_tx, updates_rx) = mpsc::unbounded(); let (state_query, state_query_rx) = ConsensusStateQuery::new(cfg.mailbox_size); - let state_cfg = StateConfig { - log: commonware_storage::journal::contiguous::variable::Config { - partition: format!("{}-finalizer_state-log", cfg.db_prefix), - write_buffer: WRITE_BUFFER, - compression: None, - codec_config: ((), ()), - items_per_section: NZU64!(262_144), - page_cache: cfg.page_cache, - }, - translator: EightCap, - init_cache_size: Some(NZUsize!(1024)), - }; + let state_cfg = crate::db::config(&cfg.db_prefix, cfg.page_cache); let db = FinalizerState::::new( context.child("finalizer_state"), @@ -333,9 +317,9 @@ impl< ) .await; - // Check if the state exists in the database. Otherwise, use the initial state. - // The initial state could be from the genesis or a checkpoint. - // If we want to load a checkpoint, we have to make sure that the DB is cleared. + // Checkpoint startup durably promotes selected state before constructing + // this actor (startup::prepare). Never replace existing state here from + // an unqualified initial-state hint; the fallback is for empty storage. let state = if let Some(state) = db.get_latest_consensus_state().await { info!( epoch = state.get_epoch(), @@ -2010,6 +1994,10 @@ impl< let value = self.canonical_state.get_observers_per_validator(); let _ = sender.send(ConsensusStateResponse::ObserversPerValidator(value)); } + ConsensusStateRequest::GetMaxValidatorCount => { + let value = self.canonical_state.get_max_validator_count(); + let _ = sender.send(ConsensusStateResponse::MaxValidatorCount(value)); + } ConsensusStateRequest::GetMinimumValidatorCount => { let value = self.canonical_state.get_minimum_validator_count(); let _ = sender.send(ConsensusStateResponse::MinimumValidatorCount(value)); diff --git a/finalizer/src/db.rs b/finalizer/src/db.rs index 80e951dd..23708402 100644 --- a/finalizer/src/db.rs +++ b/finalizer/src/db.rs @@ -15,6 +15,27 @@ use tracing::error; pub use db::Config; +/// Shared by startup recovery (before P2P allocation) and the finalizer actor. +pub fn config( + prefix: &str, + page_cache: commonware_runtime::buffer::paged::CacheRef, +) -> Config { + Config { + log: commonware_storage::journal::contiguous::variable::Config { + partition: format!("{prefix}-finalizer_state-log"), + write_buffer: commonware_utils::NZUsize!(1024 * 1024), + replay_buffer: commonware_utils::NZUsize!(1024 * 1024), + compression: None, + codec_config: ((), ()), + items_per_section: commonware_utils::NZU64!(262_144), + page_cache, + }, + translator: EightCap, + init_cache_size: Some(commonware_utils::NZUsize!(1024)), + init_buffer: commonware_utils::NZUsize!(1024 * 1024), + } +} + // Key prefixes for different data types const STATE_PREFIX: u8 = 0x01; const CONSENSUS_STATE_PREFIX: u8 = 0x05; @@ -25,9 +46,22 @@ const FINALIZED_HEADER_PREFIX: u8 = 0x07; const LATEST_CONSENSUS_STATE_EPOCH_KEY: [u8; 2] = [STATE_PREFIX, 0]; const LATEST_FINALIZED_HEADER_EPOCH_KEY: [u8; 2] = [STATE_PREFIX, 1]; const LATEST_CHECKPOINT_EPOCH_KEY: [u8; 2] = [STATE_PREFIX, 2]; +const CHECKPOINT_IMPORT_KEY: [u8; 2] = [STATE_PREFIX, 3]; +const PENDING_IMPORT_STATE_KEY: [u8; 2] = [STATE_PREFIX, 4]; +const PENDING_IMPORT_RECORD_KEY: [u8; 2] = [STATE_PREFIX, 5]; + +/// Durable authorization for the syncer to skip history covered by an import. +/// Retained across restarts, including when checkpoint files are no longer supplied. +#[derive(Clone, Debug)] +pub struct CheckpointImport { + pub processed_height: u64, + pub config_digest: [u8; 32], + pub finalized_header: FinalizedHeader>, + pub last_block: Option, +} pub struct FinalizerState { - store: Db, Value, EightCap>, + store: Option, Value, EightCap>>, cancellation_token: CancellationToken, } @@ -42,7 +76,7 @@ impl FinalizerState FinalizerState anyhow::Result<()> { + anyhow::ensure!( + !self.cancellation_token.is_cancelled(), + "finalizer database recovery failed" + ); + Ok(()) + } + + fn store(&self) -> &Db, Value, EightCap> { + self.store + .as_ref() + .expect("finalizer store unavailable after interrupted mutation") + } + + async fn update( + &mut self, + key: FixedBytes<64>, + value: Option>, + op: &str, + ) -> anyhow::Result<()> { + let Some(store) = self.store.take() else { + return Err(self.handle_db_error("store unavailable", op)); + }; + match store.apply_batch([(key, value)].into()).await { + Ok((store, _)) => { + self.store = Some(store); + Ok(()) + } + Err(e) => Err(self.handle_db_error(e, op)), + } + } + fn pad_key(key: &[u8]) -> FixedBytes<64> { let mut padded = [0u8; 64]; let len = key.len().min(64); @@ -92,7 +158,7 @@ impl FinalizerState u64 { let key = Self::pad_key(&LATEST_CONSENSUS_STATE_EPOCH_KEY); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::U64(epoch))) => epoch, Ok(_) => 0, Err(e) => { @@ -104,20 +170,18 @@ impl FinalizerState anyhow::Result<()> { let key = Self::pad_key(&LATEST_CONSENSUS_STATE_EPOCH_KEY); - if let Err(e) = self - .store - .apply_batch([(key, Some(Value::U64(epoch)))].into()) - .await - { - return Err(self.handle_db_error(e, "set_latest_consensus_state_epoch")); - } - Ok(()) + self.update( + key, + Some(Value::U64(epoch)), + "set_latest_consensus_state_epoch", + ) + .await } // FinalizedHeader epoch tracking operations async fn get_latest_finalized_header_epoch(&self) -> u64 { let key = Self::pad_key(&LATEST_FINALIZED_HEADER_EPOCH_KEY); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::U64(epoch))) => epoch, Ok(_) => 0, Err(e) => { @@ -129,20 +193,18 @@ impl FinalizerState anyhow::Result<()> { let key = Self::pad_key(&LATEST_FINALIZED_HEADER_EPOCH_KEY); - if let Err(e) = self - .store - .apply_batch([(key, Some(Value::U64(epoch)))].into()) - .await - { - return Err(self.handle_db_error(e, "set_latest_finalized_header_epoch")); - } - Ok(()) + self.update( + key, + Some(Value::U64(epoch)), + "set_latest_finalized_header_epoch", + ) + .await } // Checkpoint epoch tracking operations async fn get_latest_checkpoint_epoch(&self) -> u64 { let key = Self::pad_key(&LATEST_CHECKPOINT_EPOCH_KEY); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::U64(epoch))) => epoch, Ok(_) => 0, Err(e) => { @@ -154,14 +216,8 @@ impl FinalizerState anyhow::Result<()> { let key = Self::pad_key(&LATEST_CHECKPOINT_EPOCH_KEY); - if let Err(e) = self - .store - .apply_batch([(key, Some(Value::U64(epoch)))].into()) + self.update(key, Some(Value::U64(epoch)), "set_latest_checkpoint_epoch") .await - { - return Err(self.handle_db_error(e, "set_latest_checkpoint_epoch")); - } - Ok(()) } // ConsensusState blob operations @@ -171,13 +227,12 @@ impl FinalizerState anyhow::Result<()> { let key = Self::make_consensus_state_key(epoch); - if let Err(e) = self - .store - .apply_batch([(key, Some(Value::ConsensusState(Box::new(state.clone()))))].into()) - .await - { - return Err(self.handle_db_error(e, "store_consensus_state")); - } + self.update( + key, + Some(Value::ConsensusState(Box::new(state.clone()))), + "store_consensus_state", + ) + .await?; // Update the latest epoch tracker let current_latest = self.get_latest_consensus_state_epoch().await; @@ -189,7 +244,7 @@ impl FinalizerState Option { let key = Self::make_consensus_state_key(epoch); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::ConsensusState(state))) => Some(*state), Ok(_) => None, Err(e) => { @@ -201,7 +256,7 @@ impl FinalizerState Option { let key = Self::pad_key(&LATEST_CONSENSUS_STATE_EPOCH_KEY); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::U64(latest_epoch))) => self.get_consensus_state(latest_epoch).await, Ok(_) => None, Err(e) => { @@ -212,13 +267,13 @@ impl FinalizerState FinalizerState anyhow::Result<()> { let key = Self::make_checkpoint_key(epoch); - if let Err(e) = self - .store - .apply_batch( - [( - key, - Some(Value::Checkpoint(Box::new(( - checkpoint.clone(), - last_block, - )))), - )] - .into(), - ) - .await - { - return Err(self.handle_db_error(e, "store_finalized_checkpoint")); - } + self.update( + key, + Some(Value::Checkpoint(Box::new(( + checkpoint.clone(), + last_block, + )))), + "store_finalized_checkpoint", + ) + .await?; // Update the latest checkpoint epoch tracker let current_latest = self.get_latest_checkpoint_epoch().await; @@ -258,7 +306,7 @@ impl FinalizerState Option<(Checkpoint, Block)> { let key = Self::make_checkpoint_key(epoch); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::Checkpoint(checkpoint))) => Some(*checkpoint), Ok(_) => None, Err(e) => { @@ -281,13 +329,12 @@ impl FinalizerState>, ) -> anyhow::Result<()> { let key = Self::make_finalized_header_key(epoch); - if let Err(e) = self - .store - .apply_batch([(key, Some(Value::FinalizedHeader(Box::new(header.clone()))))].into()) - .await - { - return Err(self.handle_db_error(e, "store_finalized_header")); - } + self.update( + key, + Some(Value::FinalizedHeader(Box::new(header.clone()))), + "store_finalized_header", + ) + .await?; // Update the latest finalized header epoch tracker let current_latest = self.get_latest_finalized_header_epoch().await; @@ -303,7 +350,7 @@ impl FinalizerState Option>> { let key = Self::make_finalized_header_key(epoch); - match self.store.get(&key).await { + match self.store().get(&key).await { Ok(Some(Value::FinalizedHeader(header))) => Some(*header), Ok(_) => None, Err(e) => { @@ -320,18 +367,133 @@ impl FinalizerState anyhow::Result>> { + match self + .store() + .get(&Self::pad_key(&CHECKPOINT_IMPORT_KEY)) + .await + { + Ok(Some(Value::CheckpointImport(record))) => Ok(Some(*record)), + Ok(None) => Ok(None), + Ok(_) => Err(self.handle_db_error("wrong import record type", "get_checkpoint_import")), + Err(e) => Err(self.handle_db_error(e, "get_checkpoint_import")), + } + } + + /// Inspect staged artifacts for startup conflict checks; not skip authorization. + pub async fn get_pending_import( + &self, + ) -> anyhow::Result)>> { + let state = self + .store() + .get(&Self::pad_key(&PENDING_IMPORT_STATE_KEY)) + .await + .map_err(|e| self.handle_db_error(e, "get_pending_import"))?; + let record = self + .store() + .get(&Self::pad_key(&PENDING_IMPORT_RECORD_KEY)) + .await + .map_err(|e| self.handle_db_error(e, "get_pending_import"))?; + match (state, record) { + (None, None) => Ok(None), + (Some(Value::ConsensusState(state)), Some(Value::CheckpointImport(record))) => { + Ok(Some((*state, *record))) + } + _ => Err(self.handle_db_error("incomplete pending import", "get_pending_import")), + } + } + + pub(crate) async fn stage_checkpoint_import( + &mut self, + state: &ConsensusState, + record: &CheckpointImport, + ) -> anyhow::Result<()> { + self.commit_updates(vec![ + ( + Self::pad_key(&PENDING_IMPORT_STATE_KEY), + Some(Value::ConsensusState(Box::new(state.clone()))), + ), + ( + Self::pad_key(&PENDING_IMPORT_RECORD_KEY), + Some(Value::CheckpointImport(Box::new(record.clone()))), + ), + ]) + .await + } + + pub(crate) async fn discard_pending_import(&mut self) -> anyhow::Result<()> { + self.commit_updates(vec![ + (Self::pad_key(&PENDING_IMPORT_STATE_KEY), None), + (Self::pad_key(&PENDING_IMPORT_RECORD_KEY), None), + ]) + .await + } + + async fn commit_updates( + &mut self, + updates: Vec<(FixedBytes<64>, Option>)>, + ) -> anyhow::Result<()> { + let store = self + .store + .take() + .ok_or_else(|| self.handle_db_error("store unavailable", "checkpoint import"))?; + let (store, _) = store + .apply_batch(updates.into_iter().collect()) + .await + .map_err(|e| self.handle_db_error(e, "checkpoint import"))?; + self.store = Some(store); + self.commit().await + } + + /// Publish state, latest-state pointer, and skip authorization in one journal + /// batch, then sync. QMDB recovery rewinds incomplete/uncommitted batches. + pub(crate) async fn import_checkpoint( + &mut self, + state: &ConsensusState, + record: CheckpointImport, + ) -> anyhow::Result<()> { + anyhow::ensure!( + state.get_latest_height() == record.processed_height, + "import height mismatch" + ); + self.commit_updates(vec![ + ( + Self::make_consensus_state_key(state.get_epoch()), + Some(Value::ConsensusState(Box::new(state.clone()))), + ), + ( + Self::pad_key(&LATEST_CONSENSUS_STATE_EPOCH_KEY), + Some(Value::U64(state.get_epoch())), + ), + ( + Self::pad_key(&CHECKPOINT_IMPORT_KEY), + Some(Value::CheckpointImport(Box::new(record))), + ), + (Self::pad_key(&PENDING_IMPORT_STATE_KEY), None), + (Self::pad_key(&PENDING_IMPORT_RECORD_KEY), None), + ]) + .await + } + // Commit all pending changes to the database pub async fn commit(&mut self) -> anyhow::Result<()> { - if let Err(e) = self.store.commit().await { - return Err(self.handle_db_error(e, "commit")); + let Some(store) = self.store.take() else { + return Err(self.handle_db_error("store unavailable", "commit")); + }; + match store.commit().await { + Ok(store) => { + self.store = Some(store); + Ok(()) + } + Err(e) => Err(self.handle_db_error(e, "commit")), } - Ok(()) } } #[derive(Clone)] enum Value { U64(u64), + CheckpointImport(Box>), ConsensusState(Box), Checkpoint(Box<(Checkpoint, Block)>), FinalizedHeader(Box>>), @@ -341,6 +503,9 @@ impl EncodeSize for Value { fn encode_size(&self) -> usize { 1 + match self { Self::U64(_) => 8, + Self::CheckpointImport(record) => { + 8 + 32 + record.finalized_header.encode_size() + record.last_block.encode_size() + } Self::ConsensusState(state) => state.encode_size(), Self::Checkpoint(checkpoint) => checkpoint.encode_size(), Self::FinalizedHeader(header) => header.encode_size(), @@ -357,6 +522,18 @@ impl Read for Value { 0x01 => Ok(Self::U64( buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?, )), + 0x08 => { + let processed_height = buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?; + let mut config_digest = [0; 32]; + buf.try_copy_to_slice(&mut config_digest) + .map_err(|_| Error::EndOfBuffer)?; + Ok(Self::CheckpointImport(Box::new(CheckpointImport { + processed_height, + config_digest, + finalized_header: FinalizedHeader::read_cfg(buf, &())?, + last_block: Option::::read_cfg(buf, &())?, + }))) + } 0x05 => Ok(Self::ConsensusState(Box::new(ConsensusState::read_cfg( buf, &(), @@ -382,6 +559,13 @@ impl Write for Value { buf.put_u8(0x01); buf.put_u64(*val); } + Self::CheckpointImport(record) => { + buf.put_u8(0x08); + buf.put_u64(record.processed_height); + buf.put_slice(&record.config_digest); + record.finalized_header.write(buf); + record.last_block.write(buf); + } Self::ConsensusState(state) => { buf.put_u8(0x05); state.write(buf); @@ -450,6 +634,7 @@ mod tests { log: commonware_storage::journal::contiguous::variable::Config { partition: format!("{}-log", partition), write_buffer: NZUsize!(64 * 1024), + replay_buffer: NZUsize!(64 * 1024), compression: None, codec_config: ((), ()), items_per_section: NZU64!(4), @@ -461,6 +646,7 @@ mod tests { }, translator: EightCap, init_cache_size: Some(NZUsize!(1024)), + init_buffer: NZUsize!(64 * 1024), }; FinalizerState::::new(context, config, CancellationToken::new()).await } @@ -563,7 +749,7 @@ mod tests { let finalized = Finalization { proposal, certificate: BlsCertificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), // Valid dummy signature for test }, }; @@ -613,7 +799,7 @@ mod tests { let finalized2 = Finalization { proposal: proposal2, certificate: BlsCertificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -678,7 +864,7 @@ mod tests { let finalized1 = Finalization { proposal: proposal1, certificate: BlsCertificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -708,7 +894,7 @@ mod tests { let finalized3 = Finalization { proposal: proposal3, certificate: BlsCertificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -738,7 +924,7 @@ mod tests { let finalized2 = Finalization { proposal: proposal2, certificate: BlsCertificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; diff --git a/finalizer/src/lib.rs b/finalizer/src/lib.rs index 37eba028..8d284f85 100644 --- a/finalizer/src/lib.rs +++ b/finalizer/src/lib.rs @@ -4,6 +4,7 @@ pub mod config; pub use config::*; pub mod actor; pub mod db; +pub mod startup; #[cfg(test)] mod tests; diff --git a/finalizer/src/startup.rs b/finalizer/src/startup.rs new file mode 100644 index 00000000..b71a1eee --- /dev/null +++ b/finalizer/src/startup.rs @@ -0,0 +1,320 @@ +//! Checkpoint promotion before network allocation or actor startup. +//! +//! The caller must enforce the checkpoint trust policy (by default, verifying the +//! finalized-header chain against genesis and weak subjectivity), and check all +//! supplied history against local finalized headers. A terminal signature under +//! the checkpoint's own committee is not a trust root. +use crate::db::{CheckpointImport, FinalizerState}; +use anyhow::{Result, bail, ensure}; +use commonware_codec::Encode; +use commonware_cryptography::bls12381::primitives::variant::{MinPk, Variant}; +use commonware_parallel::Sequential; +use commonware_runtime::{BufferPooler, Clock, Metrics, Storage}; +use commonware_utils::{TryCollect, ordered::BiMap, sys_rng}; +use summit_types::{ + Block, EngineClient, FinalizedHeader, PublicKey, chain_domain, checkpoint::Checkpoint, + consensus_state::ConsensusState, scheme::MultisigScheme, +}; +use tracing::{info, warn}; + +type Import = CheckpointImport; + +/// Check all overlapping finalized epochs, not just the supplied terminal. +/// Certificate bytes may differ for a same-digest later-view reproposal. +pub async fn check_history( + db: &FinalizerState, + headers: &[FinalizedHeader], +) -> Result<()> { + let imported = db.get_checkpoint_import().await?; + let pending = db.get_pending_import().await?.map(|(_, record)| record); + for header in headers { + if let Some(local) = db.get_finalized_header(header.header().epoch()).await { + ensure!( + local.header().computed_digest() == header.header().computed_digest(), + "checkpoint conflicts with local finalized history" + ); + } + if let Some(previous) = header.header().epoch().checked_sub(1) + && let Some(local) = db.get_finalized_header(previous).await + { + ensure!( + header.header().prev_epoch_header_hash() == local.header().computed_digest(), + "checkpoint does not extend local finalized history" + ); + } + for local in imported.iter().chain(pending.iter()) { + if local.finalized_header.header().epoch() == header.header().epoch() { + ensure!( + local.finalized_header.header().computed_digest() + == header.header().computed_digest(), + "checkpoint conflicts with imported history" + ); + } else if local.finalized_header.header().epoch().checked_add(1) + == Some(header.header().epoch()) + { + ensure!( + header.header().prev_epoch_header_hash() + == local.finalized_header.header().computed_digest(), + "checkpoint does not extend imported history" + ); + } + } + } + db.ensure_healthy() +} + +fn validate_candidate( + state: ConsensusState, + header: FinalizedHeader, + last_block: Option, + config_digest: [u8; 32], +) -> Result<(ConsensusState, Import)> { + // Production restores the pending-checkpoint and captured proof fields; + // internal callers can also supply the raw decoded checkpoint state. + let raw = state + .get_pending_checkpoint() + .cloned() + .unwrap_or_else(|| Checkpoint::new(&state)); + let decoded = ConsensusState::try_from(&raw)?; + let mut restored = decoded.clone(); + restored.set_pending_checkpoint(Some(raw.clone())); + restored.capture_state_root(restored.get_latest_height()); + ensure!( + state.encode() == decoded.encode() || state.encode() == restored.encode(), + "checkpoint state differs from its authenticated snapshot" + ); + let height = restored.get_latest_height(); + ensure!( + height.checked_add(1) == Some(header.header().height()), + "checkpoint terminal is not the state's successor" + ); + ensure!( + summit_types::utils::is_last_block_of_epoch( + restored.get_epocher(), + header.header().height() + ), + "checkpoint anchor is not epoch-terminal" + ); + ensure!( + header.header().epoch() == restored.get_epoch(), + "checkpoint epoch mismatch" + ); + ensure!( + header.header().parent() == restored.get_head_digest(), + "checkpoint parent mismatch" + ); + ensure!( + header.header().checkpoint_hash() == raw.digest, + "checkpoint hash mismatch" + ); + ensure!( + header.header().computed_digest() == header.finalization().proposal.payload, + "checkpoint header/certificate mismatch" + ); + ensure!( + header.finalization().proposal.round.epoch().get() == restored.get_epoch() + && header.finalization().proposal.round.view().get() >= header.header().view(), + "checkpoint certificate round mismatch" + ); + let participants: BiMap::Public> = restored + .get_current_epoch_validators() + .into_iter() + .map(|(node, key)| { + let key: &::Public = key.as_ref(); + (node, *key) + }) + .try_collect() + .map_err(|e| anyhow::anyhow!("invalid checkpoint committee: {e:?}"))?; + let scheme = MultisigScheme::verifier(&chain_domain(config_digest), participants); + ensure!( + header + .finalization() + .verify(&mut sys_rng(), &scheme, &Sequential), + "checkpoint terminal signature verification failed" + ); + if let Some(block) = &last_block { + ensure!( + block.digest() == header.finalization().proposal.payload, + "checkpoint last_block mismatch" + ); + Block::new_with_verify( + block.header.clone(), + block.payload.clone(), + block.execution_requests.clone(), + )?; + } + Ok(( + restored, + Import { + processed_height: height, + config_digest, + finalized_header: header, + last_block, + }, + )) +} + +async fn finish_import( + db: &mut FinalizerState, + client: &mut C, + state: &ConsensusState, + record: Import, +) -> Result<()> { + // Pending state is already durable. A transport error may mean Reth applied + // forkchoice but its response was lost: retain the record for restart/retry. + let status = client.commit_hash(*state.get_forkchoice()).await?; + if !status.is_valid() && !status.is_syncing() { + db.discard_pending_import().await?; + bail!("execution client rejected checkpoint: {status:?}"); + } + if status.is_syncing() { + warn!( + "checkpoint execution head is SYNCING, not yet validated; normal execution retries remain required" + ); + } + db.import_checkpoint(state, record).await +} + +/// Select the finalizer's startup state and durably prepare any checkpoint import. +/// +/// The returned state determines where syncer replay begins, whether loaded from +/// storage, imported from a checkpoint, or taken from `initial` on an empty database. +/// The `initial` fallback is not persisted here. The optional import record provides +/// authenticated terminal artifacts for syncer seeding or fetching; an ordinary +/// finalizer height does not imply a checkpoint import. +#[allow(clippy::too_many_arguments)] +pub async fn prepare( + db: &mut FinalizerState, + client: &mut C, + initial: ConsensusState, + header: Option>, + last_block: Option, + config_digest: [u8; 32], +) -> Result<(ConsensusState, Option)> { + // Validate supplied artifacts even if they turn out to be older than storage. + let candidate = match header { + Some(header) => Some(validate_candidate( + initial.clone(), + header, + last_block, + config_digest, + )?), + None => { + ensure!( + last_block.is_none(), + "checkpoint last_block requires a verified terminal header" + ); + None + } + }; + if let Some((_, record)) = &candidate { + check_history(db, std::slice::from_ref(&record.finalized_header)).await?; + } + let mut execution_checked = false; + let mut stored = db.get_latest_consensus_state().await; + let mut imported = db.get_checkpoint_import().await?; + db.ensure_healthy()?; + if let Some(record) = &imported { + ensure!( + record.config_digest == config_digest, + "stored checkpoint belongs to a different chain" + ); + ensure!( + stored + .as_ref() + .is_some_and(|s| s.get_latest_height() >= record.processed_height), + "import authorization has no backing state" + ); + } + if let Some((pending, record)) = db.get_pending_import().await? { + ensure!( + record.config_digest == config_digest, + "pending checkpoint belongs to a different chain" + ); + ensure!( + pending.get_latest_height() == record.processed_height, + "pending import height mismatch" + ); + ensure!( + stored + .as_ref() + .is_none_or(|s| s.get_latest_height() <= record.processed_height), + "pending import is behind durable state" + ); + let (pending, record) = validate_candidate( + pending, + record.finalized_header, + record.last_block, + config_digest, + )?; + if let Some((_, candidate)) = &candidate + && candidate.processed_height == record.processed_height + { + ensure!( + candidate.finalized_header.header().computed_digest() + == record.finalized_header.header().computed_digest(), + "checkpoint conflicts with pending import" + ); + } + finish_import(db, client, &pending, record.clone()).await?; + execution_checked = true; + stored = Some(pending); + imported = Some(record); + } + if let Some((candidate, record)) = candidate { + let height = candidate.get_latest_height(); + match stored.as_ref() { + Some(local) if local.get_latest_height() > height => { + warn!( + checkpoint_height = height, + stored_height = local.get_latest_height(), + "ignoring older checkpoint without rollback" + ); + } + Some(local) if local.get_latest_height() == height => { + ensure!( + local.get_head_digest() == candidate.get_head_digest() + && local.ssz_tree().root() == candidate.ssz_tree().root(), + "checkpoint conflicts with state at the same height" + ); + // A matching checkpoint can authorize a skip even if this height + // was originally reached through normal block execution. + if imported + .as_ref() + .is_none_or(|old| old.processed_height < height) + { + db.stage_checkpoint_import(&candidate, &record).await?; + finish_import(db, client, &candidate, record.clone()).await?; + execution_checked = true; + imported = Some(record); + stored = Some(candidate); + } + } + _ => { + db.stage_checkpoint_import(&candidate, &record).await?; + finish_import(db, client, &candidate, record.clone()).await?; + execution_checked = true; + info!(height, "checkpoint state and skip authorization committed"); + imported = Some(record); + stored = Some(candidate); + } + } + } else if let Some(local) = &stored { + ensure!( + initial.get_latest_height() <= local.get_latest_height(), + "newer initial state requires a verified checkpoint" + ); + } + let state = stored.unwrap_or(initial); + // Also validate the current execution head before handing a recovered import + // authorization to the syncer. Explicit INVALID never permits startup. + if imported.is_some() && !execution_checked { + let status = client.commit_hash(*state.get_forkchoice()).await?; + ensure!( + status.is_valid() || status.is_syncing(), + "execution client rejected recovered checkpoint state: {status:?}" + ); + } + db.ensure_healthy()?; + Ok((state, imported)) +} diff --git a/finalizer/src/tests/checkpoint_import.rs b/finalizer/src/tests/checkpoint_import.rs new file mode 100644 index 00000000..6a22338b --- /dev/null +++ b/finalizer/src/tests/checkpoint_import.rs @@ -0,0 +1,530 @@ +use super::mocks::{MockEngineClient, make_finalization}; +use crate::{ + db::{self, CheckpointImport, FinalizerState}, + startup, +}; +use alloy_primitives::Address; +use alloy_rpc_types_engine::ForkchoiceState; +use commonware_codec::{DecodeExt, Encode}; +use commonware_cryptography::bls12381::primitives::{ + group, + variant::{MinPk, Variant}, +}; +use commonware_cryptography::{Signer as _, bls12381, ed25519}; +use commonware_runtime::{ + Runner as _, Supervisor as _, + buffer::paged::CacheRef, + deterministic::{self, Runner}, +}; +use commonware_utils::{NZU16, NZU64, NZUsize, TryCollect, ordered::BiMap}; +use summit_types::{ + Block, FinalizedHeader, PublicKey, + account::{ValidatorAccount, ValidatorStatus}, + chain_domain, + checkpoint::Checkpoint, + consensus_state::ConsensusState, + scheme::MultisigScheme, +}; +use tokio_util::sync::CancellationToken; + +const DOMAIN: [u8; 32] = [7; 32]; + +fn checkpoint() -> (ConsensusState, Block, FinalizedHeader) { + let node = ed25519::PrivateKey::from_seed(1).public_key(); + let key = bls12381::PrivateKey::from_seed(2); + let public = key.public_key(); + let mut state = ConsensusState::new( + ForkchoiceState { + head_block_hash: [1; 32].into(), + safe_block_hash: [1; 32].into(), + finalized_block_hash: [1; 32].into(), + }, + 32_000_000_000, + NZU64!(10), + 1000, + Address::ZERO, + 100, + 100, + 16, + 128, + 1, + 0, + 3, + ); + state.set_validator_accounts(std::collections::BTreeMap::from([( + node.as_ref().try_into().unwrap(), + ValidatorAccount { + consensus_public_key: public.clone(), + withdrawal_credentials: Address::ZERO, + balance: 32_000_000_000, + status: ValidatorStatus::Active, + joining_epoch: 0, + last_deposit_index: 0, + }, + )])); + state.set_latest_height(8); + state.set_view(8); + state.set_head_digest([8; 32].into()); + state.capture_state_root(8); + let raw = Checkpoint::new(&state); + state.set_pending_checkpoint(Some(raw.clone())); + state.capture_state_root(8); + let mut payload = Block::genesis([1; 32]).payload; + payload.payload_inner.payload_inner.block_number = 9; + payload.payload_inner.payload_inner.block_hash = [9; 32].into(); + payload.payload_inner.payload_inner.parent_hash = [1; 32].into(); + let block = Block::compute_digest( + state.get_head_digest(), + 9, + 90, + payload, + vec![], + 0, + 9, + Some(raw.digest), + DOMAIN.into(), + vec![], + vec![], + state.get_state_root(), + ); + let group_public: &::Public = public.as_ref(); + let participants: BiMap::Public> = [(node, group_public.clone())] + .into_iter() + .try_collect() + .unwrap(); + let scheme = MultisigScheme::signer( + &chain_domain(DOMAIN), + participants, + group::Private::decode(key.encode()).unwrap(), + ) + .unwrap(); + let certificate = make_finalization(block.digest(), 9, 8, &[scheme], 1); + let header = FinalizedHeader::new(block.header.clone(), certificate, 1).unwrap(); + (state, block, header) +} + +async fn open(context: deterministic::Context) -> FinalizerState { + let cache = CacheRef::from_pooler(&context, NZU16!(4096), NZUsize!(16)); + FinalizerState::new( + context, + db::config("import-test", cache), + CancellationToken::new(), + ) + .await +} + +async fn seed(db: &mut FinalizerState, height: u64) { + let mut state = ConsensusState::default(); + state.set_latest_height(height); + db.store_consensus_state(state.get_epoch(), &state) + .await + .unwrap(); + db.commit().await.unwrap(); +} + +fn record( + block: Option, + header: FinalizedHeader, +) -> CheckpointImport { + CheckpointImport { + processed_height: 8, + config_digest: DOMAIN, + finalized_header: header, + last_block: block, + } +} + +#[test] +fn newer_checkpoint_replaces_nonempty_state_and_survives_without_files() { + let (_, recovered) = Runner::default().start_and_recover(|context| async move { + let mut db = open(context).await; + seed(&mut db, 3).await; + let (state, block, header) = checkpoint(); + let (selected, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state, + Some(header), + Some(block), + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(selected.get_latest_height(), 8); + assert_eq!(imported.unwrap().processed_height, 8); + assert!(db.get_pending_import().await.unwrap().is_none()); + }); + Runner::from(recovered).start(|context| async move { + let mut db = open(context).await; + let (state, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + ConsensusState::default(), + None, + None, + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(state.get_latest_height(), 8); + assert_eq!(state.get_observers_per_validator(), 16); + assert_eq!(imported.unwrap().last_block.unwrap().height(), 9); + }); +} + +#[test] +fn repeated_checkpoint_import_is_idempotent_and_old_checkpoint_cannot_roll_back() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + let (candidate, block, header) = checkpoint(); + let (mut state, _) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + candidate.clone(), + Some(header.clone()), + Some(block.clone()), + DOMAIN, + ) + .await + .unwrap(); + let (again, _) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + candidate.clone(), + Some(header.clone()), + Some(block.clone()), + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(state.encode(), again.encode()); + state.set_latest_height(12); + db.store_consensus_state(0, &state).await.unwrap(); + db.commit().await.unwrap(); + let (selected, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + candidate, + Some(header), + Some(block), + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(selected.get_latest_height(), 12); + assert_eq!(imported.unwrap().processed_height, 8); + }); +} + +#[test] +fn older_checkpoint_without_prior_import_does_not_authorize_a_skip() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + seed(&mut db, 20).await; + let (state, block, header) = checkpoint(); + let (selected, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state, + Some(header), + Some(block), + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(selected.get_latest_height(), 20); + assert!(imported.is_none()); + }); +} + +#[test] +fn conflicting_same_height_state_is_rejected() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + seed(&mut db, 8).await; + let (state, block, header) = checkpoint(); + assert!( + startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state, + Some(header), + Some(block), + DOMAIN + ) + .await + .unwrap_err() + .to_string() + .contains("same height") + ); + assert!(db.get_checkpoint_import().await.unwrap().is_none()); + assert!(db.get_pending_import().await.unwrap().is_none()); + }); +} + +#[test] +fn invalid_older_artifacts_are_not_silently_ignored() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + seed(&mut db, 20).await; + let (mut state, block, header) = checkpoint(); + state.set_observers_per_validator(17); + assert!( + startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state, + Some(header), + Some(block), + DOMAIN + ) + .await + .is_err() + ); + assert_eq!( + db.get_latest_consensus_state() + .await + .unwrap() + .get_latest_height(), + 20 + ); + }); +} + +#[test] +fn wrong_domain_or_block_is_rejected_before_staging() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + seed(&mut db, 3).await; + let (state, block, header) = checkpoint(); + assert!( + startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state.clone(), + Some(header.clone()), + Some(block), + [6; 32] + ) + .await + .is_err() + ); + assert!( + startup::prepare( + &mut db, + &mut MockEngineClient::new(), + state, + Some(header), + Some(Block::genesis([2; 32])), + DOMAIN + ) + .await + .is_err() + ); + assert!(db.get_pending_import().await.unwrap().is_none()); + }); +} + +#[test] +fn execution_invalid_rejects_import_and_retains_old_state() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + seed(&mut db, 3).await; + let mut client = MockEngineClient::new(); + client.queue_commit_hash_invalid(1); + let (state, block, header) = checkpoint(); + assert!( + startup::prepare( + &mut db, + &mut client, + state, + Some(header), + Some(block), + DOMAIN + ) + .await + .is_err() + ); + assert_eq!( + db.get_latest_consensus_state() + .await + .unwrap() + .get_latest_height(), + 3 + ); + assert!(db.get_pending_import().await.unwrap().is_none()); + assert!(db.get_checkpoint_import().await.unwrap().is_none()); + }); +} + +#[test] +fn execution_transport_error_leaves_recoverable_pending_import() { + let (_, recovered) = Runner::default().start_and_recover(|context| async move { + let mut db = open(context).await; + seed(&mut db, 3).await; + let mut client = MockEngineClient::new(); + client.fail_commit_hash(); + let (state, block, header) = checkpoint(); + assert!( + startup::prepare( + &mut db, + &mut client, + state, + Some(header), + Some(block), + DOMAIN + ) + .await + .is_err() + ); + assert!(db.get_pending_import().await.unwrap().is_some()); + assert!(db.get_checkpoint_import().await.unwrap().is_none()); + assert_eq!( + db.get_latest_consensus_state() + .await + .unwrap() + .get_latest_height(), + 3 + ); + }); + Runner::from(recovered).start(|context| async move { + let mut db = open(context).await; + let (state, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + ConsensusState::default(), + None, + None, + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(state.get_latest_height(), 8); + assert!(imported.is_some()); + assert!(db.get_pending_import().await.unwrap().is_none()); + }); +} + +#[test] +fn restart_resumes_after_reth_acceptance_before_state_publication() { + use summit_types::EngineClient as _; + let (_, recovered) = Runner::default().start_and_recover(|context| async move { + let mut db = open(context).await; + seed(&mut db, 3).await; + let (state, block, header) = checkpoint(); + db.stage_checkpoint_import(&state, &record(Some(block), header)) + .await + .unwrap(); + assert!( + MockEngineClient::new() + .commit_hash(*state.get_forkchoice()) + .await + .unwrap() + .is_valid() + ); + // Crash here: Reth has accepted, but no canonical-state/skip publication. + assert_eq!( + db.get_latest_consensus_state() + .await + .unwrap() + .get_latest_height(), + 3 + ); + assert!(db.get_checkpoint_import().await.unwrap().is_none()); + }); + Runner::from(recovered).start(|context| async move { + let mut db = open(context).await; + let (state, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + ConsensusState::default(), + None, + None, + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(state.get_latest_height(), 8); + assert_eq!(imported.unwrap().processed_height, 8); + }); +} + +#[test] +fn missing_terminal_block_is_retained_as_a_fetchable_import() { + Runner::default().start(|context| async move { + let mut db = open(context).await; + let (state, _, header) = checkpoint(); + let mut client = MockEngineClient::new(); + client.queue_commit_hash_syncing(2); + let (state, imported) = + startup::prepare(&mut db, &mut client, state, Some(header), None, DOMAIN) + .await + .unwrap(); + assert_eq!(state.get_latest_height(), 8); + assert!(imported.unwrap().last_block.is_none()); + }); +} + +#[test] +fn failed_staging_never_calls_execution_or_returns_skip_authorization() { + Runner::default().start(|context| async move { + let mut db = open(context.child("db")).await; + seed(&mut db, 3).await; + let mut client = MockEngineClient::new(); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); + let (state, block, header) = checkpoint(); + assert!( + startup::prepare( + &mut db, + &mut client, + state, + Some(header), + Some(block), + DOMAIN + ) + .await + .is_err() + ); + assert_eq!(client.commit_hash_call_count(), 0); + assert!(db.ensure_healthy().is_err()); + }); +} + +#[test] +fn failed_publication_recovers_coherent_state_and_authorization() { + let (_, recovered) = Runner::default().start_and_recover(|context| async move { + let mut db = open(context.child("db")).await; + seed(&mut db, 3).await; + let (state, block, header) = checkpoint(); + let record = record(Some(block), header); + db.stage_checkpoint_import(&state, &record).await.unwrap(); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); + assert!(db.import_checkpoint(&state, record).await.is_err()); + assert!(db.ensure_healthy().is_err()); + }); + Runner::from(recovered).start(|context| async move { + context.storage_fault_config().write().sync_rate = None; + let mut db = open(context).await; + // Failed fsync may have persisted either complete batch. Neither may + // expose a committed skip paired with the pre-import canonical state. + let state = db.get_latest_consensus_state().await.unwrap(); + match db.get_checkpoint_import().await.unwrap() { + Some(record) => assert_eq!(state.get_latest_height(), record.processed_height), + None => assert!(db.get_pending_import().await.unwrap().is_some()), + } + let (state, imported) = startup::prepare( + &mut db, + &mut MockEngineClient::new(), + ConsensusState::default(), + None, + None, + DOMAIN, + ) + .await + .unwrap(); + assert_eq!(state.get_latest_height(), 8); + assert_eq!(imported.unwrap().processed_height, 8); + }); +} diff --git a/finalizer/src/tests/fork_handling.rs b/finalizer/src/tests/fork_handling.rs index d16494a0..47a81742 100644 --- a/finalizer/src/tests/fork_handling.rs +++ b/finalizer/src/tests/fork_handling.rs @@ -133,6 +133,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 10, 16, 0, + 256, 3, 0, 3, diff --git a/finalizer/src/tests/mocks.rs b/finalizer/src/tests/mocks.rs index 804e9bd3..15311c70 100644 --- a/finalizer/src/tests/mocks.rs +++ b/finalizer/src/tests/mocks.rs @@ -78,7 +78,12 @@ pub fn make_finalization( .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap()) .collect(); - Finalization::from_finalizes(&schemes[0], &finalizes, &Sequential).unwrap() + Finalization::from_finalizes( + &schemes[0], + commonware_utils::non_empty![@&finalizes], + &Sequential, + ) + .unwrap() } /// Minimal mock EngineClient that accepts all blocks. @@ -286,6 +291,12 @@ impl commonware_p2p::Blocker for MockNetworkOracle { fn block(&mut self, _public_key: Self::PublicKey) -> Feedback { Feedback::Ok } + fn blocked(&mut self) -> commonware_p2p::BlockedSubscription { + let (sender, receiver) = + commonware_utils::channel::ring::channel(commonware_utils::NZUsize!(1)); + sender.send_lossy(commonware_utils::ordered::Set::default()); + receiver + } } /// A single recorded `track` call: the epoch and the peer tiers handed to the @@ -326,4 +337,10 @@ impl commonware_p2p::Blocker for RecordingNetworkOracle { fn block(&mut self, _public_key: Self::PublicKey) -> Feedback { Feedback::Ok } + fn blocked(&mut self) -> commonware_p2p::BlockedSubscription { + let (sender, receiver) = + commonware_utils::channel::ring::channel(commonware_utils::NZUsize!(1)); + sender.send_lossy(commonware_utils::ordered::Set::default()); + receiver + } } diff --git a/finalizer/src/tests/mod.rs b/finalizer/src/tests/mod.rs index 31421d11..270051ff 100644 --- a/finalizer/src/tests/mod.rs +++ b/finalizer/src/tests/mod.rs @@ -1,3 +1,4 @@ +mod checkpoint_import; mod fork_handling; mod mocks; mod state_queries; diff --git a/finalizer/src/tests/state_queries.rs b/finalizer/src/tests/state_queries.rs index 70c10949..ba32cb30 100644 --- a/finalizer/src/tests/state_queries.rs +++ b/finalizer/src/tests/state_queries.rs @@ -130,6 +130,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 10, 16, 0, + 256, 3, 0, 3, diff --git a/finalizer/src/tests/syncing.rs b/finalizer/src/tests/syncing.rs index 59dbeae1..1bed7240 100644 --- a/finalizer/src/tests/syncing.rs +++ b/finalizer/src/tests/syncing.rs @@ -120,6 +120,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 10, 16, 0, + 256, 3, 0, 3, diff --git a/finalizer/src/tests/validator_lifecycle.rs b/finalizer/src/tests/validator_lifecycle.rs index bd597a98..f4f2aa55 100644 --- a/finalizer/src/tests/validator_lifecycle.rs +++ b/finalizer/src/tests/validator_lifecycle.rs @@ -163,6 +163,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 10, 16, 0, + 256, 3, 0, 3, diff --git a/finalizer/src/tests/withdrawals.rs b/finalizer/src/tests/withdrawals.rs index 1d3be4e6..cf5feb3d 100644 --- a/finalizer/src/tests/withdrawals.rs +++ b/finalizer/src/tests/withdrawals.rs @@ -138,6 +138,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 10, 16, 0, + 256, 3, 0, 3, diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 9f038ef1..c33db271 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -4,12 +4,12 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] @@ -34,6 +34,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -344,7 +353,7 @@ checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -461,7 +470,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -478,7 +487,7 @@ dependencies = [ "proc-macro2", "quote", "sha3", - "syn 2.0.117", + "syn 2.0.119", "syn-solidity", ] @@ -494,7 +503,7 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "syn-solidity", ] @@ -608,7 +617,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -634,9 +643,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -732,7 +741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -770,7 +779,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -836,12 +845,6 @@ dependencies = [ "rand 0.8.6", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" @@ -870,7 +873,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -881,7 +884,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -898,7 +901,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -909,9 +912,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -920,14 +923,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -1063,11 +1067,10 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" dependencies = [ - "arrayref", "arrayvec", "cc", "cfg-if", @@ -1127,7 +1130,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1150,9 +1153,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -1208,17 +1211,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.1" @@ -1226,21 +1218,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", + "cipher", "cpufeatures 0.3.0", "rand_core 0.10.1", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", - "zeroize", ] [[package]] @@ -1252,7 +1244,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1284,13 +1276,13 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.7", + "block-buffer 0.12.1", + "crypto-common 0.2.2", "inout", - "zeroize", ] [[package]] @@ -1327,12 +1319,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d5ce5728ecb5285a5dd35f02a6a8e34e0828e0b38e8e632e249a3fe3f320211" - [[package]] name = "cmov" version = "0.5.4" @@ -1351,9 +1337,9 @@ dependencies = [ [[package]] name = "commonware-actor" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beac6262223b45c6843ab70b245b526b28836a8257b1759a509ba5fab24fa4a6" +checksum = "e96ef599ff4c2ec1608c9b5b3527ae76be6b48cfc71dc9c89910105febc3ec91" dependencies = [ "cfg-if", "commonware-macros", @@ -1365,9 +1351,9 @@ dependencies = [ [[package]] name = "commonware-broadcast" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2d7e56dc894ad3f55dbe5726468915492ae8e4650ad2a45c1bcfd138c2037f" +checksum = "f11e2cda843c7a2d3480ea7fa2fe6d9a7b4f897dfcef395c9b85e5bceb2db7f8" dependencies = [ "commonware-actor", "commonware-codec", @@ -1382,9 +1368,9 @@ dependencies = [ [[package]] name = "commonware-codec" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8eb7efe071cea13e8b23a0a7f584398870c401d2b2aabfb6b9ab3634ebcba3a" +checksum = "4d1e1bd11366f81e304c737c098eb0c075cef59e21daff44c3227952c2f75b49" dependencies = [ "bytes", "cfg-if", @@ -1397,21 +1383,21 @@ dependencies = [ [[package]] name = "commonware-codec-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a876bec347344381948901d0f5ec2e9443ebc8dbab99a63bc2db78c2fc2efbdd" +checksum = "54ccca449c618a38da9b867c128ec1e7be55cfe0b69b8814c25daac229ac6cc9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "commonware-coding" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409e9997c1bb3273b9570c26ddef4f252327c9bf18968d4439a83cfd1d3e5661" +checksum = "07145b2f348a8d3faa058dca684a5a37e0cede9f53ef123421c2227d40477afb" dependencies = [ "bytes", "commonware-codec", @@ -1428,9 +1414,9 @@ dependencies = [ [[package]] name = "commonware-consensus" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf15e3402e26f062695d4dd5e4b5967f601dc7303d06b9fafdb20a6485a3a0a" +checksum = "e2af83244a9e1982617148e577a6f692b22f4bf727976cddf978d4afcd73a4e7" dependencies = [ "bytes", "cfg-if", @@ -1459,9 +1445,9 @@ dependencies = [ [[package]] name = "commonware-cryptography" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b842f31e1aaa1af284a1f9dc4f1234761f72d07e70d53cf3f1521b0870ade094" +checksum = "df6b593210847c596c6657f2fb033c57ac0ef2f7ce68559c0e79040a34b65b7e" dependencies = [ "ahash", "anyhow", @@ -1477,15 +1463,15 @@ dependencies = [ "commonware-math", "commonware-parallel", "commonware-utils", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "crc-fast", - "ctutils 0.3.1", + "ctutils", "curve25519-dalek 5.0.0", "ecdsa 0.17.0", "fixedbitset", "getrandom 0.2.17", "getrandom 0.4.3", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-rational", "num-traits", "once_cell", @@ -1501,9 +1487,9 @@ dependencies = [ [[package]] name = "commonware-formatting" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab01b7f2798e29b0a4f7b47da5b7f3c106881578af3e69e8d2ee9d89d7c1d4fd" +checksum = "7cdfc3a5a45f024571d9abce2bdb5a52081f9f017e61b80e2bff89856361ceee" dependencies = [ "commonware-macros", "const-hex", @@ -1511,9 +1497,9 @@ dependencies = [ [[package]] name = "commonware-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e37a17d5c99ec6711098c6614af1023fd32926aa0414ec0f0613f6bb940278" +checksum = "3058bc70420515d6e92d99510ec2b2a2e1d029a6aced44d243c05ce8d60c6493" dependencies = [ "commonware-macros-impl", "tokio", @@ -1521,22 +1507,22 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d44bceee874226efca172f0e9ec9ec4eb486788ba6be1d98710f3f159b807af" +checksum = "bbcf77c39d7486af7394b1d1c4d268d08a5763036b813a2e6aba2ee825b5bd45" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", - "toml", + "syn 2.0.119", + "toml 1.1.5+spec-1.1.0", ] [[package]] name = "commonware-math" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "593fa0c5aa8bd8d350724b9b1ad4e85072845aa85a06b39ca4598998bbec1e00" +checksum = "e0227b6d4c3e264894c92c682e532128dd22c21bf835c86a6ec6ba9cb15e3f87" dependencies = [ "bytes", "commonware-codec", @@ -1548,9 +1534,9 @@ dependencies = [ [[package]] name = "commonware-p2p" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f974fcd27c0dad7ae73caeaf00c8330fd29c60f5ff1f679ccc1a51c8ed95bc" +checksum = "38bf74a0456b74a0f84e4ab23a8765227d9c52cdef63f4968670c96467b3c63a" dependencies = [ "commonware-actor", "commonware-codec", @@ -1575,9 +1561,9 @@ dependencies = [ [[package]] name = "commonware-parallel" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7109817274c671f1fb0ba17cf8aecfbb70672d29379284feeac43fa1b1dc0201" +checksum = "10bae95b6d977699c212691a996805e241936d8f958c73a1ebff0190376d1260" dependencies = [ "cfg-if", "commonware-macros", @@ -1588,9 +1574,9 @@ dependencies = [ [[package]] name = "commonware-resolver" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a497cc9e9a2cd205d96474d0d40c7731f29085cd467adbe9ff0f94719659582" +checksum = "64cde1899be3cb1ff031f144fce7236aab504e6f56378235f16bd0469e9545e3" dependencies = [ "bytes", "commonware-actor", @@ -1610,9 +1596,9 @@ dependencies = [ [[package]] name = "commonware-runtime" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a84bc6cefb099b1ffce3e04705a1e61e91d895b9f09b9e1be14c8ec5823c181" +checksum = "fba4d9bbc36e8d5df471666c5a3f2fa2599f1e860bc61c05c1879ec5643701d9" dependencies = [ "ahash", "axum", @@ -1650,21 +1636,21 @@ dependencies = [ [[package]] name = "commonware-runtime-macros" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f7b39ef9fd9f1df4fe4445c9a5ec5ba012d90d1602b12a8b8626a8563ab02c" +checksum = "d478d4299c93e9f9a80064efdacd69ac0fe8324a9dc44264edbcd0d47c5d6eb3" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "commonware-storage" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac18e2b696ab7bbecf1059bc3b8d39fc0af002180906d65c7df3112fd9385de" +checksum = "15ec50ba1efad76170c65d8c53baefc49da20eed02df9eedb3aa0754f2ff1d53" dependencies = [ "ahash", "anyhow", @@ -1679,7 +1665,7 @@ dependencies = [ "commonware-utils", "futures", "futures-util", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "thiserror 2.0.18", "tracing", "zstd", @@ -1687,9 +1673,9 @@ dependencies = [ [[package]] name = "commonware-stream" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8890b69e3fa209bc9e87d199ff60ca5a21c88322d32914847511887e6dc5364b" +checksum = "e4448b7e1d6a0d7a3fc9521be6ddebba0e3bde321da63e414233b6c53bd39e77" dependencies = [ "chacha20poly1305", "commonware-codec", @@ -1707,13 +1693,12 @@ dependencies = [ [[package]] name = "commonware-utils" -version = "2026.7.0" +version = "2026.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a484dd46b738d9b5ba892312425e71e6afaf4320e388d74449bd9ca0ea14cd76" +checksum = "a611d1e0f4285e396deab2e76cf0554f05fee53e9c3d69a53374ce069b88ab44" dependencies = [ "ahash", "bytes", - "cfg-if", "commonware-codec", "commonware-formatting", "commonware-macros", @@ -1721,7 +1706,7 @@ dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", "getrandom 0.4.3", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-bigint", "num-integer", "num-rational", @@ -1737,9 +1722,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.18.1" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -1872,10 +1857,11 @@ dependencies = [ [[package]] name = "criterion" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", @@ -1884,6 +1870,7 @@ dependencies = [ "itertools 0.13.0", "num-traits", "oorandom", + "page_size", "plotters", "rayon", "regex", @@ -1895,9 +1882,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", @@ -1924,18 +1911,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -1962,7 +1949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", - "ctutils 0.4.2", + "ctutils", "getrandom 0.4.3", "hybrid-array", "num-traits", @@ -1978,7 +1965,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -1993,22 +1979,13 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "ctutils" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c67c81499f542d1dd38c6a2a2fe825f4dd4bca5162965dd2eea0c8119873d3c" -dependencies = [ - "cmov 0.4.6", -] - [[package]] name = "ctutils" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "cmov 0.5.4", + "cmov", "subtle", ] @@ -2051,7 +2028,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2085,7 +2062,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2099,7 +2076,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2110,7 +2087,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2121,14 +2098,14 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -2187,7 +2164,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2209,7 +2186,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.117", + "syn 2.0.119", "unicode-xid", ] @@ -2243,7 +2220,7 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", - "ctutils 0.4.2", + "ctutils", ] [[package]] @@ -2267,6 +2244,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2275,7 +2262,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2341,14 +2328,14 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -2410,7 +2397,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2477,7 +2464,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2668,7 +2655,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2872,9 +2859,12 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -3068,7 +3058,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -3206,7 +3196,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3227,18 +3217,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -3346,7 +3336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3442,9 +3432,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfuzzer-sys" @@ -3521,7 +3511,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3605,9 +3595,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -3677,7 +3667,7 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3694,6 +3684,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -3701,6 +3700,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", ] [[package]] @@ -3713,6 +3730,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3725,12 +3753,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl" version = "0.10.78" @@ -3754,7 +3776,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3866,6 +3888,16 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3891,7 +3923,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3914,7 +3946,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3951,22 +3983,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4037,12 +4069,11 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", + "cpufeatures 0.3.0", "universal-hash", ] @@ -4142,7 +4173,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4156,9 +4187,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca3d75b4566b9a29fe1ed623587fb058e826eb329a0be4b7c4da1ebb2d7a6ca" +checksum = "1784dc11a05f5a8f57c4a62915686be140f24f70301290b997e317241fa9ffc2" dependencies = [ "dtoa", "itoa", @@ -4168,13 +4199,13 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" +checksum = "01e34894696ff94f64a20c2c373a6440903e9c2789a303d68ec6e6f953f890e4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.5", ] [[package]] @@ -4216,7 +4247,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4352,7 +4383,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.1", + "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -4512,7 +4543,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4873,7 +4904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct 1.0.0", - "ctutils 0.4.2", + "ctutils", "der 0.8.1", "hybrid-array", "subtle", @@ -4975,7 +5006,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5051,7 +5082,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5269,7 +5300,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5325,7 +5356,7 @@ dependencies = [ "serde", "sha2 0.10.9", "tokio", - "toml", + "toml 0.9.12+spec-1.1.0", "tracing", ] @@ -5342,9 +5373,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -5360,7 +5402,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5380,20 +5422,21 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "sysinfo" -version = "0.37.2" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", + "objc2-open-directory", "windows", ] @@ -5431,7 +5474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5463,7 +5506,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5474,7 +5517,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5563,9 +5606,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -5586,7 +5629,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5649,6 +5692,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -5681,18 +5739,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.2", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -5760,7 +5818,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5887,12 +5945,12 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.7", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -6029,7 +6087,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -6124,37 +6182,23 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -6165,19 +6209,19 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core", + "windows-link", "windows-threading", ] @@ -6189,7 +6233,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6200,15 +6244,9 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -6217,12 +6255,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-core", + "windows-link", ] [[package]] @@ -6231,18 +6269,9 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -6251,16 +6280,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6269,7 +6289,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6305,7 +6325,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6345,7 +6365,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -6358,11 +6378,11 @@ dependencies = [ [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6583,7 +6603,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -6604,7 +6624,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6624,28 +6644,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6678,7 +6698,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4004d11b..d41cc177 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,8 +10,8 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" summit-types = { path = "../types" } -commonware-codec = "2026.7.0" -commonware-cryptography = "2026.7.0" +commonware-codec = "2026.9.0" +commonware-cryptography = "2026.9.0" arbitrary = { version = "1", features = ["derive"] } bytes = "1.10.1" diff --git a/node/Cargo.toml b/node/Cargo.toml index 5d8f9a3c..a152943b 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -62,6 +62,7 @@ commonware-codec.workspace = true commonware-consensus.workspace = true commonware-cryptography.workspace = true commonware-math.workspace = true +commonware-macros.workspace = true commonware-p2p.workspace = true commonware-parallel.workspace = true commonware-runtime.workspace = true @@ -124,7 +125,6 @@ procfs = { version = "0.17.0", optional = true } libc = { version = "0.2", optional = true } [dev-dependencies] -commonware-macros.workspace = true tracing-subscriber = "0.3.19" # test harness (move to dev-deps later) alloy-rpc-types-engine.workspace = true diff --git a/node/src/args.rs b/node/src/args.rs index 5a7b290a..035e4b57 100644 --- a/node/src/args.rs +++ b/node/src/args.rs @@ -1,7 +1,8 @@ use crate::{ config::{ - BACKFILLER_CHANNEL, BROADCASTER_CHANNEL, EngineConfig, FINALIZER_PENDING_NOTARIZED_MAX, - MESSAGE_BACKLOG, PENDING_CHANNEL, RECOVERED_CHANNEL, RESOLVER_CHANNEL, expect_key_store, + BACKFILLER_CHANNEL, BROADCASTER_CHANNEL, CHANNEL_BURST, EngineConfig, + FINALIZER_PENDING_NOTARIZED_MAX, PENDING_CHANNEL, RECOVERED_CHANNEL, RESOLVER_CHANNEL, + expect_key_store, }, engine::Engine, genesis::GenesisSubCmd, @@ -739,6 +740,7 @@ async fn run_node_inner( listen, our_ip, network_committee_ingress, + crate::config::startup_peer_limit(&initial_state), max_message_size, ); p2p_cfg.mailbox_size = MAILBOX_SIZE; @@ -753,6 +755,7 @@ async fn run_node_inner( initial_state, loaded.last_block, loaded.finalized_header, + loaded.finalized_headers_chain, observer_network_key, ) .await @@ -764,6 +767,7 @@ async fn run_node_inner( listen, our_ip, network_committee_ingress, + crate::config::startup_peer_limit(&initial_state), max_message_size, ); p2p_cfg.mailbox_size = MAILBOX_SIZE; @@ -778,6 +782,7 @@ async fn run_node_inner( initial_state, loaded.last_block, loaded.finalized_header, + loaded.finalized_headers_chain, None, ) .await @@ -903,6 +908,7 @@ async fn run_node_local_inner( listen, our_ip, network_committee_ingress, + crate::config::startup_peer_limit(&initial_state), max_message_size, ); p2p_cfg.mailbox_size = MAILBOX_SIZE; @@ -917,6 +923,7 @@ async fn run_node_local_inner( initial_state, checkpoint_parent_block, None, + None, observer_network_key, ) .await @@ -928,6 +935,7 @@ async fn run_node_local_inner( listen, our_ip, network_committee_ingress, + crate::config::startup_peer_limit(&initial_state), max_message_size, ); p2p_cfg.mailbox_size = MAILBOX_SIZE; @@ -943,6 +951,7 @@ async fn run_node_local_inner( checkpoint_parent_block, None, None, + None, ) .await }; @@ -1039,8 +1048,8 @@ async fn supervise_node_tasks( #[allow(clippy::too_many_arguments)] async fn start_network_and_engine( context: tokio::Context, - p2p_cfg: authenticated::discovery::Config, - engine_client: EC, + mut p2p_cfg: authenticated::discovery::Config, + mut engine_client: EC, key_store: KeyStore, peers: Vec<(PublicKey, bls12381::PublicKey)>, flags: RunFlags, @@ -1048,16 +1057,100 @@ async fn start_network_and_engine( initial_state: ConsensusState, checkpoint_last_block: Option, checkpoint_finalized_header: Option>, + checkpoint_headers: Option>>, observer_network_key: Option, ) -> (Handle>, Handle<()>, Handle<()>) where S: Signer, EC: EngineClient, { + // Recover authoritative protocol values before the network allocates fixed + // peer-derived mailboxes. The finalizer will reopen the same store; never + // size from stale genesis values on an ordinary restart. + let cancellation = tokio_util::sync::CancellationToken::new(); + let mut startup_db = summit_finalizer::db::FinalizerState::< + _, + commonware_cryptography::bls12381::primitives::variant::MinPk, + >::new( + context.child("startup_state"), + summit_finalizer::db::config( + &flags.db_prefix, + commonware_runtime::buffer::paged::CacheRef::from_pooler( + &context, + std::num::NonZeroU16::new(4096).unwrap(), + std::num::NonZeroUsize::new(1024).unwrap(), + ), + ), + cancellation.clone(), + ) + .await; + summit_finalizer::startup::check_history( + &startup_db, + checkpoint_headers.as_deref().unwrap_or_default(), + ) + .await + .expect("checkpoint conflicts with local finalized history"); + let mut headers = checkpoint_headers.unwrap_or_default(); + headers.extend(checkpoint_finalized_header.iter().cloned()); + if let Some(record) = startup_db + .get_checkpoint_import() + .await + .expect("failed to read import") + { + headers.push(record.finalized_header); + } + if let Some((_, record)) = startup_db + .get_pending_import() + .await + .expect("failed to read pending import") + { + headers.push(record.finalized_header); + } + if !headers.is_empty() { + let archives = crate::engine::open_syncer_archives( + &context, + &flags.db_prefix, + commonware_runtime::buffer::paged::CacheRef::from_pooler( + &context, + commonware_utils::NZU16!(4096), + commonware_utils::NZUsize!(1024), + ), + ) + .await; + crate::engine::check_syncer_history(&archives, &headers) + .await + .expect("checkpoint conflicts with syncer history"); + } + let (initial_state, _) = summit_finalizer::startup::prepare( + &mut startup_db, + &mut engine_client, + initial_state, + checkpoint_finalized_header, + checkpoint_last_block, + genesis.config_digest(), + ) + .await + .expect("failed to prepare checkpoint startup"); + drop(startup_db); + let peers = if initial_state.get_latest_height() > 0 { + initial_state.get_validator_keys() + } else { + peers + }; + p2p_cfg.max_peers_per_set = crate::config::startup_peer_limit(&initial_state); + let peer_limit = p2p_cfg.max_peers_per_set; + let local_identity = observer_network_key + .clone() + .unwrap_or_else(|| key_store.node_key.public_key()); + info!( + max_peers_per_set = peer_limit.get(), + burst = CHANNEL_BURST, + "allocating P2P capacity from startup protocol state; capacity-increasing updates require coordination" + ); let (mut network, oracle) = authenticated::discovery::Network::new(context.child("network"), p2p_cfg); - let oracle = DiscoveryOracle::new(oracle); + let oracle = DiscoveryOracle::new(oracle, local_identity, peer_limit); // In observer mode the node's identity is this derived child key, not the // master node key: the engine identifies itself by it (resolver @@ -1078,27 +1171,31 @@ where flags.db_prefix, genesis, initial_state, - checkpoint_last_block, - checkpoint_finalized_header, + None, // Engine recovers the durable import record, not the input files. + None, flags.finalizer_pending_notarized_max, ) .unwrap(); config.force_verifier_only = flags.observer.is_some(); config.observer_network_key = observer_network_key; - let pending_limit = Quota::per_second(NonZeroU32::new(512).unwrap()); - let pending = network.register(PENDING_CHANNEL, pending_limit, MESSAGE_BACKLOG); + let pending_limit = Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()); + let pending = network.register(PENDING_CHANNEL, pending_limit); - let recovered_limit = Quota::per_second(NonZeroU32::new(512).unwrap()); - let recovered = network.register(RECOVERED_CHANNEL, recovered_limit, MESSAGE_BACKLOG); + let recovered_limit = Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()); + let recovered = network.register(RECOVERED_CHANNEL, recovered_limit); - let resolver_limit = Quota::per_second(NonZeroU32::new(512).unwrap()); - let resolver = network.register(RESOLVER_CHANNEL, resolver_limit, MESSAGE_BACKLOG); + let resolver_limit = Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()); + let resolver = network.register(RESOLVER_CHANNEL, resolver_limit); - let broadcaster_limit = Quota::per_second(NonZeroU32::new(512).unwrap()); - let broadcaster = network.register(BROADCASTER_CHANNEL, broadcaster_limit, MESSAGE_BACKLOG); + let broadcaster_limit = Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()); + let broadcaster = network.register(BROADCASTER_CHANNEL, broadcaster_limit); - let backfiller = network.register(BACKFILLER_CHANNEL, config.backfill_quota, MESSAGE_BACKLOG); + let backfiller = network.register(BACKFILLER_CHANNEL, config.backfill_quota); let genesis_hash = config.genesis_hash; let namespace = config.namespace.as_bytes().to_vec(); @@ -1180,6 +1277,7 @@ fn get_initial_state( genesis.max_deposits_per_epoch, genesis.max_withdrawals_per_epoch, genesis.observers_per_validator, + genesis.max_validator_count, genesis.minimum_validator_count, genesis.invalid_deposit_tax, genesis.max_pending_withdrawals_per_validator, diff --git a/node/src/config.rs b/node/src/config.rs index afaa8fe6..809c5b17 100644 --- a/node/src/config.rs +++ b/node/src/config.rs @@ -36,9 +36,32 @@ const FETCH_CONCURRENT: usize = 8; const MAX_FETCH_COUNT: usize = 32; const MAX_FETCH_SIZE: usize = 512 * 1024; const DEQUE_SIZE: usize = 32; -pub const MESSAGE_BACKLOG: usize = 16384; -const BACKFILL_QUOTA: u32 = 512; // in seconds -const FETCH_RATE_P2P: u32 = 512; // in seconds +const BACKFILL_QUOTA: u32 = 512; // messages per second +const FETCH_RATE_P2P: u32 = 512; // messages per second +pub const CHANNEL_BURST: u32 = 16; + +/// Capacity is fixed when the network starts. Include accepted pending raises +/// because admission may already have used them before boundary application. +/// One extra slot covers the local identity outside the authorized committee. +pub(crate) fn startup_peer_limit(state: &ConsensusState) -> NonZeroUsize { + let validators = state + .get_max_validator_count() + .max(state.prospective_max_validator_count()); + let observers = state + .get_observers_per_validator() + .max(state.prospective_observers_per_validator()); + let identities = validators + .checked_mul(u64::from(observers) + 1) + .and_then(|n| n.checked_add(1)) + .and_then(|n| usize::try_from(n).ok()) + .and_then(NonZeroUsize::new) + .expect("startup peer capacity overflow"); + assert!( + state.active_or_joining_validator_count() <= validators, + "startup validator membership exceeds configured capacity" + ); + identities +} pub struct EngineConfig> { pub engine_client: C, @@ -70,7 +93,11 @@ pub struct EngineConfig, pub checkpoint_finalized_header: Option>, @@ -106,7 +133,8 @@ impl> EngineConfig> EngineConfig KeyStore { mod tests { use std::path::PathBuf; - use crate::config::expect_key_store; + use super::{ConsensusState, expect_key_store, startup_peer_limit}; + + #[test] + fn startup_peer_capacity_includes_default_observers_and_local_slot() { + let mut state = ConsensusState::default(); + state.set_observers_per_validator( + summit_types::protocol_params::DEFAULT_OBSERVERS_PER_VALIDATOR, + ); + assert_eq!(startup_peer_limit(&state).get(), 2177); + state.set_observers_per_validator(0); + assert_eq!(startup_peer_limit(&state).get(), 129); + } + + #[test] + fn startup_peer_capacity_covers_pending_raises_but_not_pending_reductions() { + use summit_types::protocol_params::ProtocolParam; + let mut state = ConsensusState::default(); + state.set_observers_per_validator(16); + state.push_protocol_param_changes([ + ProtocolParam::MaxValidatorCount(256), + ProtocolParam::ObserversPerValidator(32), + ]); + assert_eq!(startup_peer_limit(&state).get(), 256 * 33 + 1); + state.push_protocol_param_changes([ + ProtocolParam::MaxValidatorCount(64), + ProtocolParam::ObserversPerValidator(8), + ]); + assert_eq!(startup_peer_limit(&state).get(), 2177); + } #[test] fn test_expect_keys_node0() { diff --git a/node/src/engine.rs b/node/src/engine.rs index ac3c3775..6ade0273 100644 --- a/node/src/engine.rs +++ b/node/src/engine.rs @@ -72,6 +72,104 @@ pub const VALIDATOR_NUM_WARM_UP_EPOCHS: u64 = 2; pub const VALIDATOR_WITHDRAWAL_NUM_EPOCHS: u64 = 2; // +type Finalizations = immutable::Archive< + E, + summit_types::Digest, + commonware_consensus::simplex::types::Finalization, +>; +type FinalizedBlocks = immutable::Archive; + +/// Shared layout for pre-import conflict checks and the running syncer. +#[commonware_macros::boxed] +pub(crate) async fn open_syncer_archives< + E: BufferPooler + Clock + Rng + Spawner + Storage + Metrics, +>( + context: &E, + prefix: &str, + page_cache: CacheRef, +) -> (Finalizations, FinalizedBlocks) { + // create the syncer + // Initialize finalizations by height archive + let finalizations_by_height = immutable::Archive::init( + context.child("finalizations_by_height"), + immutable::Config { + metadata_partition: format!("{}-finalizations-by-height-metadata", prefix), + freezer_table_partition: format!("{}-finalizations-by-height-freezer-table", prefix), + freezer_table_initial_size: FREEZER_TABLE_INITIAL_SIZE, + freezer_table_resize_frequency: FREEZER_TABLE_RESIZE_FREQUENCY, + freezer_table_resize_chunk_size: FREEZER_TABLE_RESIZE_CHUNK_SIZE, + freezer_key_partition: format!("{}-finalizations-by-height-freezer-key", prefix), + freezer_key_page_cache: page_cache.clone(), + freezer_value_partition: format!("{}-finalizations-by-height-freezer-value", prefix), + freezer_value_target_size: FREEZER_JOURNAL_TARGET_SIZE, + freezer_value_compression: FREEZER_JOURNAL_COMPRESSION, + ordinal_partition: format!("{}-finalizations-by-height-ordinal", prefix), + items_per_section: IMMUTABLE_ITEMS_PER_SECTION, + codec_config: usize::MAX, + freezer_key_write_buffer: WRITE_BUFFER, + freezer_value_write_buffer: WRITE_BUFFER, + ordinal_write_buffer: WRITE_BUFFER, + replay_buffer: REPLAY_BUFFER, + }, + ) + .await + .expect("failed to initialize finalizations by height archive"); + + // Initialize finalized blocks archive + let finalized_blocks = immutable::Archive::init( + context.child("finalized_blocks"), + immutable::Config { + metadata_partition: format!("{}-finalized_blocks-metadata", prefix), + freezer_table_partition: format!("{}-finalized_blocks-freezer-table", prefix), + freezer_table_initial_size: FREEZER_TABLE_INITIAL_SIZE, + freezer_table_resize_frequency: FREEZER_TABLE_RESIZE_FREQUENCY, + freezer_table_resize_chunk_size: FREEZER_TABLE_RESIZE_CHUNK_SIZE, + freezer_key_partition: format!("{}-finalized_blocks-freezer-key", prefix), + freezer_key_page_cache: page_cache.clone(), + freezer_value_partition: format!("{}-finalized_blocks-freezer-value", prefix), + freezer_value_target_size: FREEZER_JOURNAL_TARGET_SIZE, + freezer_value_compression: FREEZER_JOURNAL_COMPRESSION, + ordinal_partition: format!("{}-finalized_blocks-ordinal", prefix), + items_per_section: IMMUTABLE_ITEMS_PER_SECTION, + freezer_key_write_buffer: WRITE_BUFFER, + freezer_value_write_buffer: WRITE_BUFFER, + ordinal_write_buffer: WRITE_BUFFER, + replay_buffer: REPLAY_BUFFER, + codec_config: (), + }, + ) + .await + .expect("failed to initialize finalized blocks archive"); + + (finalizations_by_height, finalized_blocks) +} + +pub(crate) async fn check_syncer_history< + E: BufferPooler + Clock + Rng + Spawner + Storage + Metrics, +>( + archives: &(Finalizations, FinalizedBlocks), + headers: &[summit_types::FinalizedHeader], +) -> anyhow::Result<()> { + use commonware_storage::archive::{Archive as _, Identifier}; + for header in headers { + let height = header.header().height(); + let digest = header.header().computed_digest(); + if let Some(certificate) = archives.0.get(Identifier::Index(height)).await? { + anyhow::ensure!( + certificate.proposal.payload == digest, + "checkpoint conflicts with syncer certificate" + ); + } + if let Some(block) = archives.1.get(Identifier::Index(height)).await? { + anyhow::ensure!( + block.digest() == digest, + "checkpoint conflicts with syncer block" + ); + } + } + Ok(()) +} + pub struct Engine< E: BufferPooler + Clock + GClock + Rng + CryptoRng + Spawner + Storage + Metrics + Network, C: EngineClient, @@ -81,10 +179,10 @@ pub struct Engine< context: E, application: summit_application::Actor, - buffer: buffered::Engine, - buffer_mailbox: buffered::Mailbox, + pub(crate) buffer: buffered::Engine, + pub(crate) buffer_mailbox: buffered::Mailbox, #[allow(clippy::type_complexity)] - syncer: summit_syncer::Actor< + pub(crate) syncer: summit_syncer::Actor< E, Block, SummitSchemeProvider, @@ -101,8 +199,8 @@ pub struct Engine< Sequential, Exact, >, - syncer_mailbox: summit_syncer::Mailbox, - finalizer: Finalizer, + pub(crate) syncer_mailbox: summit_syncer::Mailbox, + pub(crate) finalizer: Finalizer, pub finalizer_mailbox: FinalizerMailbox, pub finalizer_state_query: ConsensusStateQuery, orchestrator: summit_orchestrator::Actor< @@ -112,13 +210,13 @@ pub struct Engine< Sequential, DynamicEpocher, >, - orchestrator_mailbox: summit_orchestrator::Mailbox, + pub(crate) orchestrator_mailbox: summit_orchestrator::Mailbox, oracle: O, node_public_key: PublicKey, mailbox_size: NonZeroUsize, fetch_timeout: Duration, - sync_start: SyncStart, - checkpoint: Option>, + pub(crate) sync_start: SyncStart, + pub(crate) checkpoint: Option>, cancellation_token: CancellationToken, #[cfg(feature = "permissioned")] pub paused: Arc, @@ -133,7 +231,7 @@ impl< where MultisigScheme: Scheme, { - pub async fn new(context: E, cfg: EngineConfig) -> Self { + pub async fn new(context: E, mut cfg: EngineConfig) -> Self { let blocks_per_epoch = cfg.blocks_per_epoch; // The key this node identifies itself by: the derived child key in @@ -172,6 +270,54 @@ where #[cfg(feature = "permissioned")] let paused = Arc::new(AtomicBool::new(false)); + // Resolve explicit checkpoint imports before any actors run. Production + // also does this before P2P allocation; this path covers embedded engines + // and recovers imports without requiring the original checkpoint files. + let mut startup_db = summit_finalizer::db::FinalizerState::<_, MinPk>::new( + context.child("startup_state"), + summit_finalizer::db::config(&cfg.partition_prefix, page_cache.clone()), + cancellation_token.clone(), + ) + .await; + let archives = + open_syncer_archives(&context, &cfg.partition_prefix, page_cache.clone()).await; + let mut headers: Vec<_> = cfg.checkpoint_finalized_header.iter().cloned().collect(); + if let Some(record) = startup_db + .get_checkpoint_import() + .await + .expect("failed to read import") + { + headers.push(record.finalized_header); + } + if let Some((_, record)) = startup_db + .get_pending_import() + .await + .expect("failed to read pending import") + { + headers.push(record.finalized_header); + } + check_syncer_history(&archives, &headers) + .await + .expect("checkpoint conflicts with syncer history"); + let (finalizations_by_height, finalized_blocks) = archives; + let (selected, imported) = summit_finalizer::startup::prepare( + &mut startup_db, + &mut cfg.engine_client, + cfg.initial_state, + cfg.checkpoint_finalized_header.take(), + cfg.checkpoint_last_block.take(), + cfg.config_digest, + ) + .await + .expect("failed to prepare checkpoint startup"); + drop(startup_db); + cfg.initial_state = selected; + let checkpoint = imported.map(|record| SyncCheckpoint { + processed_height: commonware_consensus::types::Height::new(record.processed_height), + last_block: record.last_block, + finalized_header: record.finalized_header, + }); + // create finalizer let (finalizer, initial_state, finalizer_mailbox, finalizer_state_query) = Finalizer::new( context.child("finalizer"), @@ -236,83 +382,6 @@ where }, ); - // create the syncer - // Initialize finalizations by height archive - let finalizations_by_height = immutable::Archive::init( - context.child("finalizations_by_height"), - immutable::Config { - metadata_partition: format!( - "{}-finalizations-by-height-metadata", - cfg.partition_prefix - ), - freezer_table_partition: format!( - "{}-finalizations-by-height-freezer-table", - cfg.partition_prefix - ), - freezer_table_initial_size: FREEZER_TABLE_INITIAL_SIZE, - freezer_table_resize_frequency: FREEZER_TABLE_RESIZE_FREQUENCY, - freezer_table_resize_chunk_size: FREEZER_TABLE_RESIZE_CHUNK_SIZE, - freezer_key_partition: format!( - "{}-finalizations-by-height-freezer-key", - cfg.partition_prefix - ), - freezer_key_page_cache: page_cache.clone(), - freezer_value_partition: format!( - "{}-finalizations-by-height-freezer-value", - cfg.partition_prefix - ), - freezer_value_target_size: FREEZER_JOURNAL_TARGET_SIZE, - freezer_value_compression: FREEZER_JOURNAL_COMPRESSION, - ordinal_partition: format!( - "{}-finalizations-by-height-ordinal", - cfg.partition_prefix - ), - items_per_section: IMMUTABLE_ITEMS_PER_SECTION, - codec_config: usize::MAX, - freezer_key_write_buffer: WRITE_BUFFER, - freezer_value_write_buffer: WRITE_BUFFER, - ordinal_write_buffer: WRITE_BUFFER, - replay_buffer: REPLAY_BUFFER, - }, - ) - .await - .expect("failed to initialize finalizations by height archive"); - - // Initialize finalized blocks archive - let finalized_blocks = immutable::Archive::init( - context.child("finalized_blocks"), - immutable::Config { - metadata_partition: format!("{}-finalized_blocks-metadata", cfg.partition_prefix), - freezer_table_partition: format!( - "{}-finalized_blocks-freezer-table", - cfg.partition_prefix - ), - freezer_table_initial_size: FREEZER_TABLE_INITIAL_SIZE, - freezer_table_resize_frequency: FREEZER_TABLE_RESIZE_FREQUENCY, - freezer_table_resize_chunk_size: FREEZER_TABLE_RESIZE_CHUNK_SIZE, - freezer_key_partition: format!( - "{}-finalized_blocks-freezer-key", - cfg.partition_prefix - ), - freezer_key_page_cache: page_cache.clone(), - freezer_value_partition: format!( - "{}-finalized_blocks-freezer-value", - cfg.partition_prefix - ), - freezer_value_target_size: FREEZER_JOURNAL_TARGET_SIZE, - freezer_value_compression: FREEZER_JOURNAL_COMPRESSION, - ordinal_partition: format!("{}-finalized_blocks-ordinal", cfg.partition_prefix), - items_per_section: IMMUTABLE_ITEMS_PER_SECTION, - freezer_key_write_buffer: WRITE_BUFFER, - freezer_value_write_buffer: WRITE_BUFFER, - ordinal_write_buffer: WRITE_BUFFER, - replay_buffer: REPLAY_BUFFER, - codec_config: (), - }, - ) - .await - .expect("failed to initialize finalized blocks archive"); - let syncer_config = summit_syncer::Config { scheme_provider: scheme_provider.clone(), epocher: epocher.clone(), @@ -380,11 +449,6 @@ where "engine initialized" ); - let checkpoint = cfg.checkpoint_last_block.map(|last_block| SyncCheckpoint { - last_block, - finalized_header: cfg.checkpoint_finalized_header, - }); - Self { context, application, diff --git a/node/src/test_harness/common.rs b/node/src/test_harness/common.rs index 5df300a1..19af7e83 100644 --- a/node/src/test_harness/common.rs +++ b/node/src/test_harness/common.rs @@ -3,7 +3,10 @@ use commonware_math::algebra::Random; use std::num::NonZeroU64; use crate::test_harness::mock_engine_client::MockEngineNetwork; -use crate::{config::EngineConfig, engine::Engine}; +use crate::{ + config::{CHANNEL_BURST, EngineConfig}, + engine::Engine, +}; use alloy_eips::eip7685::Requests; use alloy_primitives::{Address, B256, Bytes}; use alloy_rpc_types_engine::ForkchoiceState; @@ -151,6 +154,7 @@ pub fn run_until_height( let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -393,6 +397,7 @@ pub fn get_initial_state( 10, 16, 0, + 256, 3, 0, 3, @@ -703,7 +708,8 @@ where mailbox_size: NZUsize!(1024), finalizer_pending_notarized_max: 1000, deque_size: 10, - backfill_quota: Quota::per_second(NonZeroU32::new(512).unwrap()), + backfill_quota: Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()), leader_timeout: Duration::from_secs(1), notarization_timeout: Duration::from_secs(2), nullify_retry: Duration::from_secs(10), @@ -713,7 +719,8 @@ where max_fetch_count: 10, _max_fetch_size: 1024 * 512, fetch_concurrent: 10, - fetch_rate_per_peer: Quota::per_second(NonZeroU32::new(512).unwrap()), + fetch_rate_per_peer: Quota::per_second(NonZeroU32::new(512).unwrap()) + .allow_burst(NonZeroU32::new(CHANNEL_BURST).unwrap()), initial_state, checkpoint_last_block: None, checkpoint_finalized_header: None, @@ -725,12 +732,15 @@ where pub struct SimulatedOracle { inner: simulated::Manager, + blocked_subscribers: + Vec>>, } impl Clone for SimulatedOracle { fn clone(&self) -> Self { Self { inner: self.inner.clone(), + blocked_subscribers: self.blocked_subscribers.clone(), } } } @@ -745,6 +755,7 @@ impl SimulatedOracle { pub fn new(oracle: Oracle) -> Self { Self { inner: oracle.manager(), + blocked_subscribers: Vec::new(), } } } @@ -768,6 +779,14 @@ impl Blocker for SimulatedOracle { // This is only used in production for misbehaving peers Feedback::Ok } + + fn blocked(&mut self) -> commonware_p2p::BlockedSubscription { + let (sender, receiver) = + commonware_utils::channel::ring::channel(commonware_utils::NZUsize!(1)); + sender.send_lossy(commonware_utils::ordered::Set::default()); + self.blocked_subscribers.push(sender); + receiver + } } impl Provider for SimulatedOracle { diff --git a/node/src/tests/checkpointing/creation.rs b/node/src/tests/checkpointing/creation.rs index 0e971b23..ec49142c 100644 --- a/node/src/tests/checkpointing/creation.rs +++ b/node/src/tests/checkpointing/creation.rs @@ -33,7 +33,7 @@ fn test_checkpoint_created() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -43,6 +43,7 @@ fn test_checkpoint_created() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -227,7 +228,7 @@ fn test_previous_header_hash_matches() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -237,6 +238,7 @@ fn test_previous_header_hash_matches() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times diff --git a/node/src/tests/checkpointing/joining.rs b/node/src/tests/checkpointing/joining.rs index 37a1bef3..f827ea6a 100644 --- a/node/src/tests/checkpointing/joining.rs +++ b/node/src/tests/checkpointing/joining.rs @@ -29,7 +29,7 @@ fn test_single_engine_with_checkpoint() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(42); @@ -39,6 +39,7 @@ fn test_single_engine_with_checkpoint() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(10), @@ -147,7 +148,7 @@ fn test_node_joins_later_with_checkpoint() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -157,6 +158,7 @@ fn test_node_joins_later_with_checkpoint() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -395,11 +397,50 @@ fn test_node_joins_later_with_checkpoint() { /// the startup replay. #[test_traced("INFO")] fn test_checkpoint_join_replays_and_seeds_finalized_header() { + checkpoint_replay(CheckpointStart::Fresh); +} + +#[derive(Clone, Copy)] +enum CheckpointStart { + Fresh, + Existing, + CommittedImport, + FetchTerminal, + CommittedImportMissingTerminal, + ArchiveConflict, +} + +#[test_traced("WARN")] +fn test_checkpoint_fast_forward_over_nonempty_finalizer() { + checkpoint_replay(CheckpointStart::Existing); +} + +#[test_traced("WARN")] +fn test_checkpoint_restart_after_import_without_input_files() { + checkpoint_replay(CheckpointStart::CommittedImport); +} + +#[test_traced("WARN")] +fn test_checkpoint_fetches_missing_terminal_block() { + checkpoint_replay(CheckpointStart::FetchTerminal); +} + +#[test_traced("WARN")] +fn test_checkpoint_restart_before_missing_terminal_delivery() { + checkpoint_replay(CheckpointStart::CommittedImportMissingTerminal); +} + +#[test_traced("WARN")] +fn test_checkpoint_conflicting_syncer_certificate_rejected_before_promotion() { + checkpoint_replay(CheckpointStart::ArchiveConflict); +} + +fn checkpoint_replay(start: CheckpointStart) { let n = 5; let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; let cfg = deterministic::Config::default().with_seed(0); let executor = Runner::from(cfg); @@ -407,6 +448,7 @@ fn test_checkpoint_join_replays_and_seeds_finalized_header() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -543,11 +585,128 @@ fn test_checkpoint_join_replays_and_seeds_finalized_header() { config.checkpoint_last_block = Some(last_block.clone()); config.checkpoint_finalized_header = Some(source_header.clone()); - let engine = Engine::new( + if !matches!( + start, + CheckpointStart::Fresh | CheckpointStart::FetchTerminal + ) { + let cache = commonware_runtime::buffer::paged::CacheRef::from_pooler( + &context, + commonware_utils::NZU16!(4096), + NZUsize!(1024), + ); + let mut db = summit_finalizer::db::FinalizerState::< + _, + commonware_cryptography::bls12381::primitives::variant::MinPk, + >::new( + context.child("seed_import"), + summit_finalizer::db::config(&uid, cache), + tokio_util::sync::CancellationToken::new(), + ) + .await; + // A genuine nonempty database, not an empty-store checkpoint boot. + db.store_consensus_state(initial_state.get_epoch(), &initial_state) + .await + .unwrap(); + db.commit().await.unwrap(); + if matches!( + start, + CheckpointStart::CommittedImport | CheckpointStart::CommittedImportMissingTerminal + ) { + let block = if matches!(start, CheckpointStart::CommittedImportMissingTerminal) { + None + } else { + Some(last_block.clone()) + }; + summit_finalizer::startup::prepare( + &mut db, + &mut config.engine_client, + checkpoint_state.clone(), + Some(source_header.clone()), + block, + config.config_digest, + ) + .await + .unwrap(); + // Model restart before the syncer starts. Only durable storage + // remains; no checkpoint artifacts are supplied to the engine. + config.initial_state = initial_state.clone(); + config.checkpoint_last_block = None; + config.checkpoint_finalized_header = None; + } + } + if matches!( + start, + CheckpointStart::FetchTerminal | CheckpointStart::CommittedImportMissingTerminal + ) { + config.checkpoint_last_block = None; + common::link_validators( + &mut oracle, + &node_public_keys, + link, + Some(|n, from, to| from == n - 1 || to == n - 1), + ) + .await; + } + + if matches!(start, CheckpointStart::ArchiveConflict) { + use commonware_consensus::marshal::store::Certificates as _; + let cache = commonware_runtime::buffer::paged::CacheRef::from_pooler( + &context, + commonware_utils::NZU16!(4096), + NZUsize!(1024), + ); + let (certificates, _) = + crate::engine::open_syncer_archives(&context, &uid, cache).await; + let mut conflict = source_header.finalization().clone(); + conflict.proposal.payload = [42; 32].into(); + certificates + .put( + commonware_consensus::types::Height::new(last_block.height()), + conflict.proposal.payload, + conflict, + ) + .await + .unwrap() + .sync() + .await + .unwrap(); + } + let init = Engine::new( context.child("engine").with_attribute("uid", uid.clone()), config, - ) - .await; + ); + if matches!(start, CheckpointStart::ArchiveConflict) { + assert!( + futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(init)) + .await + .is_err() + ); + let cache = commonware_runtime::buffer::paged::CacheRef::from_pooler( + &context, + commonware_utils::NZU16!(4096), + NZUsize!(1024), + ); + let db = summit_finalizer::db::FinalizerState::< + _, + commonware_cryptography::bls12381::primitives::variant::MinPk, + >::new( + context.child("verify_rejected_import"), + summit_finalizer::db::config(&uid, cache), + tokio_util::sync::CancellationToken::new(), + ) + .await; + assert_eq!( + db.get_latest_consensus_state() + .await + .unwrap() + .get_latest_height(), + 0 + ); + assert!(db.get_checkpoint_import().await.unwrap().is_none()); + assert!(db.get_pending_import().await.unwrap().is_none()); + return context.auditor().state(); + } + let engine = init.await; let joiner_query = engine.finalizer_mailbox.clone(); let (pending, recovered, resolver, orchestrator, broadcast) = late_registrations.remove(&public_key).unwrap(); @@ -601,7 +760,7 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -611,6 +770,7 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times diff --git a/node/src/tests/checkpointing/startup.rs b/node/src/tests/checkpointing/startup.rs index 7157963f..67e49e76 100644 --- a/node/src/tests/checkpointing/startup.rs +++ b/node/src/tests/checkpointing/startup.rs @@ -140,6 +140,7 @@ fn single_file_import_has_no_chain_and_is_refused() { 3, 16, 0, + 256, 1, 0, 3, @@ -206,6 +207,7 @@ fn read_checkpoint_repopulates_pending_checkpoint() { 3, 16, 0, + 256, 1, 0, 3, diff --git a/node/src/tests/checkpointing/verification.rs b/node/src/tests/checkpointing/verification.rs index db8b6246..9c8d2c3c 100644 --- a/node/src/tests/checkpointing/verification.rs +++ b/node/src/tests/checkpointing/verification.rs @@ -65,7 +65,7 @@ fn test_checkpoint_verification_fixed_committee() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; let cfg = deterministic::Config::default().with_seed(0); let executor = Runner::from(cfg); @@ -73,6 +73,7 @@ fn test_checkpoint_verification_fixed_committee() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -130,6 +131,7 @@ fn test_checkpoint_verification_fixed_committee() { max_deposits_per_epoch: 10, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, invalid_deposit_tax: 0, max_pending_withdrawals_per_validator: 3, @@ -438,7 +440,7 @@ fn test_checkpoint_verification_dynamic_committee() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; let cfg = deterministic::Config::default().with_seed(0); let executor = Runner::from(cfg); @@ -446,6 +448,7 @@ fn test_checkpoint_verification_dynamic_committee() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -503,6 +506,7 @@ fn test_checkpoint_verification_dynamic_committee() { max_deposits_per_epoch: 10, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, invalid_deposit_tax: 0, max_pending_withdrawals_per_validator: 3, diff --git a/node/src/tests/engine.rs b/node/src/tests/engine.rs index bf51a6c8..f8253660 100644 --- a/node/src/tests/engine.rs +++ b/node/src/tests/engine.rs @@ -41,6 +41,7 @@ fn test_backfill_resolver_inherits_fetch_timeout() { let (network, oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(10), @@ -144,6 +145,7 @@ fn test_engine_detects_single_actor_clean_exit() { let (network, oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(10), diff --git a/node/src/tests/execution_requests/deposit_withdrawal_combined.rs b/node/src/tests/execution_requests/deposit_withdrawal_combined.rs index cc01cdf5..d52ab030 100644 --- a/node/src/tests/execution_requests/deposit_withdrawal_combined.rs +++ b/node/src/tests/execution_requests/deposit_withdrawal_combined.rs @@ -14,7 +14,7 @@ fn test_deposit_and_withdrawal_request_single() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -24,6 +24,7 @@ fn test_deposit_and_withdrawal_request_single() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -221,7 +222,7 @@ fn test_deposit_and_withdrawal_request_multiple() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -231,6 +232,7 @@ fn test_deposit_and_withdrawal_request_multiple() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -460,7 +462,7 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -469,6 +471,7 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -659,7 +662,7 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -668,6 +671,7 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -836,7 +840,7 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -845,6 +849,7 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1035,7 +1040,7 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -1044,6 +1049,7 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1240,7 +1246,7 @@ fn test_deposit_and_withdrawal_same_block() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -1249,6 +1255,7 @@ fn test_deposit_and_withdrawal_same_block() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1457,7 +1464,7 @@ fn test_inactive_withdrawals_then_rejoin_panics_epoch_boundary() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -1466,6 +1473,7 @@ fn test_inactive_withdrawals_then_rejoin_panics_epoch_boundary() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/execution_requests/deposits.rs b/node/src/tests/execution_requests/deposits.rs index 81848df7..643f1e01 100644 --- a/node/src/tests/execution_requests/deposits.rs +++ b/node/src/tests/execution_requests/deposits.rs @@ -11,7 +11,7 @@ fn test_deposit_request_single() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -21,6 +21,7 @@ fn test_deposit_request_single() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -177,7 +178,7 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -187,6 +188,7 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -393,7 +395,7 @@ fn test_duplicate_bls_consensus_key_rejected() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -402,6 +404,7 @@ fn test_duplicate_bls_consensus_key_rejected() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -636,7 +639,7 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -645,6 +648,7 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/execution_requests/malformed_requests.rs b/node/src/tests/execution_requests/malformed_requests.rs index 5052d382..b6d19e20 100644 --- a/node/src/tests/execution_requests/malformed_requests.rs +++ b/node/src/tests/execution_requests/malformed_requests.rs @@ -19,7 +19,7 @@ fn test_single_byte_execution_request_block_is_rejected() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -28,6 +28,7 @@ fn test_single_byte_execution_request_block_is_rejected() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/execution_requests/protocol_params.rs b/node/src/tests/execution_requests/protocol_params.rs index 287fdc03..f5916f86 100644 --- a/node/src/tests/execution_requests/protocol_params.rs +++ b/node/src/tests/execution_requests/protocol_params.rs @@ -12,7 +12,7 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -21,6 +21,7 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -158,7 +159,7 @@ fn test_protocol_param_allowed_timestamp_future() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -168,6 +169,7 @@ fn test_protocol_param_allowed_timestamp_future() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -320,7 +322,7 @@ fn test_protocol_param_treasury_address() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); let executor = Runner::from(cfg); @@ -328,6 +330,7 @@ fn test_protocol_param_treasury_address() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -505,7 +508,7 @@ fn test_protocol_param_max_deposits_per_epoch() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -514,6 +517,7 @@ fn test_protocol_param_max_deposits_per_epoch() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -660,7 +664,7 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -669,6 +673,7 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -827,7 +832,7 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -836,6 +841,7 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1051,7 +1057,7 @@ fn test_stake_increase_topup_keeps_active_validator() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -1060,6 +1066,7 @@ fn test_stake_increase_topup_keeps_active_validator() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1277,7 +1284,7 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -1286,6 +1293,7 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/execution_requests/validator_set.rs b/node/src/tests/execution_requests/validator_set.rs index d24fbf4d..a605bddb 100644 --- a/node/src/tests/execution_requests/validator_set.rs +++ b/node/src/tests/execution_requests/validator_set.rs @@ -20,7 +20,7 @@ fn test_added_validators_at_epoch_boundary() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -29,6 +29,7 @@ fn test_added_validators_at_epoch_boundary() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -235,7 +236,7 @@ fn test_removed_validators_at_epoch_boundary() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -244,6 +245,7 @@ fn test_removed_validators_at_epoch_boundary() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: true, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/execution_requests/withdrawals.rs b/node/src/tests/execution_requests/withdrawals.rs index eca415c9..44a5c47e 100644 --- a/node/src/tests/execution_requests/withdrawals.rs +++ b/node/src/tests/execution_requests/withdrawals.rs @@ -14,7 +14,7 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(3); @@ -23,6 +23,7 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -224,7 +225,7 @@ fn test_full_exit_withdrawal_removes_validator_and_pays_out() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -233,6 +234,7 @@ fn test_full_exit_withdrawal_removes_validator_and_pays_out() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -413,7 +415,7 @@ fn test_multiple_partial_withdrawals_paid_out_clamped_to_minimum() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -422,6 +424,7 @@ fn test_multiple_partial_withdrawals_paid_out_clamped_to_minimum() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -619,7 +622,7 @@ fn test_withdrawal_wrong_source_address_rejected() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -628,6 +631,7 @@ fn test_withdrawal_wrong_source_address_rejected() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -787,7 +791,7 @@ fn test_withdrawal_nonexistent_validator_ignored() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -796,6 +800,7 @@ fn test_withdrawal_nonexistent_validator_ignored() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -969,7 +974,7 @@ fn test_withdrawal_during_onboarding_aborts() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -978,6 +983,7 @@ fn test_withdrawal_during_onboarding_aborts() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1178,7 +1184,7 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(44); @@ -1187,6 +1193,7 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1383,7 +1390,7 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(42); @@ -1392,6 +1399,7 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1612,7 +1620,7 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(43); @@ -1621,6 +1629,7 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -1821,7 +1830,7 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(43); @@ -1830,6 +1839,7 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -2071,7 +2081,7 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -2080,6 +2090,7 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -2299,7 +2310,7 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(0); @@ -2308,6 +2319,7 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), @@ -2532,7 +2544,7 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; let cfg = deterministic::Config::default().with_seed(1); @@ -2541,6 +2553,7 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), diff --git a/node/src/tests/mod.rs b/node/src/tests/mod.rs index d06d3fa0..9c212c3c 100644 --- a/node/src/tests/mod.rs +++ b/node/src/tests/mod.rs @@ -2,6 +2,7 @@ mod checkpointing; mod engine; mod execution_requests; mod observer; +mod recovery; mod syncer; use crate::test_harness::common::run_until_height; @@ -14,7 +15,7 @@ fn test_20_blocks_and_verify() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 0.98, + success_rate: commonware_utils::probability!(0.98), }; run_until_height(5, 0, link.clone(), 20, true); } diff --git a/node/src/tests/observer.rs b/node/src/tests/observer.rs index 3aa84de2..a1c5fc78 100644 --- a/node/src/tests/observer.rs +++ b/node/src/tests/observer.rs @@ -34,7 +34,7 @@ fn test_observer_reaches_end_height() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; let cfg = deterministic::Config::default().with_seed(0); @@ -44,6 +44,7 @@ fn test_observer_reaches_end_height() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(total_nodes as usize * 10), @@ -237,7 +238,7 @@ fn test_observer_backfills_from_parent_validator() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; let cfg = deterministic::Config::default().with_seed(0); @@ -247,6 +248,7 @@ fn test_observer_backfills_from_parent_validator() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(total_nodes as usize * 10), diff --git a/node/src/tests/recovery.rs b/node/src/tests/recovery.rs new file mode 100644 index 00000000..eedaa74a --- /dev/null +++ b/node/src/tests/recovery.rs @@ -0,0 +1,230 @@ +//! Real finalizer + syncer recovery, using the production immutable archives. +use crate::engine::Engine; +use crate::test_harness::common::{ + GENESIS_HASH, SimulatedOracle, get_default_engine_config, get_initial_state, link_validators, + register_validators, +}; +use crate::test_harness::mock_engine_client::{MockEngineClient, MockEngineNetwork}; +use commonware_actor::Feedback; +use commonware_consensus::{Reporter, types::Height}; +use commonware_cryptography::Signer; +use commonware_formatting::from_hex; +use commonware_macros::test_traced; +use commonware_math::algebra::Random; +use commonware_p2p::simulated::{self, Link, Network, Oracle}; +use commonware_runtime::{Clock, Runner as _, Supervisor as _, deterministic}; +use commonware_utils::NZUsize; +use rand::{SeedableRng, rngs::StdRng}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use summit_finalizer::FinalizerMailbox; +use summit_types::{ + Block, PrivateKey, PublicKey, consensus_state::ConsensusState, keystore::KeyStore, + scheme::MultisigScheme, +}; + +const STOP: u64 = 24; + +struct Fixture { + key: KeyStore, + validators: Vec<(PublicKey, commonware_cryptography::bls12381::PublicKey)>, + genesis: [u8; 32], + initial: ConsensusState, + client: MockEngineClient, + expected_root: ([u8; 32], u64), +} + +fn network(context: deterministic::Context) -> Oracle { + let (network, oracle) = Network::new( + context, + simulated::Config { + max_peers_per_set: NZUsize!(2177), + max_size: 1024 * 1024, + disconnect_on_block: true, + tracked_peer_sets: NZUsize!(40), + }, + ); + network.start(); + oracle +} + +/// Forward actual blocks/acks to the real finalizer, but hold the replay tail +/// when modelling a second crash. Keeping the updates alive also holds their +/// acknowledgements, rather than accidentally shutting the syncer down. +#[derive(Clone)] +struct ReplayReporter { + finalizer: FinalizerMailbox, + through: u64, + delivered: Arc>>, + held: Vec>, +} + +impl Reporter for ReplayReporter { + type Activity = summit_syncer::Update; + + fn report(&mut self, update: Self::Activity) -> Feedback { + if let summit_syncer::Update::FinalizedBlock((block, _), _) = &update { + self.delivered.lock().unwrap().push(block.height()); + if block.height() > self.through { + self.held.push(update); + return Feedback::Ok; + } + } + self.finalizer.report(update) + } +} + +#[test_traced("WARN")] +fn mid_epoch_restart_replays_from_finalizer_and_survives_another_crash() { + let (fixture, recovered) = deterministic::Runner::timed(Duration::from_secs(300)) + .start_and_recover(|context| async move { + let mut oracle = network(context.child("network")); + let mut keys: Vec<_> = (0..4) + .map(|seed| { + let mut rng = StdRng::seed_from_u64(seed); + KeyStore { + node_key: PrivateKey::random(&mut rng), + consensus_key: commonware_cryptography::bls12381::PrivateKey::random( + &mut rng, + ), + } + }) + .collect(); + keys.sort_by_key(|key| key.node_key.public_key()); + let validators: Vec<_> = keys + .iter() + .map(|key| (key.node_key.public_key(), key.consensus_key.public_key())) + .collect(); + let public_keys: Vec<_> = validators.iter().map(|(key, _)| key.clone()).collect(); + let mut registrations = register_validators(&oracle, &public_keys).await; + link_validators( + &mut oracle, + &public_keys, + Link { + latency: Duration::from_millis(10), + jitter: Duration::from_millis(1), + success_rate: commonware_utils::probability!(1.0), + }, + None, + ) + .await; + let genesis: [u8; 32] = from_hex(GENESIS_HASH).unwrap().try_into().unwrap(); + let initial = get_initial_state(genesis, &validators, None, None, 32_000_000_000); + let clients = MockEngineNetwork::new(genesis, Some(STOP)); + let key = KeyStore { + node_key: keys[0].node_key.clone(), + consensus_key: keys[0].consensus_key.clone(), + }; + let mut primary = None; + for (index, key) in keys.into_iter().enumerate() { + let public_key = key.node_key.public_key(); + let uid = format!("recovery_{index}"); + let client = clients.create_client(uid.clone()); + let config = get_default_engine_config( + client.clone(), + SimulatedOracle::new(oracle.clone()), + uid.clone(), + genesis, + "_SUMMIT".into(), + key, + validators.clone(), + initial.clone(), + ); + let engine = + Engine::new(context.child("engine").with_attribute("uid", uid), config).await; + assert_eq!(engine.sync_start.height, 0); + if index == 0 { + primary = Some(( + client, + engine.finalizer_mailbox.clone(), + engine.syncer_mailbox.clone(), + )); + } + let (pending, recovered, resolver, broadcast, backfill) = + registrations.remove(&public_key).unwrap(); + engine.start(pending, recovered, resolver, broadcast, backfill); + } + let (client, finalizer, mut syncer) = primary.unwrap(); + while syncer.get_processed_height().await != Some(Height::new(STOP)) { + context.sleep(Duration::from_millis(10)).await; + } + assert_eq!(finalizer.get_latest_height().await, STOP); + assert!( + syncer.get_block(1u64).await.is_some(), + "genesis startup must deliver height 1 onward" + ); + Fixture { + key, + validators, + genesis, + initial, + client, + expected_root: finalizer.get_state_root().await, + } + }); + + // Runtime recovery drops all actors and reopens only durable storage. The + // finalizer restores the epoch boundary at 19, behind live acknowledgements + // at 24. Interrupt this first replay at 22 (still short of a durable boundary). + let (fixture, recovered) = + deterministic::Runner::from(recovered).start_and_recover(|context| async move { + replay(&context, &fixture, 22).await; + fixture + }); + deterministic::Runner::from(recovered).start(|context| async move { + replay(&context, &fixture, STOP).await; + }); +} + +async fn replay(context: &deterministic::Context, fixture: &Fixture, through: u64) { + let oracle = network(context.child("network")); + let public_key = fixture.key.node_key.public_key(); + let mut registrations = register_validators(&oracle, std::slice::from_ref(&public_key)).await; + let config = get_default_engine_config( + fixture.client.clone(), + SimulatedOracle::new(oracle), + "recovery_0".into(), + fixture.genesis, + "_SUMMIT".into(), + KeyStore { + node_key: fixture.key.node_key.clone(), + consensus_key: fixture.key.consensus_key.clone(), + }, + fixture.validators.clone(), + fixture.initial.clone(), + ); + let engine = Engine::new(context.child("engine"), config).await; + // This asserts real database recovery, rather than supplying an invented + // SyncStart to a mock application. Live progress must not override it. + assert_eq!(engine.sync_start.height, 19); + let finalizer = engine.finalizer_mailbox.clone(); + let syncer = engine.syncer_mailbox.clone(); + let (_, _, _, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + let resolver_config = engine.backfill_resolver_config(); + let resolver = + summit_syncer::resolver::p2p::init(context.child("backfill"), resolver_config, backfill); + engine.buffer.start(broadcast); + engine.finalizer.start(engine.orchestrator_mailbox); + let delivered = Arc::new(Mutex::new(Vec::new())); + engine.syncer.start( + ReplayReporter { + finalizer: engine.finalizer_mailbox, + through, + delivered: delivered.clone(), + held: Vec::new(), + }, + engine.buffer_mailbox, + resolver, + engine.sync_start, + engine.checkpoint, + ); + while syncer.get_processed_height().await != Some(Height::new(through)) { + context.sleep(Duration::from_millis(1)).await; + } + assert_eq!(finalizer.get_latest_height().await, through); + assert_eq!(delivered.lock().unwrap()[0], 20); + if through == STOP { + assert_eq!(*delivered.lock().unwrap(), (20..=STOP).collect::>()); + assert_eq!(finalizer.get_state_root().await, fixture.expected_root); + } +} diff --git a/node/src/tests/syncer.rs b/node/src/tests/syncer.rs index dd9fca47..1748c01d 100644 --- a/node/src/tests/syncer.rs +++ b/node/src/tests/syncer.rs @@ -28,7 +28,7 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -38,6 +38,7 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times @@ -255,7 +256,7 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { let link = Link { latency: Duration::from_millis(80), jitter: Duration::from_millis(10), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; // Create context let cfg = deterministic::Config::default().with_seed(0); @@ -265,6 +266,7 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { let (network, mut oracle) = Network::new( context.child("network"), simulated::Config { + max_peers_per_set: commonware_utils::NZUsize!(2177), max_size: 1024 * 1024, disconnect_on_block: false, tracked_peer_sets: NZUsize!(n as usize * 10), // Each engine may subscribe multiple times diff --git a/orchestrator/src/actor.rs b/orchestrator/src/actor.rs index fdd5f368..64e6f94e 100644 --- a/orchestrator/src/actor.rs +++ b/orchestrator/src/actor.rs @@ -96,7 +96,6 @@ where timeout_retry: Duration, fetch_timeout: Duration, activity_timeout: ViewDelta, - skip_timeout: ViewDelta, _strategy: std::marker::PhantomData, } @@ -132,7 +131,6 @@ where timeout_retry: config.timeout_retry, fetch_timeout: config.fetch_timeout, activity_timeout: config.activity_timeout, - skip_timeout: config.skip_timeout, _strategy: std::marker::PhantomData, }, Mailbox::new(sender), @@ -376,11 +374,11 @@ where certification_timeout: self.certification_timeout, timeout_retry: self.timeout_retry, fetch_timeout: self.fetch_timeout, - activity_timeout: self.activity_timeout, - skip_timeout: self.skip_timeout, - fetch_concurrent: NZUsize!(2), + view_retention: self.activity_timeout, + skip: simplex::SkipPolicy::Disabled, + track_historical_votes: true, page_cache: self.page_cache.clone(), - forwarding: simplex::ForwardingPolicy::SilentVoters, + forward: simplex::ForwardPolicy::SilentVoters, }, ); diff --git a/rpc/tests/utils.rs b/rpc/tests/utils.rs index 54d1a305..bad4ba45 100644 --- a/rpc/tests/utils.rs +++ b/rpc/tests/utils.rs @@ -140,7 +140,10 @@ pub fn create_test_finalizer_mailbox( let _ = response.send(ConsensusStateResponse::MaxWithdrawalsPerEpoch(16)); } ConsensusStateRequest::GetObserversPerValidator => { - let _ = response.send(ConsensusStateResponse::ObserversPerValidator(0)); + let _ = response.send(ConsensusStateResponse::ObserversPerValidator(16)); + } + ConsensusStateRequest::GetMaxValidatorCount => { + let _ = response.send(ConsensusStateResponse::MaxValidatorCount(256)); } ConsensusStateRequest::GetMinimumValidatorCount => { let _ = response.send(ConsensusStateResponse::MinimumValidatorCount(3)); @@ -275,10 +278,11 @@ pub fn create_test_finalized_header(epoch: u64) -> summit_types::FinalizedHeader let finalized = Finalization { proposal, certificate: BlsCertificate:: { - signers: commonware_cryptography::certificate::Signers::from( + signers: commonware_cryptography::certificate::Signers::new( 3, [0, 1, 2].map(Participant::new), - ), + ) + .unwrap(), signature: signature.into(), }, }; diff --git a/syncer/src/acks.rs b/syncer/src/acks.rs index aa38c0fd..9f3c6c27 100644 --- a/syncer/src/acks.rs +++ b/syncer/src/acks.rs @@ -113,9 +113,9 @@ mod tests { type TestBlock = MockBlock; fn digest(byte: u8) -> Digest { - let mut hasher = Sha256::new(); + let mut hasher = Sha256::default(); hasher.update(&[byte]); - hasher.finalize() + hasher.finalize().1 } fn pending_ack(height: u64, byte: u8) -> (PendingAck, Exact) { diff --git a/syncer/src/actor.rs b/syncer/src/actor.rs index 3285b592..0aa2bec2 100644 --- a/syncer/src/actor.rs +++ b/syncer/src/actor.rs @@ -9,7 +9,6 @@ use super::{ handler::{self, Annotation, Key, Request}, mailbox::{Identifier as BlockID, Mailbox, Message}, }, - stream::Stream, }; use crate::{Update, variant::Buffer as _}; use bytes::Bytes; @@ -21,7 +20,7 @@ use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::simplex::types::{ Finalization, Notarization, Subject, verify_certificates, }; -use commonware_consensus::types::{Epoch, Epocher, Height, Round, View, ViewDelta}; +use commonware_consensus::types::{Epoch, Epocher, Height, Round, ViewDelta}; use commonware_consensus::{Block, Epochable, Reporter, Viewable}; use commonware_cryptography::PublicKey; use commonware_cryptography::certificate::{Provider, Verifier as CertificateVerifier}; @@ -75,6 +74,7 @@ enum PooledSync { /// Pool of subscription waiter futures. Each resolves to the requested /// (digest, block) pair on delivery, or to the digest when the wait fails. type BlockWaiters = AbortablePool< + 'static, Result< (::Digest, B), ::Digest, @@ -161,8 +161,8 @@ where last_proposed_block: Option<(Round, B::Digest, B)>, // Current processed floor and any pending floor update floor: Floor, - // Application delivery cursor - stream: Stream, + // Startup recovery baseline; live acknowledgements and floor skips never advance it. + recovery_height: Height, // Pending application acknowledgements pending_acks: PendingAcks, // Highest known finalized height @@ -178,9 +178,10 @@ where // Prunable cache cache: cache::Manager, // Finalizations stored by height - finalizations_by_height: FC, - // Finalized blocks stored by height - finalized_blocks: FB, + finalizations_by_height: Option, + // Finalized blocks stored by height. None while a consuming mutation owns + // the handle; a failed/cancelled mutation must not leave a usable store. + finalized_blocks: Option, // ---------- Metrics ---------- // Latest height metric @@ -201,6 +202,7 @@ where A: Acknowledgement, { /// Create a new application actor. + #[commonware_macros::boxed] pub async fn init( context: E, finalizations_by_height: FC, @@ -223,15 +225,6 @@ where ) .await; - // Initialize metadata tracking application progress - let application_metadata_partition = - format!("{}-application-metadata", config.partition_prefix); - let stream = Stream::new( - context.child("application_metadata"), - &application_metadata_partition, - ) - .await; - // Create metrics let finalized_height = context.gauge("finalized_height", "Finalized height of application"); let processed_height = context.gauge("processed_height", "Processed height of application"); @@ -250,15 +243,15 @@ where strategy: config.strategy, last_proposed_block: None, floor: Floor::resolved(None, Round::zero()), - stream, + recovery_height: Height::zero(), pending_acks: PendingAcks::new(config.max_pending_acks.get()), tip: Height::zero(), block_subscriptions: BTreeMap::new(), dispatch_gate: DispatchGate::default(), pending_notarized_reports: BTreeSet::new(), cache, - finalizations_by_height, - finalized_blocks, + finalizations_by_height: Some(finalizations_by_height), + finalized_blocks: Some(finalized_blocks), finalized_height, processed_height, }, @@ -310,45 +303,100 @@ where epoch: sync_epoch, view: sync_view, } = sync_start; - self.stream.acknowledge(Height::new(sync_height)); - self.floor.set_processed_height(Height::new(sync_height)); - self.floor - .set_processed_round(Round::new(Epoch::new(sync_epoch), View::new(sync_view))); - self.tip = Height::new(sync_height); - info!(sync_height, sync_epoch, sync_view, "syncer initialized"); - - // If we have a checkpoint, finalize the last block to complete the checkpoint + // Replay starts after the state actually selected by the finalizer, not + // after independently persisted delivery acknowledgements. Legacy + // *-application-metadata partitions are intentionally never opened. + let recovered_height = Height::new(sync_height); + self.recovery_height = recovered_height; + self.update_processed_height(recovered_height, &mut resolver); + let recovered_round = self.recover_processed_round(recovered_height).await; + self.floor.set_processed_round(recovered_round); + self.tip = recovered_height; + let _ = self.finalized_height.try_set(recovered_height.get()); + info!(sync_height, sync_epoch, sync_view, processed_height = %recovered_height, "syncer initialized from finalizer state"); + + // Keep the durable import's terminal block/certificate available for replay. if let Some(checkpoint) = checkpoint { - let height = checkpoint.last_block.height(); - let last_block_digest = checkpoint.last_block.digest(); - let finalization = checkpoint.finalized_header.map(|h| h.into_finalization()); - // Defense in depth: last_block and finalized_header arrive as - // independent artifacts. The finalization certifies a specific block - // digest, so refuse to complete the checkpoint with a finalization - // that certifies a different block than the supplied last_block. The - // node startup path already binds these against the verified header - // chain; asserting it here keeps the boundary safe for any caller. - if let Some(finalization) = finalization.as_ref() { + let checkpoint_floor = checkpoint.processed_height; + let height = Height::new(checkpoint.finalized_header.header().height()); + let commitment = checkpoint.finalized_header.finalization().proposal.payload; + assert_eq!( + checkpoint_floor.get().checked_add(1), + Some(height.get()), + "checkpoint terminal must follow imported state" + ); + assert!( + checkpoint_floor.get() <= sync_height, + "checkpoint skip has no backing finalizer state" + ); + assert_eq!( + checkpoint.finalized_header.header().computed_digest(), + commitment, + "checkpoint header/certificate mismatch" + ); + let finalization = checkpoint.finalized_header.into_finalization(); + if let Some(block) = &checkpoint.last_block { + assert_eq!(block.digest(), commitment, "checkpoint last_block mismatch"); + } + let stored = self.get_finalized_block(height).await; + if let Some(block) = &stored { + assert_eq!( + block.digest(), + commitment, + "checkpoint anchor conflicts with stored data" + ); + } + if let Some(certificate) = self.get_finalization_by_height(height).await { + assert_eq!( + certificate.proposal.payload, commitment, + "checkpoint anchor conflicts with stored certificate" + ); + } + if height > recovered_height { assert!( - finalization.proposal.payload == last_block_digest, - "checkpoint finalization certifies a different block than last_block" + is_last_block_of_epoch(&self.epocher, height.get()), + "checkpoint anchor must be epoch-terminal" ); + if let Some(block) = checkpoint.last_block.or(stored) { + assert!( + self.store_finalization( + height, + commitment, + block, + Some(finalization), + &mut application + ) + .await, + "checkpoint anchor conflicts with stored data" + ); + } else { + // Pin the certificate before fetching by digest. A certificate + // without a block must never cause the successor to be skipped. + let round = finalization.round(); + self.cache + .put_finalization(round, commitment, finalization.clone()) + .await; + let certificates = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + self.finalizations_by_height = Some( + certificates + .put(height, commitment, finalization) + .await + .expect("failed to store checkpoint certificate"), + ); + self.floor + .fetch_if_permitted( + &mut resolver, + Request::finalized_block_by_height(commitment, height), + ) + .ignore(); + } + self.sync_finalized().await; } - self.store_finalization( - height, - last_block_digest, - checkpoint.last_block, - finalization, - &mut application, - ) - .await; - self.sync_finalized().await; } - let _ = self - .processed_height - .try_set(self.floor.processed_height().get()); - // Create a local pool for waiter futures. let mut waiters = BlockWaiters::::default(); @@ -359,8 +407,8 @@ where let tip = self.get_latest().await; if let Some((height, commitment)) = tip { application.report(Update::Tip(height.get(), commitment)); - self.tip = height; - let _ = self.finalized_height.try_set(height.get()); + self.tip = self.tip.max(height); + let _ = self.finalized_height.try_set(self.tip.get()); } // Load persisted cache epochs so find_block can discover blocks @@ -387,6 +435,14 @@ where bs.subscribers.retain(|tx| !tx.is_closed()); !bs.subscribers.is_empty() }); + // A processed round can supersede a pending anchor whose fetch + // was pruned. Release it so repair/dispatch cannot remain stuck. + if self.floor.take_superseded_anchor().is_some() { + if self.try_repair_gaps(&mut buffer, &mut resolver, &mut application).await { + self.sync_finalized().await; + } + self.try_dispatch_blocks(&mut application, &mut resolver).await; + } }, on_stopped => { debug!("context shutdown, stopping syncer"); @@ -425,7 +481,7 @@ where } } }, - // Handle application acknowledgements (drain all ready acks, sync once) + // Handle application acknowledgements (drain all ready acks) result = self.pending_acks.current() => { if !self.handle_ack(result, &mut application, &mut resolver).await { return; @@ -494,20 +550,13 @@ where } } - // Opportunistically drain any additional already-ready acks so we - // can persist one metadata sync for the whole batch. + // Opportunistically drain any additional already-ready acks. let Some(next) = self.pending_acks.pop_ready() else { break; }; pending = Some(next); } - // Persist buffered processed-height updates once after draining all ready acks. - if let Err(e) = self.stream.sync().await { - error!(?e, "failed to sync application progress"); - return false; - } - // Fill the pipeline self.try_dispatch_blocks(application, resolver).await; true @@ -519,7 +568,7 @@ where message: Message, resolver: &mut R, waiters: &mut BlockWaiters, - syncs: &mut Pool>, + syncs: &mut Pool<'static, PooledSync>, buffer: &mut buffered::Mailbox, application: &mut impl Reporter>, ) where @@ -542,6 +591,8 @@ where let info = match identifier { BlockID::Digest(commitment) => self .finalized_blocks + .as_ref() + .expect("block archive unavailable") .get(ArchiveID::Key(&commitment)) .await .ok() @@ -775,7 +826,7 @@ where response.send_lossy(finalization); } Message::GetProcessedHeight { response } => { - response.send_lossy(self.stream.processed_height()); + response.send_lossy(Some(self.floor.processed_height())); } Message::HintFinalized { height, targets } => { // Skip if finalization is already available locally. @@ -811,25 +862,12 @@ where // the request as we wouldn't know when to drop it, and the request may // never complete if the block is not finalized. if let Some(round) = round { - if self - .floor + // Resolver retention controls remote acquisition, not local + // availability. Keep the subscription even when this fetch + // is denied: later broadcast/ingestion can still satisfy it. + self.floor .fetch_if_permitted(resolver, Request::notarized(round)) - .denied() - { - warn!( - ?round, - ?commitment, - last_processed_round = ?self.floor.processed_round(), - last_processed_height = %self.floor.processed_height(), - tip = %self.tip, - "subscription for block in past round that wasn't finalized - possible notarize-nullify race" - ); - - #[cfg(feature = "prom")] - counter!("syncer_stuck_subscription_total").increment(1); - - return; - } + .ignore(); // The fetch (with notarization) was issued. If this is a valid // view, this request should be fine to keep open until // resolution or pruning (even if the oneshot is canceled). @@ -886,7 +924,7 @@ where message: handler::Message, resolver_rx: &mut handler::Receiver, resolver: &mut R, - syncs: &mut Pool>, + syncs: &mut Pool<'static, PooledSync>, buffer: &mut buffered::Mailbox, application: &mut impl Reporter>, ) where @@ -1109,23 +1147,31 @@ where .take_pending_anchor() .expect("pending floor anchor missing"); let round = finalization.round(); - try_join!( + let blocks = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let certificates = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let (blocks, certificates) = try_join!( async { - self.finalized_blocks + blocks .put(block.clone()) .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) + .map_err(|e| Box::new(e) as BoxedError) }, async { - self.finalizations_by_height + certificates .put(height, commitment, finalization) .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) - } + .map_err(|e| Box::new(e) as BoxedError) + }, ) .expect("failed to store floor anchor"); + self.finalized_blocks = Some(blocks); + self.finalizations_by_height = Some(certificates); self.sync_finalized().await; self.notify_subscribers(commitment, &block); @@ -1143,16 +1189,13 @@ where self.update_processed_height(dispatch_floor, resolver); self.update_processed_round_floor(dispatch_floor, round, resolver) .await; - self.stream - .sync() - .await - .expect("failed to sync floor metadata"); // Drop all pending acknowledgement waiters so any in-flight application // acks for blocks below the new floor cannot rewrite the processed floor. self.pending_acks.clear(); - // The floor is durable, so cache/finalized data below it can be pruned. + // Prune temporary data below the live floor, but retain finalized + // replay history above the startup recovery baseline. self.prune_after_floor(height) .await .expect("failed to prune data below floor"); @@ -1244,7 +1287,7 @@ where } else { if annotations .iter() - .any(|annotation| matches!(annotation, Annotation::Certified { .. })) + .any(|annotation| matches!(annotation, Annotation::Certified { height: bound } if height <= *bound)) && height > self.floor.processed_height() && let Some(bounds) = self.epocher.containing(height) { @@ -1318,6 +1361,7 @@ where return false; } delivers.push(PendingVerification::Finalized { + scoped: scheme, finalization, block, response, @@ -1369,6 +1413,7 @@ where return false; } delivers.push(PendingVerification::Notarized { + scoped: scheme, notarization, block, response, @@ -1427,14 +1472,13 @@ where by_epoch.entry(epoch).or_default().push(i); } - // Batch verify each epoch group. - for (epoch, indices) in &by_epoch { - let Some(scheme) = self.provider.scoped(*epoch) else { - continue; - }; + // Keep using the scope captured at admission, even if the provider + // retired this epoch while the parsed deliveries were queued. + for indices in by_epoch.values() { + let scheme = delivers[indices[0]].scoped(); let group: Vec<_> = indices.iter().map(|&i| certs[i]).collect(); let results = - verify_certificates(self.context.as_mut(), &scheme, &group, &self.strategy); + verify_certificates(self.context.as_mut(), scheme, &group, &self.strategy); for (j, &idx) in indices.iter().enumerate() { verified[idx] = results[j]; } @@ -1458,6 +1502,7 @@ where finalization, block, response, + .. } => { // Valid finalization received. response.send_lossy(true); @@ -1491,6 +1536,7 @@ where notarization, block, response, + .. } => { // Valid notarization received. response.send_lossy(true); @@ -1568,8 +1614,8 @@ where /// Attempt to dispatch finalized blocks to the application until the pipeline is full /// or no more blocks are available. /// - /// This does NOT advance the processed floor height or sync metadata. It only - /// sends blocks to the application and enqueues pending acks. Metadata is + /// This does NOT advance the processed floor height. It only sends blocks + /// to the application and enqueues pending acks. The in-memory cursor is /// updated later when acks arrive and [`Self::handle_ack`] runs. /// /// Acks are processed in FIFO order so the processed floor height always @@ -1590,7 +1636,7 @@ where while self.pending_acks.has_capacity() { let next_height = self .pending_acks - .next_dispatch_height(self.stream.next_height()); + .next_dispatch_height(self.floor.processed_height().next()); if barrier.is_some_and(|lowest| next_height >= lowest) { return; } @@ -1686,21 +1732,26 @@ where /// [`Self::store_finalization`] / [`Self::try_repair_gaps`] writes, before yielding back /// to the loop. async fn sync_finalized(&mut self) { - if let Err(e) = try_join!( - async { - self.finalized_blocks.sync().await.map_err(Box::new)?; - Ok::<_, BoxedError>(()) - }, + let blocks = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let certificates = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let (blocks, certificates) = try_join!( + async { blocks.sync().await.map_err(|e| Box::new(e) as BoxedError) }, async { - self.finalizations_by_height + certificates .sync() .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) + .map_err(|e| Box::new(e) as BoxedError) }, - ) { - panic!("failed to sync finalization archives: {e}"); - } + ) + .expect("failed to sync finalization archives"); + self.finalized_blocks = Some(blocks); + self.finalizations_by_height = Some(certificates); self.dispatch_gate.clear(); } @@ -1709,25 +1760,39 @@ where /// Stores with a native non-blocking `start_sync` keep the actor responsive /// while durability is pending. Stores without one may complete the sync /// before returning the handle, as permitted by the storage trait. - async fn start_finalized_sync(&mut self, round: Round, syncs: &mut Pool>) { + async fn start_finalized_sync( + &mut self, + round: Round, + syncs: &mut Pool<'static, PooledSync>, + ) { let Some(seq) = self.dispatch_gate.adopt() else { return; }; - let (blocks, finalizations) = try_join!( + let block_archive = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let certificate_archive = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let ((block_archive, blocks), (certificate_archive, finalizations)) = try_join!( async { - let handle = self.finalized_blocks.start_sync().await.map_err(Box::new)?; - Ok::<_, BoxedError>(handle) + block_archive + .start_sync() + .await + .map_err(|e| Box::new(e) as BoxedError) }, async { - let handle = self - .finalizations_by_height + certificate_archive .start_sync() .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(handle) + .map_err(|e| Box::new(e) as BoxedError) }, ) - .unwrap_or_else(|e| panic!("failed to start finalization archive sync: {e}")); + .expect("failed to start finalization archive sync"); + self.finalized_blocks = Some(block_archive); + self.finalizations_by_height = Some(certificate_archive); syncs.push(async move { let (blocks, finalizations) = join( blocks.durable(round, "finalized blocks"), @@ -1746,6 +1811,8 @@ where async fn get_finalized_block(&self, height: Height) -> Option { match self .finalized_blocks + .as_ref() + .expect("block archive unavailable") .get(ArchiveID::Index(height.get())) .await { @@ -1761,6 +1828,8 @@ where ) -> Option> { match self .finalizations_by_height + .as_ref() + .expect("certificate archive unavailable") .get(ArchiveID::Index(height.get())) .await { @@ -1771,7 +1840,13 @@ where /// Check whether a finalization exists at `height` without fetching it. async fn has_finalization_by_height(&self, height: Height) -> bool { - match self.finalizations_by_height.has(height).await { + match self + .finalizations_by_height + .as_ref() + .expect("certificate archive unavailable") + .has(height) + .await + { Ok(has) => has, Err(e) => panic!("failed to check finalization: {e}"), } @@ -1873,31 +1948,49 @@ where return false; } + // An imported checkpoint can pin a certificate before its block arrives. + // Do not pair that certificate with a different block at the same height. + if let Some(existing) = self.get_finalization_by_height(height).await + && existing.proposal.payload != commitment + { + error!(%height, ?commitment, "block conflicts with stored finalization certificate"); + return false; + } + self.notify_subscribers(commitment, &block); #[cfg(feature = "prom")] let store_start = Instant::now(); // In parallel, update the finalized blocks and finalizations archives - if let Err(e) = try_join!( - // Update the finalized blocks archive + let blocks = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let certificates = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let (blocks, certificates) = try_join!( async { - self.finalized_blocks.put(block).await.map_err(Box::new)?; - Ok::<_, BoxedError>(()) + blocks + .put(block) + .await + .map_err(|e| Box::new(e) as BoxedError) }, - // Update the finalizations archive (if provided) async { - if let Some(finalization) = finalization { - self.finalizations_by_height + match finalization { + Some(finalization) => certificates .put(height, commitment, finalization) .await - .map_err(Box::new)?; + .map_err(|e| Box::new(e) as BoxedError), + None => Ok(certificates), } - Ok::<_, BoxedError>(()) - } - ) { - panic!("failed to finalize: {e}"); - } + }, + ) + .expect("failed to finalize"); + self.finalized_blocks = Some(blocks); + self.finalizations_by_height = Some(certificates); self.dispatch_gate.defer(height); @@ -1927,9 +2020,52 @@ where true } + /// Recover the round floor from archives at the selected finalizer height. + /// A certificate-only successor must not suppress fetching its missing block. + async fn recover_processed_round(&self, height: Height) -> Round { + let certificates = self + .finalizations_by_height + .as_ref() + .expect("certificate archive unavailable"); + let latest = certificates + .ranges_from(Height::zero()) + .filter_map(|(start, end)| (start <= height).then_some(end.min(height))) + .max(); + let mut round = match latest { + Some(height) => self + .get_finalization_by_height(height) + .await + .expect("processed certificate missing") + .round(), + None => Round::zero(), + }; + if height.get() == u64::MAX { + return round; + } + let successor = height.next(); + if let (Some(block), Some(finalization)) = join( + self.get_finalized_block(successor), + self.get_finalization_by_height(successor), + ) + .await + { + assert_eq!( + block.digest(), + finalization.proposal.payload, + "successor block/certificate mismatch" + ); + round = round.max(finalization.round()); + } + round + } + /// Get the latest finalized block information (height and commitment tuple). async fn get_latest(&mut self) -> Option<(Height, B::Digest)> { - let height = self.finalizations_by_height.last_index()?; + let height = self + .finalizations_by_height + .as_ref() + .expect("certificate archive unavailable") + .last_index()?; let finalization = self .get_finalization_by_height(height) .await @@ -1954,7 +2090,13 @@ where return Some(block); } // Check finalized blocks. - match self.finalized_blocks.get(ArchiveID::Key(&commitment)).await { + match self + .finalized_blocks + .as_ref() + .expect("block archive unavailable") + .get(ArchiveID::Key(&commitment)) + .await + { Ok(block) => block, // may be None Err(e) => panic!("failed to get block: {e}"), } @@ -1984,7 +2126,12 @@ where let mut wrote = false; let start = self.floor.processed_height().next(); 'cache_repair: loop { - let (gap_start, Some(gap_end)) = self.finalized_blocks.next_gap(start) else { + let (gap_start, Some(gap_end)) = self + .finalized_blocks + .as_ref() + .expect("block archive unavailable") + .next_gap(start) + else { // No gaps detected return wrote; }; @@ -2046,6 +2193,8 @@ where // the `max_repair` quota. let missing_items = self .finalized_blocks + .as_ref() + .expect("block archive unavailable") .missing_items(start, self.max_repair.get()); let requests: Vec<_> = missing_items.into_iter().map(Request::finalized).collect(); if !requests.is_empty() { @@ -2056,13 +2205,11 @@ where wrote } - /// Buffers a processed height update in memory and metrics. Does NOT sync - /// to durable storage. Sync metadata after buffered updates to make them durable. + /// Updates process-local delivery progress and metrics, never pruning authority. fn update_processed_height(&mut self, height: Height, resolver: &mut R) where R: Resolver, Subscriber = Annotation>, { - self.stream.acknowledge(height); self.floor.set_processed_height(height); let _ = self .processed_height @@ -2114,59 +2261,104 @@ where )); } - /// Prunes finalized blocks and certificates below the given height. + /// Prunes finalized history only as far as the startup recovery baseline permits. async fn prune_finalized_archives(&mut self, height: Height) -> Result<(), BoxedError> { - try_join!( + let height = finalized_prune_cutoff(height, self.recovery_height); + let blocks = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let certificates = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let (blocks, certificates) = try_join!( async { - self.finalized_blocks + blocks .prune(height) .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) + .map_err(|e| Box::new(e) as BoxedError) }, async { - self.finalizations_by_height + certificates .prune(height) .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) - } + .map_err(|e| Box::new(e) as BoxedError) + }, )?; + self.finalized_blocks = Some(blocks); + self.finalizations_by_height = Some(certificates); Ok(()) } - /// Prunes finalized archives and height-indexed certified cache data below the durable floor. + /// Prunes temporary cache data below the live floor and finalized archives + /// below the recovery-safe cutoff. Archive section rounding may retain more. async fn prune_after_floor(&mut self, height: Height) -> Result<(), BoxedError> { + let archive_cutoff = finalized_prune_cutoff(height, self.recovery_height); let cache = &mut self.cache; - let finalized_blocks = &mut self.finalized_blocks; - let finalizations_by_height = &mut self.finalizations_by_height; - try_join!( + let finalized_blocks = self + .finalized_blocks + .take() + .expect("block archive unavailable"); + let finalizations_by_height = self + .finalizations_by_height + .take() + .expect("certificate archive unavailable"); + let (_, blocks, certificates) = try_join!( async { cache.prune_by_height(height).await; Ok::<_, BoxedError>(()) }, async { - finalized_blocks.prune(height).await.map_err(Box::new)?; - Ok::<_, BoxedError>(()) + finalized_blocks + .prune(archive_cutoff) + .await + .map_err(|e| Box::new(e) as BoxedError) }, async { finalizations_by_height - .prune(height) + .prune(archive_cutoff) .await - .map_err(Box::new)?; - Ok::<_, BoxedError>(()) + .map_err(|e| Box::new(e) as BoxedError) } )?; + self.finalized_blocks = Some(blocks); + self.finalizations_by_height = Some(certificates); Ok(()) } } +/// Archives delete strictly below the cutoff. Preserve every successor of the +/// selected startup state, regardless of live acknowledgements or floor skips. +fn finalized_prune_cutoff(requested: Height, recovery_height: Height) -> Height { + requested.min(Height::new(recovery_height.get().saturating_add(1))) +} + #[cfg(test)] mod tests { - use super::header_view_binds_to_round; - use commonware_consensus::types::FixedEpocher; + use super::{finalized_prune_cutoff, header_view_binds_to_round}; + use commonware_consensus::types::{FixedEpocher, Height}; use std::num::NonZeroU64; + #[test] + fn recovery_prune_cutoff_is_overflow_safe() { + for (requested, recovery, expected) in [ + (0, 0, 0), + (10, 0, 1), + (5, 10, 5), + (20, 10, 11), + (u64::MAX, u64::MAX - 2, u64::MAX - 1), + (u64::MAX, u64::MAX - 1, u64::MAX), + (u64::MAX, u64::MAX, u64::MAX), + (1, u64::MAX, 1), + ] { + assert_eq!( + finalized_prune_cutoff(Height::new(requested), Height::new(recovery)), + Height::new(expected), + ); + } + } + // Epoch length 10: epoch E spans heights [E*10, E*10 + 9], so the last block // of an epoch is E*10 + 9 (e.g. height 9 for epoch 0, height 19 for epoch 1). fn epocher() -> FixedEpocher { diff --git a/syncer/src/cache.rs b/syncer/src/cache.rs index de8b7d90..bba96298 100644 --- a/syncer/src/cache.rs +++ b/syncer/src/cache.rs @@ -9,7 +9,7 @@ use commonware_runtime::{ BufferPooler, Clock, Handle, Metrics, Spawner, Storage, buffer::paged::CacheRef, }; use commonware_storage::{ - archive::{self, Archive as _, Identifier, MultiArchive as _, prunable}, + archive::{Archive as _, Identifier, MultiArchive as _, prunable}, metadata::{self, Metadata}, translator::TwoCap, }; @@ -23,10 +23,8 @@ use std::{ }; use tracing::{debug, info}; -// The key used to store the current epoch in the metadata store. const CACHED_EPOCHS_KEY: u8 = 0; -/// Configuration parameters for prunable archives. pub(crate) struct Config { pub partition_prefix: String, pub prunable_items_per_section: NonZero, @@ -36,22 +34,21 @@ pub(crate) struct Config { pub key_page_cache: CacheRef, } -/// Prunable archives for a single epoch. +type NotarizationArchive = prunable::Archive>; +type FinalizationArchive = prunable::Archive>; + +/// Each handle is absent while a consuming mutation owns it. Failure or +/// cancellation never substitutes an empty archive for a lost handle. struct Cache< R: BufferPooler + Rng + Spawner + Metrics + Clock + GClock + Storage, B: Block, S: Scheme, > { - /// Verified blocks stored by view - verified_blocks: prunable::Archive, - /// Notarized blocks stored by view - notarized_blocks: prunable::Archive, - /// Certified blocks indexed by height and keyed by commitment. - certified_blocks: prunable::Archive, - /// Notarizations stored by view - notarizations: prunable::Archive>, - /// Finalizations stored by view - finalizations: prunable::Archive>, + verified_blocks: Option>, + notarized_blocks: Option>, + certified_blocks: Option>, + notarizations: Option>, + finalizations: Option>, } impl< @@ -60,48 +57,60 @@ impl< S: Scheme, > Cache { - /// Prune view-indexed archives to the given view. async fn prune_by_view(&mut self, min_view: View) { - match futures::try_join!( - self.verified_blocks.prune(min_view.get()), - self.notarized_blocks.prune(min_view.get()), - self.notarizations.prune(min_view.get()), - self.finalizations.prune(min_view.get()), - ) { - Ok(_) => debug!(min_view = %min_view, "pruned archives"), - Err(e) => panic!("failed to prune archives: {e}"), - } + let verified = self + .verified_blocks + .take() + .expect("verified archive unavailable"); + let notarized = self + .notarized_blocks + .take() + .expect("notarized archive unavailable"); + let notarizations = self + .notarizations + .take() + .expect("notarizations archive unavailable"); + let finalizations = self + .finalizations + .take() + .expect("finalizations archive unavailable"); + let (verified, notarized, notarizations, finalizations) = futures::try_join!( + verified.prune(min_view.get()), + notarized.prune(min_view.get()), + notarizations.prune(min_view.get()), + finalizations.prune(min_view.get()), + ) + .expect("failed to prune archives"); + self.verified_blocks = Some(verified); + self.notarized_blocks = Some(notarized); + self.notarizations = Some(notarizations); + self.finalizations = Some(finalizations); + debug!(%min_view, "pruned archives"); } - /// Prune height-indexed archives to the given height. async fn prune_by_height(&mut self, min_height: Height) { - self.certified_blocks - .prune(min_height.get()) - .await - .expect("failed to prune certified blocks"); + let archive = self + .certified_blocks + .take() + .expect("certified archive unavailable"); + self.certified_blocks = Some( + archive + .prune(min_height.get()) + .await + .expect("failed to prune certified blocks"), + ); } } -/// Manages prunable caches and their metadata. pub(crate) struct Manager< R: BufferPooler + Rng + Spawner + Metrics + Clock + GClock + Storage, B: Block, S: Scheme, > { - /// Context context: R, - - /// Configuration for underlying prunable archives cfg: Config, - - /// Codec configuration for block type block_codec_config: B::Cfg, - - /// Metadata store for recording which epochs may have data. The value is a tuple of the floor - /// and ceiling, the minimum and maximum epochs (inclusive) that may have data. - metadata: Metadata, - - /// A map from epoch to its cache + metadata: Option>, caches: BTreeMap>, } @@ -111,9 +120,8 @@ impl< S: Scheme, > Manager { - /// Initialize the cache manager and its metadata store. + #[commonware_macros::boxed] pub(crate) async fn init(context: R, cfg: Config, block_codec_config: B::Cfg) -> Self { - // Initialize metadata let metadata = Metadata::init( context.child("metadata"), metadata::Config { @@ -123,21 +131,16 @@ impl< ) .await .expect("failed to initialize metadata"); - - // We don't eagerly initialize any epoch caches here, they will be - // initialized on demand, otherwise there could be coordination issues - // around the scheme provider. Self { context, cfg, block_codec_config, - metadata, + metadata: Some(metadata), caches: BTreeMap::new(), } } - /// Load all persisted epoch caches so that `find_block` can discover - /// blocks written before the last shutdown. + /// Open persisted caches lazily only after the scheme provider is ready. pub(crate) async fn load_persisted_epochs(&mut self) { let (floor, ceiling) = self.get_metadata(); for e in floor.get()..=ceiling.get() { @@ -148,49 +151,42 @@ impl< } } - /// Retrieve the epoch range that may have data. fn get_metadata(&self) -> (Epoch, Epoch) { self.metadata + .as_ref() + .expect("cache metadata unavailable") .get(&CACHED_EPOCHS_KEY) .cloned() .unwrap_or((Epoch::zero(), Epoch::zero())) } - /// Set the epoch range that may have data. async fn set_metadata(&mut self, floor: Epoch, ceiling: Epoch) { - self.metadata - .put_sync(CACHED_EPOCHS_KEY, (floor, ceiling)) - .await - .expect("failed to write metadata"); + let metadata = self.metadata.take().expect("cache metadata unavailable"); + self.metadata = Some( + metadata + .put_sync(CACHED_EPOCHS_KEY, (floor, ceiling)) + .await + .expect("failed to write metadata"), + ); } - /// Get the cache for the given epoch, initializing it if it doesn't exist. - /// - /// If the epoch is less than the minimum cached epoch, then it has already been pruned, - /// and this will return `None`. async fn get_or_init_epoch(&mut self, epoch: Epoch) -> Option<&mut Cache> { - // If the cache exists, return it if self.caches.contains_key(&epoch) { return self.caches.get_mut(&epoch); } - - // If the epoch is less than the epoch floor, then it has already been pruned let (floor, ceiling) = self.get_metadata(); if epoch < floor { return None; } - - // Update the metadata (metadata-first is safe; init is idempotent) + // Metadata first: initialization is idempotent after a crash. if epoch > ceiling { self.set_metadata(floor, epoch).await; } - - // Initialize and return the epoch self.init_epoch(epoch).await; - self.caches.get_mut(&epoch) // Should always be Some + self.caches.get_mut(&epoch) } - /// Helper to initialize the cache for a given epoch. + #[commonware_macros::boxed] async fn init_epoch(&mut self, epoch: Epoch) { let context = self.context.child("epoch").with_attribute("epoch", epoch); let (verified_blocks, notarized_blocks, certified_blocks, notarizations, finalizations) = futures::join!( @@ -220,30 +216,29 @@ impl< &self.cfg, epoch, "notarizations", - S::certificate_codec_config_unbounded(), + S::certificate_codec_config_unbounded() ), Self::init_archive( &context, &self.cfg, epoch, "finalizations", - S::certificate_codec_config_unbounded(), + S::certificate_codec_config_unbounded() ), ); let existing = self.caches.insert( epoch, Cache { - verified_blocks, - notarized_blocks, - certified_blocks, - notarizations, - finalizations, + verified_blocks: Some(verified_blocks), + notarized_blocks: Some(notarized_blocks), + certified_blocks: Some(certified_blocks), + notarizations: Some(notarizations), + finalizations: Some(finalizations), }, ); assert!(existing.is_none(), "cache already exists for epoch {epoch}"); } - /// Helper to initialize an archive. async fn init_archive( ctx: &R, cfg: &Config, @@ -252,26 +247,31 @@ impl< codec_config: T::Cfg, ) -> prunable::Archive { let start = ctx.current(); - let archive_cfg = prunable::Config { - translator: TwoCap, - key_partition: format!("{}-cache-{epoch}-{name}-key", cfg.partition_prefix), - key_page_cache: cfg.key_page_cache.clone(), - value_partition: format!("{}-cache-{epoch}-{name}-value", cfg.partition_prefix), - items_per_section: cfg.prunable_items_per_section, - compression: None, - codec_config, - replay_buffer: cfg.replay_buffer, - key_write_buffer: cfg.key_write_buffer, - value_write_buffer: cfg.value_write_buffer, - }; - let archive = prunable::Archive::init(ctx.child(name), archive_cfg) - .await - .unwrap_or_else(|_| panic!("failed to initialize {name} archive")); + let archive = prunable::Archive::init( + ctx.child(name), + prunable::Config { + translator: TwoCap, + key_partition: format!("{}-cache-{epoch}-{name}-key", cfg.partition_prefix), + key_page_cache: cfg.key_page_cache.clone(), + value_partition: format!("{}-cache-{epoch}-{name}-value", cfg.partition_prefix), + metadata_partition: format!( + "{}-cache-{epoch}-{name}-metadata", + cfg.partition_prefix + ), + items_per_section: cfg.prunable_items_per_section, + compression: None, + codec_config, + replay_buffer: cfg.replay_buffer, + key_write_buffer: cfg.key_write_buffer, + value_write_buffer: cfg.value_write_buffer, + }, + ) + .await + .unwrap_or_else(|e| panic!("failed to initialize {name} archive: {e}")); info!(elapsed = ?ctx.current().duration_since(start).unwrap_or(Duration::ZERO), "restored {name} archive"); archive } - /// Add a verified block to the prunable archive and start syncing it. pub(crate) async fn put_verified( &mut self, round: Round, @@ -281,26 +281,25 @@ impl< let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { return Handle::ready(Ok(())); }; - let view = round.view().get(); - match cache.verified_blocks.has_at(view, &commitment).await { - Ok(true) => { - return Self::handle_start_result( - cache.verified_blocks.start_sync().await, - round, - "verified", - ); - } - Ok(false) => {} - Err(e) => panic!("failed to check verified blocks: {e}"), - } - let result = cache + let archive = cache .verified_blocks - .put_multi_start_sync(view, commitment, block) - .await; - Self::handle_start_result(result, round, "verified") + .take() + .expect("verified archive unavailable"); + let view = round.view().get(); + let result = if archive + .has_at(view, &commitment) + .await + .expect("failed to check verified blocks") + { + archive.start_sync().await + } else { + archive.put_multi_start_sync(view, commitment, block).await + }; + let (archive, handle) = result.expect("failed to persist verified block"); + cache.verified_blocks = Some(archive); + handle } - /// Add a certified block to the height-indexed archive. pub(crate) async fn put_certified( &mut self, epoch: Epoch, @@ -311,33 +310,29 @@ impl< let Some(cache) = self.get_or_init_epoch(epoch).await else { return; }; - - // A digest determines its height, so scoping the dedup to this height - // is exact and avoids fetching values. - match cache + if cache .certified_blocks + .as_ref() + .expect("certified archive unavailable") .has_at(height.get(), &commitment) .await + .expect("failed to check certified block") { - Ok(true) => return, - Ok(false) => {} - Err(e) => panic!("failed to check certified block: {e}"), + return; } - - match cache + let archive = cache .certified_blocks - .put_multi_sync(height.get(), commitment, block) - .await - { - Ok(()) => debug!(%height, "cached certified block"), - Err(archive::Error::AlreadyPrunedTo(_)) => { - debug!(%height, "certified block already pruned"); - } - Err(e) => panic!("failed to insert certified block: {e}"), - } + .take() + .expect("certified archive unavailable"); + // Below-floor puts are successful no-ops in Commonware 2026.9.0. + cache.certified_blocks = Some( + archive + .put_multi_sync(height.get(), commitment, block) + .await + .expect("failed to insert certified block"), + ); } - /// Add a notarized block to the prunable archive and start syncing it. pub(crate) async fn put_block( &mut self, round: Round, @@ -347,14 +342,18 @@ impl< let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { return Handle::ready(Ok(())); }; - let result = cache + let archive = cache .notarized_blocks + .take() + .expect("notarized archive unavailable"); + let (archive, handle) = archive .put_start_sync(round.view().get(), commitment, block) - .await; - Self::handle_start_result(result, round, "notarized") + .await + .expect("failed to persist notarized block"); + cache.notarized_blocks = Some(archive); + handle } - /// Add a notarization to the prunable archive and start syncing it. pub(crate) async fn put_notarization( &mut self, round: Round, @@ -364,14 +363,18 @@ impl< let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { return Handle::ready(Ok(())); }; - let result = cache + let archive = cache .notarizations + .take() + .expect("notarizations archive unavailable"); + let (archive, handle) = archive .put_start_sync(round.view().get(), commitment, notarization) - .await; - Self::handle_start_result(result, round, "notarization") + .await + .expect("failed to persist notarization"); + cache.notarizations = Some(archive); + handle } - /// Add a finalization to the prunable archive. pub(crate) async fn put_finalization( &mut self, round: Round, @@ -381,79 +384,63 @@ impl< let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { return; }; - let result = cache + let archive = cache .finalizations - .put_sync(round.view().get(), commitment, finalization) - .await; - Self::handle_result(result, round, "finalization"); - } - - /// Helper to debug cache results. - fn handle_result(result: Result<(), archive::Error>, round: Round, name: &str) { - match result { - Ok(_) => { - debug!(?round, name, "cached"); - } - Err(archive::Error::AlreadyPrunedTo(_)) => { - debug!(?round, name, "already pruned"); - } - Err(e) => { - panic!("failed to insert {name}: {e}"); - } - } - } - - fn handle_start_result( - result: Result, archive::Error>, - round: Round, - name: &str, - ) -> Handle<()> { - match result { - Ok(handle) => { - debug!(?round, name, "cache sync started"); - handle - } - Err(archive::Error::AlreadyPrunedTo(_)) => { - debug!(?round, name, "already pruned"); - Handle::ready(Ok(())) - } - Err(e) => panic!("failed to persist {name}: {e}"), - } + .take() + .expect("finalizations archive unavailable"); + cache.finalizations = Some( + archive + .put_sync(round.view().get(), commitment, finalization) + .await + .expect("failed to persist finalization"), + ); } - /// Returns whether the verified archive holds `commitment` at `round`. pub(crate) async fn has_verified(&self, round: Round, commitment: &B::Digest) -> bool { let Some(cache) = self.caches.get(&round.epoch()) else { return false; }; cache .verified_blocks + .as_ref() + .expect("verified archive unavailable") .has_at(round.view().get(), commitment) .await .expect("failed to check verified blocks") } - /// Observe all verified-block writes accepted before this call. pub(crate) async fn start_sync_verified(&mut self, round: Round) -> Handle<()> { let Some(cache) = self.caches.get_mut(&round.epoch()) else { return Handle::ready(Ok(())); }; - Self::handle_start_result(cache.verified_blocks.start_sync().await, round, "verified") + let archive = cache + .verified_blocks + .take() + .expect("verified archive unavailable"); + let (archive, handle) = archive + .start_sync() + .await + .expect("failed to sync verified blocks"); + cache.verified_blocks = Some(archive); + handle } - /// Observe all notarization writes accepted before this call. pub(crate) async fn start_sync_notarizations(&mut self, round: Round) -> Handle<()> { let Some(cache) = self.caches.get_mut(&round.epoch()) else { return Handle::ready(Ok(())); }; - Self::handle_start_result( - cache.notarizations.start_sync().await, - round, - "notarization", - ) + let archive = cache + .notarizations + .take() + .expect("notarizations archive unavailable"); + let (archive, handle) = archive + .start_sync() + .await + .expect("failed to sync notarizations"); + cache.notarizations = Some(archive); + handle } - /// Get a notarization from the prunable archive by round. pub(crate) async fn get_notarization( &self, round: Round, @@ -461,129 +448,125 @@ impl< let cache = self.caches.get(&round.epoch())?; cache .notarizations + .as_ref() + .expect("notarizations archive unavailable") .get(Identifier::Index(round.view().get())) .await .expect("failed to get notarization") } - /// Get a block previously persisted in the verified archive for `round`. - /// - /// The archive can hold multiple candidates at one view when a leader - /// equivocates across a crash. This returns the first stored candidate; - /// callers must validate its digest and context before reuse. + /// Returns the first candidate at a view; callers must check digest/context + /// because verified storage retains equivocations across crashes. pub(crate) async fn get_verified(&self, round: Round) -> Option { let cache = self.caches.get(&round.epoch())?; cache .verified_blocks + .as_ref() + .expect("verified archive unavailable") .get(Identifier::Index(round.view().get())) .await .expect("failed to get verified block") } - /// Get a finalization from the prunable archive by commitment. pub(crate) async fn get_finalization_for( &self, commitment: B::Digest, ) -> Option> { for cache in self.caches.values().rev() { - match cache.finalizations.get(Identifier::Key(&commitment)).await { - Ok(Some(finalization)) => return Some(finalization), - Ok(None) => continue, - Err(e) => panic!("failed to get cached finalization: {e}"), + if let Some(finalization) = cache + .finalizations + .as_ref() + .expect("finalizations archive unavailable") + .get(Identifier::Key(&commitment)) + .await + .expect("failed to get cached finalization") + { + return Some(finalization); } } None } - /// Looks for a block (verified, notarized, or certified by height). pub(crate) async fn find_block(&self, commitment: B::Digest) -> Option { self.find_block_matching(commitment, |_| true).await } - /// Looks for a block (verified, notarized, or certified by height) that matches `predicate`. pub(crate) async fn find_block_matching( &self, commitment: B::Digest, mut predicate: impl FnMut(&B) -> bool, ) -> Option { - // Check in reverse order for cache in self.caches.values().rev() { - // Check verified blocks - if let Some(block) = cache - .verified_blocks - .get(Identifier::Key(&commitment)) - .await - .expect("failed to get verified block") - && predicate(&block) - { - return Some(block); - } - - // Check notarized blocks - if let Some(block) = cache - .notarized_blocks - .get(Identifier::Key(&commitment)) - .await - .expect("failed to get notarized block") - && predicate(&block) - { - return Some(block); - } - - // Check certified blocks - if let Some(block) = cache - .certified_blocks - .get(Identifier::Key(&commitment)) - .await - .expect("failed to get certified block") - && predicate(&block) - { - return Some(block); + for archive in [ + &cache.verified_blocks, + &cache.notarized_blocks, + &cache.certified_blocks, + ] { + if let Some(block) = archive + .as_ref() + .expect("block cache unavailable") + .get(Identifier::Key(&commitment)) + .await + .expect("failed to get cached block") + && predicate(&block) + { + return Some(block); + } } } None } - /// Prune the view-indexed caches below the given round. pub(crate) async fn prune_by_view(&mut self, round: Round) { - // Remove and close prunable archives from older epochs let new_floor = round.epoch(); - let old_epochs: Vec = self + let old_epochs: Vec<_> = self .caches .keys() .copied() .filter(|epoch| *epoch < new_floor) .collect(); - for epoch in old_epochs.iter() { - let Cache:: { - verified_blocks: vb, - notarized_blocks: nb, - certified_blocks: cb, - notarizations: nv, - finalizations: fv, - } = self.caches.remove(epoch).unwrap(); - vb.destroy().await.expect("failed to destroy vb"); - nb.destroy().await.expect("failed to destroy nb"); - cb.destroy().await.expect("failed to destroy cb"); - nv.destroy().await.expect("failed to destroy nv"); - fv.destroy().await.expect("failed to destroy fv"); + for epoch in old_epochs { + let cache = self.caches.remove(&epoch).unwrap(); + cache + .verified_blocks + .expect("verified archive unavailable") + .destroy() + .await + .expect("failed to destroy verified archive"); + cache + .notarized_blocks + .expect("notarized archive unavailable") + .destroy() + .await + .expect("failed to destroy notarized archive"); + cache + .certified_blocks + .expect("certified archive unavailable") + .destroy() + .await + .expect("failed to destroy certified archive"); + cache + .notarizations + .expect("notarizations archive unavailable") + .destroy() + .await + .expect("failed to destroy notarizations archive"); + cache + .finalizations + .expect("finalizations archive unavailable") + .destroy() + .await + .expect("failed to destroy finalizations archive"); } - - // Update metadata if necessary let (floor, ceiling) = self.get_metadata(); if new_floor > floor { - let new_ceiling = max(ceiling, new_floor); - self.set_metadata(new_floor, new_ceiling).await; + self.set_metadata(new_floor, max(ceiling, new_floor)).await; } - - // Prune archives for the given epoch - let min_view = round.view(); - if let Some(prunable) = self.caches.get_mut(&round.epoch()) { - prunable.prune_by_view(min_view).await; + if let Some(cache) = self.caches.get_mut(&round.epoch()) { + cache.prune_by_view(round.view()).await; } } - /// Prune height-indexed certified blocks below the given height. pub(crate) async fn prune_by_height(&mut self, height: Height) { for cache in self.caches.values_mut() { cache.prune_by_height(height).await; diff --git a/syncer/src/config.rs b/syncer/src/config.rs index efd4b839..36c312b0 100644 --- a/syncer/src/config.rs +++ b/syncer/src/config.rs @@ -7,17 +7,29 @@ use commonware_runtime::buffer::paged::CacheRef; use std::num::{NonZeroU64, NonZeroUsize}; use summit_types::FinalizedHeader; -/// The initial sync position for the syncer. +/// The state actually selected by the finalizer at startup. Delivery begins at +/// `height + 1`, regardless of acknowledgements from a previous process. +/// +/// This height also bounds finalized-archive pruning for this process: entries +/// above it must remain available for replay. Subsequent restarts must recover +/// this state or a newer one. Genesis and durable checkpoint imports satisfy +/// this requirement. Callers supplying a state-only bootstrap to an empty +/// finalizer database must preserve and resupply it until a durable finalizer +/// state supersedes it; restarting from an older baseline after pruning is not +/// supported. The epoch/view are diagnostic; round recovery uses the archives. pub struct SyncStart { pub height: u64, pub epoch: u64, pub view: u64, } -/// Checkpoint data to restore from when loading from a checkpoint. +/// Terminal-block replay artifacts from a durably committed finalizer import. +/// Ordinary restarts select their delivery position through [`SyncStart`], but +/// must not manufacture checkpoint artifacts or bypass import validation. pub struct SyncCheckpoint> { - pub last_block: B, - pub finalized_header: Option>, + pub processed_height: commonware_consensus::types::Height, + pub finalized_header: FinalizedHeader, + pub last_block: Option, } /// Marshal configuration. diff --git a/syncer/src/delivery.rs b/syncer/src/delivery.rs index 216bca13..43b1404e 100644 --- a/syncer/src/delivery.rs +++ b/syncer/src/delivery.rs @@ -8,11 +8,13 @@ use commonware_utils::channel::oneshot; /// A parsed-but-unverified resolver delivery awaiting batch certificate verification. pub(crate) enum PendingVerification, B: Block> { Notarized { + scoped: commonware_cryptography::certificate::Scoped, notarization: Notarization, block: B, response: oneshot::Sender, }, Finalized { + scoped: commonware_cryptography::certificate::Scoped, finalization: Finalization, block: B, response: oneshot::Sender, @@ -20,6 +22,12 @@ pub(crate) enum PendingVerification, B: Block> { } impl, B: Block> PendingVerification { + pub(crate) fn scoped(&self) -> &commonware_cryptography::certificate::Scoped { + match self { + Self::Notarized { scoped, .. } | Self::Finalized { scoped, .. } => scoped, + } + } + pub(crate) fn response_closed(&self) -> bool { match self { Self::Notarized { response, .. } | Self::Finalized { response, .. } => { diff --git a/syncer/src/floor.rs b/syncer/src/floor.rs index 05559362..492a1015 100644 --- a/syncer/src/floor.rs +++ b/syncer/src/floor.rs @@ -7,7 +7,7 @@ use commonware_cryptography::{Digest, certificate::Scheme as CertificateScheme}; use commonware_resolver::{Resolver, TargetedResolver}; use commonware_utils::vec::NonEmptyVec; -/// Durable processed floor used to admit or reject resolver fetches. +/// Process-local processed floor used to admit or reject resolver fetches. #[derive(Clone, Copy)] struct ProcessedFloor { height: Option, @@ -34,6 +34,7 @@ pub(crate) enum FetchAdmission { } impl FetchAdmission { + #[cfg(test)] pub(crate) const fn denied(self) -> bool { matches!(self, Self::Denied) } @@ -100,6 +101,20 @@ impl Floor { self.pending.take() } + /// Release an anchor already covered by the processed round. Its resolver + /// request has been pruned and must no longer block application progress. + pub(crate) fn take_superseded_anchor(&mut self) -> Option> { + if self + .pending + .as_ref() + .is_some_and(|pending| pending.round() <= self.processed.round) + { + self.pending.take() + } else { + None + } + } + pub(crate) fn fetch_if_permitted( &self, resolver: &mut R, @@ -248,9 +263,9 @@ mod tests { fn digest(byte: u8) -> TestDigest { use commonware_cryptography::Hasher as _; - let mut hasher = Sha256::new(); + let mut hasher = Sha256::default(); hasher.update(&[byte]); - hasher.finalize() + hasher.finalize().1 } fn floor() -> Floor { diff --git a/syncer/src/ingress/handler.rs b/syncer/src/ingress/handler.rs index e00906b9..6c121d58 100644 --- a/syncer/src/ingress/handler.rs +++ b/syncer/src/ingress/handler.rs @@ -148,6 +148,7 @@ impl Consumer for Handler { type Key = Key; type Value = Bytes; type Subscriber = Annotation; + type Outcome = bool; fn deliver( &mut self, @@ -600,7 +601,7 @@ mod tests { #[test] fn test_subject_block_encoding() { - let commitment = Sha256::hash(b"test"); + let commitment = Sha256::hash(&[b"test"]); let request = Key::::Block(commitment); // Test encoding @@ -678,7 +679,7 @@ mod tests { let notarized = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(150)), }; - let block = Key::::Block(Sha256::hash(b"block")); + let block = Key::::Block(Sha256::hash(&[b"block"])); let stale_finalized = Annotation::Finalized(Finalized::ByHeight { height: Height::new(100), }); @@ -718,7 +719,7 @@ mod tests { #[test] fn test_round_floor_predicate() { let floor = Round::new(Epoch::new(1), View::new(10)); - let block = Key::::Block(Sha256::hash(b"block")); + let block = Key::::Block(Sha256::hash(&[b"block"])); let higher_notarized = Key::::Notarized { round: Round::new(Epoch::new(1), View::new(11)), }; @@ -762,7 +763,7 @@ mod tests { #[test] fn test_encode_size() { - let commitment = Sha256::hash(&[0u8; 32]); + let commitment = Sha256::hash(&[&[0u8; 32]]); let r1 = Key::::Block(commitment); let r2 = Key::::Finalized { height: u64::MAX }; let r3 = Key::::Notarized { @@ -778,8 +779,8 @@ mod tests { #[test] fn test_request_ord_same_variant() { // Test ordering within the same variant - let commitment1 = Sha256::hash(b"test1"); - let commitment2 = Sha256::hash(b"test2"); + let commitment1 = Sha256::hash(&[b"test1"]); + let commitment2 = Sha256::hash(&[b"test2"]); let block1 = Key::::Block(commitment1); let block2 = Key::::Block(commitment2); @@ -819,7 +820,7 @@ mod tests { #[test] fn test_request_ord_cross_variant() { - let commitment = Sha256::hash(b"test"); + let commitment = Sha256::hash(&[b"test"]); let block = Key::::Block(commitment); let finalized = Key::::Finalized { height: 100 }; let notarized = Key::::Notarized { @@ -846,9 +847,9 @@ mod tests { #[test] fn test_request_ord_sorting() { - let commitment1 = Sha256::hash(b"a"); - let commitment2 = Sha256::hash(b"b"); - let commitment3 = Sha256::hash(b"c"); + let commitment1 = Sha256::hash(&[b"a"]); + let commitment2 = Sha256::hash(&[b"b"]); + let commitment3 = Sha256::hash(&[b"c"]); let requests = vec![ Key::::Notarized { @@ -915,7 +916,7 @@ mod tests { assert!(max_finalized < min_notarized); // Test self-comparison - let commitment = Sha256::hash(b"self"); + let commitment = Sha256::hash(&[b"self"]); let block = Key::::Block(commitment); assert_eq!(block.cmp(&block), std::cmp::Ordering::Equal); assert_eq!(min_finalized.cmp(&min_finalized), std::cmp::Ordering::Equal); diff --git a/syncer/src/ingress/mailbox.rs b/syncer/src/ingress/mailbox.rs index 2bf6b4ca..5fa67bd6 100644 --- a/syncer/src/ingress/mailbox.rs +++ b/syncer/src/ingress/mailbox.rs @@ -197,9 +197,17 @@ pub(crate) enum Message, B: Block> { }, /// Attempts to set the sync starting point from a finalized commitment. /// - /// If the verified finalization advances the current floor, the syncer - /// anchors on its block, prunes below it, then syncs and delivers blocks - /// starting at the floor height. Stale or superseded floors may be ignored. + /// If the verified finalization advances the current floor, the syncer obtains + /// its anchor block, stores and syncs the block and certificate, then sets the + /// process-local processed height to the anchor's predecessor and clears pending + /// acknowledgements. It prunes temporary data below the anchor and finalized + /// history only within the startup recovery bound, then repairs gaps and delivers + /// from the anchor itself. Stale or superseded floors may be ignored. + /// + /// The caller must arrange compatible live finalizer state. The delivery skip + /// is not persisted: restart follows the newly selected finalizer state and may + /// require replaying skipped blocks and fetching missing history. Finalized-archive + /// pruning remains bounded by the recovery-baseline contract in [`crate::SyncStart`]. /// /// To prune data without changing the sync starting point, use /// [Message::Prune] instead. @@ -211,7 +219,8 @@ pub(crate) enum Message, B: Block> { /// /// Unlike [Message::SetFloor], this does not affect the sync starting point. /// The height must be at or below the current floor (last processed height), - /// otherwise the prune request is ignored. + /// otherwise the prune request is ignored. Valid requests are capped at the + /// startup recovery height's saturating successor, preserving replay history. Prune { /// The minimum height to keep (blocks below this are pruned). height: Height, @@ -553,7 +562,10 @@ impl, B: Block> Mailbox { receiver.await.ok().flatten() } - /// Retrieve the latest processed height. + /// Retrieve the process-local delivery cursor, initialized from the finalizer's + /// selected startup state and advanced by acknowledgements or floor changes. + /// This is not a durable finalizer state height or pruning authorization. + /// Returns `None` only when no actor response was obtained. pub async fn get_processed_height(&self) -> Option { let (response, receiver) = oneshot::channel(); let _ = self @@ -699,9 +711,17 @@ impl, B: Block> Mailbox { /// Attempts to set the sync starting point from a finalized commitment. /// - /// If the verified finalization advances the current floor, the syncer - /// anchors on its block, prunes below it, then syncs and delivers blocks - /// starting at the floor height. Stale or superseded floors may be ignored. + /// If the verified finalization advances the current floor, the syncer obtains + /// its anchor block, stores and syncs the block and certificate, then sets the + /// process-local processed height to the anchor's predecessor and clears pending + /// acknowledgements. It prunes temporary data below the anchor and finalized + /// history only within the startup recovery bound, then repairs gaps and delivers + /// from the anchor itself. Stale or superseded floors may be ignored. + /// + /// The caller must arrange compatible live finalizer state. The delivery skip + /// is not persisted: restart follows the newly selected finalizer state and may + /// require replaying skipped blocks and fetching missing history. Finalized-archive + /// pruning remains bounded by the recovery-baseline contract in [`crate::SyncStart`]. /// /// To prune data without changing the sync starting point, use /// [`Self::prune`] instead. @@ -713,7 +733,9 @@ impl, B: Block> Mailbox { /// /// Unlike [`Self::set_floor`], this does not affect the sync starting point. /// The height must be at or below the current floor (last processed height), - /// otherwise the prune request is ignored. + /// otherwise the prune request is ignored. Valid requests are capped at the + /// saturating successor of the startup recovery height, fixed for the process + /// lifetime. Archive section rounding may retain additional entries. pub fn prune(&mut self, height: Height) { let _ = self.sender.enqueue(Message::Prune { height }); } diff --git a/syncer/src/lib.rs b/syncer/src/lib.rs index 62f30411..5f32c5c6 100644 --- a/syncer/src/lib.rs +++ b/syncer/src/lib.rs @@ -21,7 +21,10 @@ //! ## Delivery //! //! The actor will deliver a block to the reporter at-least-once. The reporter should be prepared to -//! handle duplicate deliveries. However the blocks will be in order. +//! handle duplicate deliveries. However the blocks will be in order. Acknowledgements +//! advance delivery only within the current process. Restart begins after the state +//! selected by the finalizer ([`SyncStart`]), even if later blocks were acknowledged. +//! Legacy `*-application-metadata` partitions are ignored, not migrated or deleted. //! //! ## Finalization //! @@ -49,7 +52,12 @@ //! Marshal will store all blocks after a configurable starting height (or, floor) onward. //! This allows for state sync from a specific height rather than from genesis. When //! updating the starting height, marshal will attempt to prune blocks in external storage -//! that are no longer needed. +//! that are no longer needed. Finalized block and certificate pruning is capped at +//! the successor of the startup recovery height, fixed for the process lifetime; +//! archive section rounding can retain additional history. Live acknowledgements +//! and floor changes do not authorize deleting replay history. Callers must satisfy +//! [`SyncStart`]'s recovery-baseline contract. This conservative cap may retain extra +//! history until restart and is not a bounded-disk retention policy. //! //! _Setting a configurable starting height will prevent others from backfilling blocks below said height. This //! feature is only recommended for applications that support state sync (i.e., those that don't require full @@ -76,7 +84,6 @@ pub use ingress::mailbox::Mailbox; pub mod resolver; pub mod standard; pub use standard::Standard; -mod stream; pub mod variant; pub use variant::{Buffer, Variant}; @@ -101,8 +108,9 @@ pub enum Update, A: Acknowledgement = Exact> { Tip(u64, B::Digest), /// A new finalized block and an [Acknowledgement] for the application to signal once processed. /// - /// To ensure all blocks are delivered at least once, marshal waits to mark a block as delivered - /// until the application explicitly acknowledges the update. If the [Acknowledgement] is dropped before + /// Acknowledgements advance the process-local delivery cursor, not a durable + /// restart position. Restart follows the finalizer's selected state, so even + /// acknowledged blocks can be replayed. If the [Acknowledgement] is dropped before /// handling, marshal will exit (assuming the application is shutting down). /// /// Because the [Acknowledgement] is clonable, the application can pass [Update] to multiple consumers @@ -218,6 +226,8 @@ pub mod mocks; #[cfg(all(test, feature = "test-mocks"))] mod tests { + mod recovery; + use super::{ actor, cache, config::{Config, SyncStart}, @@ -293,12 +303,12 @@ mod tests { const LINK: Link = Link { latency: Duration::from_millis(10), jitter: Duration::from_millis(1), - success_rate: 1.0, + success_rate: commonware_utils::probability!(1.0), }; const UNRELIABLE_LINK: Link = Link { latency: Duration::from_millis(200), jitter: Duration::from_millis(50), - success_rate: 0.7, + success_rate: commonware_utils::probability!(0.7), }; const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX); @@ -314,28 +324,34 @@ mod tests { type Block = T::Block; type Error = T::Error; - async fn put(&mut self, block: Self::Block) -> Result<(), Self::Error> { - self.inner.put(block).await + async fn put(mut self, block: Self::Block) -> Result { + self.inner = self.inner.put(block).await?; + Ok(self) } - async fn sync(&mut self) -> Result<(), Self::Error> { + async fn sync(mut self) -> Result { self.context.sleep(self.pace).await; - self.inner.sync().await + self.inner = self.inner.sync().await?; + Ok(self) } - async fn start_sync(&mut self) -> Result, Self::Error> { - let inner = self.inner.start_sync().await?; + async fn start_sync( + mut self, + ) -> Result<(Self, commonware_runtime::Handle<()>), Self::Error> { + let (inner, handle) = self.inner.start_sync().await?; + self.inner = inner; let sleep = self.context.sleep(self.pace); let fail_sync = self.fail_sync; - Ok(commonware_runtime::Handle::from_future(async move { + let handle = commonware_runtime::Handle::from_future(async move { sleep.await; - inner.await?; + handle.await?; if fail_sync { Err(commonware_runtime::Error::WriteFailed) } else { Ok(()) } - })) + }); + Ok((self, handle)) } async fn get( @@ -345,8 +361,9 @@ mod tests { self.inner.get(id).await } - async fn prune(&mut self, min: Height) -> Result<(), Self::Error> { - self.inner.prune(min).await + async fn prune(mut self, min: Height) -> Result { + self.inner = self.inner.prune(min).await?; + Ok(self) } fn missing_items(&self, start: Height, max: usize) -> Vec { @@ -369,32 +386,38 @@ mod tests { type Error = T::Error; async fn put( - &mut self, + mut self, height: Height, digest: Self::BlockDigest, finalization: Finalization, - ) -> Result<(), Self::Error> { - self.inner.put(height, digest, finalization).await + ) -> Result { + self.inner = self.inner.put(height, digest, finalization).await?; + Ok(self) } - async fn sync(&mut self) -> Result<(), Self::Error> { + async fn sync(mut self) -> Result { self.context.sleep(self.pace).await; - self.inner.sync().await + self.inner = self.inner.sync().await?; + Ok(self) } - async fn start_sync(&mut self) -> Result, Self::Error> { - let inner = self.inner.start_sync().await?; + async fn start_sync( + mut self, + ) -> Result<(Self, commonware_runtime::Handle<()>), Self::Error> { + let (inner, handle) = self.inner.start_sync().await?; + self.inner = inner; let sleep = self.context.sleep(self.pace); let fail_sync = self.fail_sync; - Ok(commonware_runtime::Handle::from_future(async move { + let handle = commonware_runtime::Handle::from_future(async move { sleep.await; - inner.await?; + handle.await?; if fail_sync { Err(commonware_runtime::Error::WriteFailed) } else { Ok(()) } - })) + }); + Ok((self, handle)) } async fn get( @@ -408,8 +431,9 @@ mod tests { self.inner.has(height).await } - async fn prune(&mut self, min: Height) -> Result<(), Self::Error> { - self.inner.prune(min).await + async fn prune(mut self, min: Height) -> Result { + self.inner = self.inner.prune(min).await?; + Ok(self) } fn last_index(&self) -> Option { @@ -541,6 +565,7 @@ mod tests { context.child("paced_finalizations"), prunable::Config { translator: EightCap, + metadata_partition: format!("{partition_prefix}-fbh-metadata"), key_partition: format!("{partition_prefix}-fbh-key"), key_page_cache: page_cache.clone(), value_partition: format!("{partition_prefix}-fbh-value"), @@ -558,6 +583,7 @@ mod tests { context.child("paced_blocks"), prunable::Config { translator: EightCap, + metadata_partition: format!("{partition_prefix}-fb-metadata"), key_partition: format!("{partition_prefix}-fb-key"), key_page_cache: page_cache, value_partition: format!("{partition_prefix}-fb-value"), @@ -849,6 +875,33 @@ mod tests { Application, crate::ingress::mailbox::Mailbox, commonware_runtime::Handle<()>, + ) { + setup_validator_with_start( + context, + oracle, + validator, + provider, + partition_prefix, + SyncStart { + height: 0, + epoch: 0, + view: 0, + }, + ) + .await + } + + async fn setup_validator_with_start( + context: deterministic::Context, + oracle: &mut Oracle, + validator: K, + provider: P, + partition_prefix: &str, + sync_start: SyncStart, + ) -> ( + Application, + crate::ingress::mailbox::Mailbox, + commonware_runtime::Handle<()>, ) { let config = Config { scheme_provider: provider, @@ -949,17 +1002,12 @@ mod tests { let (actor, mailbox) = actor::Actor::init(context, finalizations_by_height, finalized_blocks, config).await; let application = Application::::default(); - let handle = actor.start( - application.clone(), - buffer, - resolver, - SyncStart { - height: 0, - epoch: 0, - view: 0, - }, - None, - ); + let handle = actor.start(application.clone(), buffer, resolver, sync_start, None); + // Complete startup before callers enable operation-specific fault injection. + mailbox + .get_processed_height() + .await + .expect("actor startup failed"); (application, mailbox, handle) } @@ -972,7 +1020,12 @@ mod tests { .collect(); // Generate certificate signatures - Finalization::from_finalizes(&schemes[0], &finalizes, &Sequential).unwrap() + Finalization::from_finalizes( + &schemes[0], + commonware_utils::non_empty![@&finalizes], + &Sequential, + ) + .unwrap() } fn make_notarization(proposal: Proposal, schemes: &[S], quorum: u32) -> Notarization { @@ -984,7 +1037,12 @@ mod tests { .collect(); // Generate certificate signatures - Notarization::from_notarizes(&schemes[0], ¬arizes, &Sequential).unwrap() + Notarization::from_notarizes( + &schemes[0], + commonware_utils::non_empty![@¬arizes], + &Sequential, + ) + .unwrap() } fn setup_network( @@ -995,6 +1053,7 @@ mod tests { context.child("network"), simulated::Config { max_size: 1024 * 1024, + max_peers_per_set: NZUsize!(16), disconnect_on_block: true, tracked_peer_sets, }, @@ -1081,7 +1140,7 @@ mod tests { // Generate blocks, skipping the genesis block. let mut blocks = Vec::::new(); - let mut parent = Sha256::hash(b""); + let mut parent = Sha256::hash(&[b""]); for i in 1..=NUM_BLOCKS { let block = B::new::(parent, Height::new(i), i); parent = block.digest(); @@ -1169,6 +1228,56 @@ mod tests { }) } + #[test_traced("WARN")] + fn test_subscribe_survives_denied_round_fetch() { + deterministic::Runner::timed(Duration::from_secs(60)).start(|mut context| async move { + let mut oracle = setup_network(context.child("network"), NZUsize!(1)); + let Fixture { + participants, + schemes, + .. + } = bls12381_threshold::(&mut context, NUM_VALIDATORS); + let (_application, mut mailbox, _handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "denied-fetch-subscription", + ) + .await; + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); + // Round zero is at the processed floor: remote fetching is denied, + // but the caller still owns its local subscription. + let subscription = mailbox.subscribe(Some(Round::zero()), block.digest()); + assert!( + mailbox + .verified(Round::new(Epoch::zero(), View::new(1)), block.clone()) + .await + ); + assert_eq!(subscription.await.unwrap(), block); + }); + } + + #[test_traced("WARN")] + fn test_processed_round_releases_only_superseded_floor_anchor() { + deterministic::Runner::default().start(|mut context| async move { + let Fixture { schemes, .. } = bls12381_threshold::(&mut context, NUM_VALIDATORS); + let round = Round::new(Epoch::zero(), View::new(2)); + let finalization = make_finalization( + Proposal::new(round, View::new(1), Sha256::hash(&[b"anchor"])), + &schemes, + QUORUM, + ); + let mut floor = crate::floor::Floor::resolved(Some(Height::new(1)), Round::zero()); + floor.await_anchor(finalization); + assert!(floor.take_superseded_anchor().is_none()); + assert!(floor.blocks_progress()); + floor.set_processed_round(round); + assert!(floor.take_superseded_anchor().is_some()); + assert!(!floor.blocks_progress()); + }); + } + #[test_traced("WARN")] fn test_subscribe_basic_block_delivery() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); @@ -1195,7 +1304,7 @@ mod tests { setup_network_links(&mut oracle, &participants, LINK).await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); @@ -1251,7 +1360,7 @@ mod tests { setup_network_links(&mut oracle, &participants, LINK).await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block1 = B::new::(parent, Height::new(1), 1); let block2 = B::new::(block1.digest(), Height::new(2), 2); let commitment1 = block1.digest(); @@ -1327,7 +1436,7 @@ mod tests { setup_network_links(&mut oracle, &participants, LINK).await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block1 = B::new::(parent, Height::new(1), 1); let block2 = B::new::(block1.digest(), Height::new(2), 2); let commitment1 = block1.digest(); @@ -1399,7 +1508,7 @@ mod tests { setup_network_links(&mut oracle, &participants, LINK).await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block1 = B::new::(parent, Height::new(1), 1); let block2 = B::new::(block1.digest(), Height::new(2), 2); let block3 = B::new::(block2.digest(), Height::new(3), 3); @@ -1522,7 +1631,7 @@ mod tests { assert!(actor.get_info(1).await.is_none()); // Create and verify a block, then finalize it - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let digest = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); @@ -1555,7 +1664,7 @@ mod tests { assert!(actor.get_info(2).await.is_none()); // Missing commitment - let missing = Sha256::hash(b"missing"); + let missing = Sha256::hash(&[b"missing"]); assert!(actor.get_info(&missing).await.is_none()); }) } @@ -1585,7 +1694,7 @@ mod tests { assert!(actor.get_info(Identifier::Latest).await.is_none()); // Build and finalize heights 1..=3 - let parent0 = Sha256::hash(b""); + let parent0 = Sha256::hash(&[b""]); let block1 = B::new::(parent0, Height::new(1), 1); let d1 = block1.digest(); assert!( @@ -1674,7 +1783,7 @@ mod tests { assert!(application.tip().is_none()); // Finalize a block at height 1 - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); @@ -1728,7 +1837,7 @@ mod tests { .await; // 1) From cache via verified - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let ver_block = B::new::(parent, Height::new(1), 1); let ver_commitment = ver_block.digest(); let round1 = Round::new(Epoch::new(0), View::new(1)); @@ -1759,7 +1868,7 @@ mod tests { assert_eq!(got.height, Height::new(2)); // 3) Missing commitment - let missing = Sha256::hash(b"definitely-missing"); + let missing = Sha256::hash(&[b"definitely-missing"]); let missing_block = actor.get_block(&missing).await; assert!(missing_block.is_none()); }) @@ -1790,7 +1899,7 @@ mod tests { assert!(finalization.is_none()); // Finalize a block at height 1 - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); @@ -1849,7 +1958,7 @@ mod tests { // binding only permits a finalization view to differ from the header // view for the epoch-terminal block (a same-digest reproposal), which // is exactly the cross-view scenario this test exercises. - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(19), 19); let commitment = block.digest(); @@ -1961,7 +2070,7 @@ mod tests { .await; // Create block at height 1 - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); @@ -2047,7 +2156,7 @@ mod tests { setup_network_links(&mut oracle, &participants, LINK).await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); let round = Round::new(Epoch::zero(), View::new(1)); @@ -2109,7 +2218,7 @@ mod tests { ) .await; - let parent = Sha256::hash(b""); + let parent = Sha256::hash(&[b""]); let proposed = B::new::(parent, Height::new(1), 1); let proposed_conflict = B::new::(parent, Height::new(1), 5); let verified_a = B::new::(proposed.digest(), Height::new(2), 2); @@ -2190,6 +2299,124 @@ mod tests { }); } + #[test_traced("WARN")] + fn test_restart_replays_acknowledged_blocks_after_finalizer_state() { + restart_from_finalizer_state(1, None); + } + + #[test_traced("WARN")] + fn test_restart_ignores_legacy_acknowledgements() { + restart_from_finalizer_state(1, Some(2)); + } + + #[test_traced("WARN")] + fn test_restart_does_not_replay_committed_finalizer_state() { + restart_from_finalizer_state(2, None); + } + + fn restart_from_finalizer_state(height: u64, legacy_ack: Option) { + use commonware_consensus::{Heightable as _, Viewable as _}; + let runner = deterministic::Runner::timed(Duration::from_secs(60)); + let ((validator, scheme, second), recovered) = + runner.start_and_recover(|mut context| async move { + let mut oracle = setup_network(context.child("network"), NZUsize!(1)); + let Fixture { + participants, + schemes, + .. + } = bls12381_threshold::(&mut context, NUM_VALIDATORS); + let validator = participants[0].clone(); + let scheme = schemes[0].clone(); + let (application, mut mailbox, handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + validator.clone(), + ConstantProvider::new(scheme.clone()), + "ack-recovery", + ) + .await; + assert_eq!(mailbox.get_processed_height().await, Some(Height::zero())); + if height == 2 { + application.withhold_ack(2); + } + let first = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); + let second = B::new::(first.digest(), Height::new(2), 2); + for block in [&first, &second] { + let round = Round::new(Epoch::zero(), block.view()); + assert!(mailbox.verified(round, block.clone()).await); + let _ = mailbox.report(Activity::Finalization(make_finalization( + Proposal::new(round, View::new(block.height().get() - 1), block.digest()), + &schemes, + QUORUM, + ))); + } + wait_until( + &context, + Duration::from_secs(5), + "genesis deliveries", + || application.blocks().len() == 2, + ) + .await; + let acknowledged = if height == 2 { 1 } else { 2 }; + while mailbox.get_processed_height().await != Some(Height::new(acknowledged)) { + context.sleep(Duration::from_millis(10)).await; + } + assert_eq!( + application.blocks(), + BTreeMap::from([(1, first), (2, second.clone())]) + ); + // With height=2 the application selected state 2, but its ack is + // still held and the syncer cursor is only 1 when the process stops. + handle.abort(); + let _ = handle.await; + // Compatibility fixture only: production must never open this partition. + if let Some(ack) = legacy_ack { + commonware_storage::metadata::Metadata::< + _, + commonware_utils::sequence::U64, + Height, + >::init( + context.child("legacy_ack"), + commonware_storage::metadata::Config { + partition: "ack-recovery-application-metadata".into(), + codec_config: (), + }, + ) + .await + .unwrap() + .put_sync(commonware_utils::sequence::U64::new(0xFF), Height::new(ack)) + .await + .unwrap(); + } + (validator, scheme, second) + }); + deterministic::Runner::from(recovered).start(|context| async move { + let mut oracle = setup_network(context.child("restart_network"), NZUsize!(1)); + let (application, mailbox, _handle) = setup_validator_with_start( + context.child("restart_validator"), + &mut oracle, + validator, + ConstantProvider::new(scheme), + "ack-recovery", + SyncStart { + height, + epoch: 0, + view: height, + }, + ) + .await; + while mailbox.get_processed_height().await != Some(Height::new(2)) { + context.sleep(Duration::from_millis(10)).await; + } + let expected = if height == 1 { + BTreeMap::from([(2, second)]) + } else { + BTreeMap::new() + }; + assert_eq!(application.blocks(), expected); + }); + } + /// Port of marshal's fatal durability policy: a real sync failure must /// panic rather than become a recoverable `false` verification verdict. #[test_traced("WARN")] @@ -2211,8 +2438,9 @@ mod tests { ) .await; - context.storage_fault_config().write().sync_rate = Some(1.0); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let _ = mailbox .verified(Round::new(Epoch::zero(), View::new(1)), block) .await; @@ -2240,8 +2468,9 @@ mod tests { ) .await; - context.storage_fault_config().write().sync_rate = Some(1.0); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let _ = mailbox .proposed(Round::new(Epoch::zero(), View::new(1)), block) .await; @@ -2269,8 +2498,9 @@ mod tests { ) .await; - context.storage_fault_config().write().sync_rate = Some(1.0); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let _ = mailbox .certified(Round::new(Epoch::zero(), View::new(1)), block) .await; @@ -2298,9 +2528,10 @@ mod tests { ) .await; - context.storage_fault_config().write().sync_rate = Some(1.0); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let _ = mailbox.report(Activity::Notarization(make_notarization( Proposal { round, @@ -2339,7 +2570,7 @@ mod tests { .await; let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); assert!(mailbox.verified(round, block.clone()).await); let _ = mailbox.report(Activity::Finalization(make_finalization( Proposal { @@ -2379,7 +2610,7 @@ mod tests { .await; let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); assert!(mailbox.verified(round, block.clone()).await); let _ = mailbox.report(Activity::Finalization(make_finalization( Proposal { @@ -2402,7 +2633,7 @@ mod tests { let (round, checkpoint) = runner.start_and_recover(|mut context| async move { let Fixture { schemes, .. } = bls12381_threshold::(&mut context, NUM_VALIDATORS); let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let notarization = make_notarization( Proposal { round, @@ -2460,7 +2691,7 @@ mod tests { deterministic::Runner::timed(Duration::from_secs(30)).start(|mut context| async move { let Fixture { schemes, .. } = bls12381_threshold::(&mut context, NUM_VALIDATORS); let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let notarization = make_notarization( Proposal { round, @@ -2481,7 +2712,8 @@ mod tests { let mut manager = cache::Manager::<_, B, S>::init(context.child("cache"), config, ()).await; - context.storage_fault_config().write().sync_rate = Some(1.0); + context.storage_fault_config().write().sync_rate = + Some(commonware_utils::probability!(1.0)); drop( manager .put_notarization(round, block.digest(), notarization) @@ -2515,7 +2747,7 @@ mod tests { ConstantProvider::new(schemes[0].clone()), ) .await; - let block = B::new::(Sha256::hash(b""), Height::new(19), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(19), 1); let original = Round::new(Epoch::zero(), View::new(19)); let reproposal = Round::new(Epoch::zero(), View::new(21)); @@ -2551,7 +2783,7 @@ mod tests { .await; const ANCHOR_HEIGHT: u64 = 5; - let mut parent = Sha256::hash(b""); + let mut parent = Sha256::hash(&[b""]); let mut anchor = None; for height in 1..=ANCHOR_HEIGHT { let block = B::new::(parent, Height::new(height), height); @@ -2612,7 +2844,7 @@ mod tests { ) .await; let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); let proposal = Proposal { round, parent: View::zero(), @@ -2668,7 +2900,7 @@ mod tests { .await; let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); assert!(mailbox.verified(round, block.clone()).await); let _ = mailbox.report(Activity::Finalization(make_finalization( Proposal { @@ -2755,7 +2987,7 @@ mod tests { .await; let first_round = Round::new(Epoch::zero(), View::new(1)); - let first = B::new::(Sha256::hash(b""), Height::new(1), 100); + let first = B::new::(Sha256::hash(&[b""]), Height::new(1), 100); assert!(mailbox.verified(first_round, first.clone()).await); let second_round = Round::new(Epoch::zero(), View::new(2)); let second = B::new::(first.digest(), Height::new(2), 200); @@ -2856,7 +3088,7 @@ mod tests { .await; let round = Round::new(Epoch::zero(), View::new(1)); - let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let block = B::new::(Sha256::hash(&[b""]), Height::new(1), 1); assert!(mailbox.verified(round, block.clone()).await); let _ = mailbox.report(Activity::Finalization(make_finalization( Proposal { @@ -2878,7 +3110,7 @@ mod tests { context.sleep(Duration::from_millis(1)).await; } - let fork = B::new::(Sha256::hash(b""), Height::new(1), 999); + let fork = B::new::(Sha256::hash(&[b""]), Height::new(1), 999); mailbox.set_floor(make_finalization( Proposal { round: Round::new(Epoch::zero(), View::new(5)), diff --git a/syncer/src/mocks/application.rs b/syncer/src/mocks/application.rs index 34c8e122..7a38d8b0 100644 --- a/syncer/src/mocks/application.rs +++ b/syncer/src/mocks/application.rs @@ -2,7 +2,7 @@ use crate::Update; use commonware_actor::Feedback; use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::{Block, Reporter}; -use commonware_utils::Acknowledgement; +use commonware_utils::{Acknowledgement, acknowledgement::Exact}; use std::{ collections::BTreeMap, sync::{Arc, Mutex}, @@ -21,6 +21,7 @@ pub struct Application> { updates: Arc>>>, #[allow(clippy::type_complexity)] tip: Arc>>, + withheld_acks: Arc>>>, _phantom: std::marker::PhantomData, } @@ -30,6 +31,7 @@ impl> Default for Application { blocks: Default::default(), updates: Default::default(), tip: Default::default(), + withheld_acks: Default::default(), _phantom: std::marker::PhantomData, } } @@ -41,6 +43,12 @@ impl> Application { self.blocks.lock().unwrap().clone() } + /// Receive a block without acknowledging it, modelling a crash between + /// committing application state and returning its delivery acknowledgement. + pub fn withhold_ack(&self, height: u64) { + self.withheld_acks.lock().unwrap().insert(height, None); + } + /// Returns the tip. pub fn tip(&self) -> Option<(u64, B::Digest)> { *self.tip.lock().unwrap() @@ -65,11 +73,13 @@ impl> Reporter for Application { .lock() .unwrap() .push(RecordedUpdate::Finalized(block.digest())); - self.blocks - .lock() - .unwrap() - .insert(block.height().get(), block); - ack_tx.acknowledge(); + let height = block.height().get(); + self.blocks.lock().unwrap().insert(height, block); + if let Some(held) = self.withheld_acks.lock().unwrap().get_mut(&height) { + *held = Some(ack_tx); + } else { + ack_tx.acknowledge(); + } } Update::NotarizedBlock(block) => { self.updates diff --git a/syncer/src/mocks/block.rs b/syncer/src/mocks/block.rs index bc604f66..0b165c41 100644 --- a/syncer/src/mocks/block.rs +++ b/syncer/src/mocks/block.rs @@ -28,11 +28,11 @@ pub struct Block { impl Block { fn compute_digest>(parent: &D, height: Height, timestamp: u64) -> D { - let mut hasher = H::new(); + let mut hasher = H::default(); hasher.update(parent); hasher.update(&height.get().to_be_bytes()); hasher.update(×tamp.to_be_bytes()); - hasher.finalize() + hasher.finalize().1 } pub fn new>(parent: D, height: Height, timestamp: u64) -> Self { diff --git a/syncer/src/resolver/p2p.rs b/syncer/src/resolver/p2p.rs index 2a74aedc..006f180d 100644 --- a/syncer/src/resolver/p2p.rs +++ b/syncer/src/resolver/p2p.rs @@ -69,7 +69,6 @@ where producer: handler, mailbox_size: config.mailbox_size, me: Some(config.public_key), - initial: config.initial, timeout: config.timeout, fetch_retry_timeout: config.fetch_retry_timeout, priority_requests: config.priority_requests, diff --git a/syncer/src/stream.rs b/syncer/src/stream.rs deleted file mode 100644 index 62bbcada..00000000 --- a/syncer/src/stream.rs +++ /dev/null @@ -1,82 +0,0 @@ -use commonware_consensus::types::Height; -use commonware_storage::{ - Context, - metadata::{self, Metadata}, -}; -use commonware_utils::sequence::U64; - -/// The key used to store the last processed height in the metadata store. -const LATEST_KEY: U64 = U64::new(0xFF); - -/// Last block acknowledged by the application. -#[derive(Clone, Copy)] -enum State { - Unprocessed, - Processed(Height), -} - -impl State { - const fn new(processed_height: Option) -> Self { - match processed_height { - Some(height) => Self::Processed(height), - None => Self::Unprocessed, - } - } - - const fn processed_height(self) -> Option { - match self { - Self::Unprocessed => None, - Self::Processed(height) => Some(height), - } - } - - const fn next_height(self) -> Height { - match self { - Self::Unprocessed => Height::zero(), - Self::Processed(height) => height.next(), - } - } - - const fn acknowledge(&mut self, height: Height) { - *self = Self::Processed(height); - } -} - -/// Application delivery stream progress and durable metadata. -pub(crate) struct Stream { - metadata: Metadata, - state: State, -} - -impl Stream { - pub(crate) async fn new(context: E, application_metadata_partition: &str) -> Self { - let metadata = Metadata::init( - context, - metadata::Config { - partition: application_metadata_partition.to_string(), - codec_config: (), - }, - ) - .await - .expect("failed to initialize application metadata"); - let state = State::new(metadata.get(&LATEST_KEY).copied()); - Self { metadata, state } - } - - pub(crate) const fn processed_height(&self) -> Option { - self.state.processed_height() - } - - pub(crate) const fn next_height(&self) -> Height { - self.state.next_height() - } - - pub(crate) fn acknowledge(&mut self, height: Height) { - self.state.acknowledge(height); - self.metadata.put(LATEST_KEY, height); - } - - pub(crate) async fn sync(&mut self) -> Result<(), metadata::Error> { - self.metadata.sync().await - } -} diff --git a/syncer/src/tests/recovery.rs b/syncer/src/tests/recovery.rs new file mode 100644 index 00000000..05428650 --- /dev/null +++ b/syncer/src/tests/recovery.rs @@ -0,0 +1,322 @@ +//! Restart/pruning regressions with real section-prunable finalized archives. +use super::*; +use commonware_consensus::{Epochable as _, Heightable as _, Viewable as _}; +use commonware_storage::archive::Identifier as ArchiveID; + +async fn start_prunable( + context: deterministic::Context, + validator: K, + scheme: S, + height: u64, +) -> ( + Application, + crate::Mailbox, + RecordingResolver, + commonware_runtime::Handle<()>, +) { + let oracle = setup_network(context.child("network"), NZUsize!(1)); + let control = oracle.control(validator.clone()); + let (broadcast, buffer) = buffered::Engine::new( + context.child("broadcast"), + buffered::Config { + public_key: validator, + mailbox_size: NZUsize!(100), + deque_size: 10, + priority: false, + codec_config: (), + peer_provider: oracle.manager(), + }, + ); + broadcast.start(control.register(2, TEST_QUOTA).await.unwrap()); + let (certificates, blocks) = paced_finalized_stores( + &context, + "recovery", + Duration::ZERO, + FinalizedSyncFailure::None, + ) + .await; + let (actor, mailbox) = actor::Actor::init( + context.child("actor"), + certificates.inner, + blocks.inner, + Config { + scheme_provider: ConstantProvider::new(scheme), + epocher: FixedEpocher::new(BLOCKS_PER_EPOCH), + mailbox_size: NZUsize!(100), + namespace: NAMESPACE.to_vec(), + view_retention_timeout: ViewDelta::new(10), + max_repair: NZUsize!(10), + max_pending_acks: NZUsize!(1), + block_codec_config: (), + partition_prefix: "recovery".into(), + prunable_items_per_section: NZU64!(10), + replay_buffer: NZUsize!(1024), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE), + strategy: Sequential, + }, + ) + .await; + let (rx, resolver) = RecordingResolver::holding(context.child("resolver")); + let application = Application::default(); + let handle = actor.start( + application.clone(), + buffer, + (rx, resolver.clone()), + // Deliberately newer than the archive round: startup must not use this + // diagnostic view to suppress a successor certificate's missing block. + SyncStart { + height, + epoch: height / BLOCKS_PER_EPOCH.get() + 1, + view: 1000, + }, + None, + ); + assert!(mailbox.get_processed_height().await.is_some()); + (application, mailbox, resolver, handle) +} + +fn chain(schemes: &[S], count: u64) -> Vec<(B, Finalization)> { + let mut parent = Sha256::hash(&[b""]); + (1..=count) + .map(|height| { + let block = B::new::(parent, Height::new(height), height); + parent = block.digest(); + let round = Round::new(block.epoch(), block.view()); + let certificate = make_finalization( + Proposal::new(round, View::new(height - 1), block.digest()), + schemes, + QUORUM, + ); + (block, certificate) + }) + .collect() +} + +async fn seed( + context: &deterministic::Context, + chain: &[(B, Finalization)], + missing: Option, +) { + let (certificates, blocks) = paced_finalized_stores( + context, + "recovery", + Duration::ZERO, + FinalizedSyncFailure::None, + ) + .await; + let (mut certificates, mut blocks) = (certificates.inner, blocks.inner); + for (block, certificate) in chain { + certificates = Certificates::put( + certificates, + block.height(), + block.digest(), + certificate.clone(), + ) + .await + .unwrap(); + if missing != Some(block.height().get()) { + blocks = Blocks::put(blocks, block.clone()).await.unwrap(); + } + } + Certificates::sync(certificates).await.unwrap(); + Blocks::sync(blocks).await.unwrap(); +} + +async fn processed(context: &deterministic::Context, mailbox: &crate::Mailbox, height: u64) { + while mailbox.get_processed_height().await != Some(Height::new(height)) { + context.sleep(Duration::from_millis(1)).await; + } +} + +#[test_traced("WARN")] +fn explicit_prune_preserves_replay_history_after_restart() { + pruning_restart(false); +} + +#[test_traced("WARN")] +fn set_floor_preserves_replay_history_after_restart() { + pruning_restart(true); +} + +fn pruning_restart(set_floor: bool) { + let ((validator, scheme, history), recovered) = deterministic::Runner::timed( + Duration::from_secs(60), + ) + .start_and_recover(|mut context| async move { + let Fixture { + participants, + schemes, + .. + } = bls12381_threshold::(&mut context, NUM_VALIDATORS); + let history = chain(&schemes, 40); + seed(&context, &history[..35], None).await; + let (application, mut mailbox, _, handle) = start_prunable( + context.child("live"), + participants[0].clone(), + schemes[0].clone(), + 19, + ) + .await; + processed(&context, &mailbox, 35).await; + assert_eq!( + application.blocks().keys().copied().collect::>(), + (20..=35).collect::>() + ); + // Invalid requests must still be ignored BEFORE clamping, rather + // than deleting an otherwise-safe old section. + mailbox.prune(Height::new(36)); + assert!(mailbox.get_block(1u64).await.is_some()); + assert!(mailbox.get_finalization(Height::new(1)).await.is_some()); + if set_floor { + let (anchor, certificate) = &history[39]; + assert!(mailbox.verified(certificate.round(), anchor.clone()).await); + mailbox.set_floor(certificate.clone()); + processed(&context, &mailbox, 40).await; + assert_eq!(application.blocks().get(&40), Some(anchor)); + } else { + mailbox.prune(Height::new(35)); + } + // Section size is ten: cutoff min(request, H+1)=20 must really + // delete both old sections, not just preserve successors by accident. + for height in 1..=19 { + assert!( + mailbox.get_block(height).await.is_none(), + "block {height} not pruned" + ); + assert!( + mailbox + .get_finalization(Height::new(height)) + .await + .is_none(), + "certificate {height} not pruned" + ); + } + for height in 20..=35 { + assert_eq!( + mailbox.get_block(height).await, + Some(history[height as usize - 1].0.clone()) + ); + assert!( + mailbox + .get_finalization(Height::new(height)) + .await + .is_some() + ); + } + handle.abort(); + let _ = handle.await; + (participants[0].clone(), schemes[0].clone(), history) + }); + deterministic::Runner::from(recovered).start(|context| async move { + let (application, mut mailbox, _, _) = + start_prunable(context.child("restart"), validator, scheme, 19).await; + processed(&context, &mailbox, 35).await; + assert_eq!( + application.blocks(), + history[19..35] + .iter() + .map(|(b, _)| (b.height().get(), b.clone())) + .collect() + ); + for height in 1..=19 { + assert!(mailbox.get_block(height).await.is_none()); + assert!( + mailbox + .get_finalization(Height::new(height)) + .await + .is_none() + ); + } + for height in 20..=35 { + assert!( + mailbox + .get_finalization(Height::new(height)) + .await + .is_some() + ); + } + }); +} + +#[test_traced("WARN")] +fn restart_after_pruning_keeps_certificate_only_successor_fetchable() { + let ((validator, scheme, successor), recovered) = deterministic::Runner::timed( + Duration::from_secs(60), + ) + .start_and_recover(|mut context| async move { + let Fixture { + participants, + schemes, + .. + } = bls12381_threshold::(&mut context, NUM_VALIDATORS); + let history = chain(&schemes, 20); + seed(&context, &history, Some(20)).await; + // Reproduce the on-disk result of pruning up to H+1 with H=19. + // Only the successor certificate remains, without its block. + let (certificates, blocks) = paced_finalized_stores( + &context, + "recovery", + Duration::ZERO, + FinalizedSyncFailure::None, + ) + .await; + let certificates = Certificates::prune(certificates.inner, Height::new(20)) + .await + .unwrap(); + let blocks = Blocks::prune(blocks.inner, Height::new(20)).await.unwrap(); + assert!( + Certificates::get(&certificates, ArchiveID::Index(19)) + .await + .unwrap() + .is_none() + ); + assert!( + Blocks::get(&blocks, ArchiveID::Index(19)) + .await + .unwrap() + .is_none() + ); + ( + participants[0].clone(), + schemes[0].clone(), + history[19].clone(), + ) + }); + deterministic::Runner::from(recovered).start(|context| async move { + let (application, mut mailbox, resolver, _) = + start_prunable(context.child("restart"), validator, scheme, 19).await; + assert!(application.blocks().is_empty()); + let (block, certificate) = successor; + let _ = mailbox.report(Activity::Finalization(certificate.clone())); + // The certificate-only successor must not become the processed round: + // a round-bound acquisition of its missing block must still be permitted. + wait_until(&context, Duration::from_secs(1), "successor fetch", || { + resolver + .active_fetches + .lock() + .unwrap() + .iter() + .any(|fetch| fetch.key == Key::Block(block.digest())) + }) + .await; + let fetch = resolver + .fetches() + .into_iter() + .find(|fetch| fetch.key == Key::Block(block.digest())) + .unwrap(); + let (response, rx) = oneshot::channel(); + let _ = resolver.enqueue(handler::Message::Deliver { + delivery: Delivery { + key: fetch.key, + subscribers: NonEmptyVec::new((fetch.subscriber, tracing::Span::none())), + }, + value: block.encode(), + response, + }); + assert!(rx.await.unwrap()); + processed(&context, &mailbox, 20).await; + assert_eq!(application.blocks(), BTreeMap::from([(20, block)])); + }); +} diff --git a/types/src/block.rs b/types/src/block.rs index 973a5e7e..03251483 100644 --- a/types/src/block.rs +++ b/types/src/block.rs @@ -49,15 +49,11 @@ impl Block { parent_beacon_block_root: [u8; 32], ) -> Self { let payload_ssz = payload.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&payload_ssz); - let payload_hash = hasher.finalize(); + let payload_hash = Sha256::hash(&[&payload_ssz]); let execution_request_hash = if !execution_requests.is_empty() { let execution_requests_ssz = execution_requests.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&execution_requests_ssz); - hasher.finalize() + Sha256::hash(&[&execution_requests_ssz]) } else { [0; 32].into() }; @@ -96,15 +92,11 @@ impl Block { execution_requests: Vec, ) -> Result { let payload_ssz = payload.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&payload_ssz); - let payload_hash = hasher.finalize(); + let payload_hash = Sha256::hash(&[&payload_ssz]); let execution_request_hash = if !execution_requests.is_empty() { let execution_requests_ssz = execution_requests.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&execution_requests_ssz); - hasher.finalize() + Sha256::hash(&[&execution_requests_ssz]) } else { [0; 32].into() }; @@ -125,9 +117,7 @@ impl Block { pub fn genesis(genesis_hash: [u8; 32]) -> Self { let payload = ExecutionPayloadV3::from_block_slow(&AlloyBlock::::default()); let payload_ssz = payload.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&payload_ssz); - let payload_hash = hasher.finalize(); + let payload_hash = Sha256::hash(&[&payload_ssz]); let header = Header::new_with_digest( genesis_hash.into(), @@ -639,7 +629,11 @@ mod test { Finalization { proposal, certificate: Certificate:: { - signers: Signers::from(n_validators, [0, 1, 2].map(Participant::new)), + signers: Signers::new( + n_validators.try_into().unwrap(), + [0, 1, 2].map(Participant::new), + ) + .unwrap(), signature: signature.into(), }, }; diff --git a/types/src/checkpoint.rs b/types/src/checkpoint.rs index 80497e0e..12dd9959 100644 --- a/types/src/checkpoint.rs +++ b/types/src/checkpoint.rs @@ -29,9 +29,7 @@ pub struct Checkpoint { impl Checkpoint { pub fn new(state: &ConsensusState) -> Self { let data = state.encode(); - let mut hasher = Sha256::new(); - hasher.update(&data); - let digest = hasher.finalize(); + let digest = Sha256::hash(&[&data]); Self { data, digest } } } @@ -89,9 +87,7 @@ impl Decode for Checkpoint { // Bind the redundant `digest` field to `data`: a decoded checkpoint must // satisfy `digest == sha256(data)`. let digest = Digest::from(digest_bytes); - let mut hasher = Sha256::new(); - hasher.update(&data); - if hasher.finalize() != digest { + if Sha256::hash(&[&data]) != digest { return Err(ssz::DecodeError::BytesInvalid( "checkpoint digest does not match sha256(data)".to_string(), )); @@ -142,9 +138,7 @@ impl TryFrom<&Checkpoint> for ConsensusState { fn try_from(checkpoint: &Checkpoint) -> Result { // Verify the digest matches the data - let mut hasher = Sha256::new(); - hasher.update(&checkpoint.data); - let computed_digest = hasher.finalize(); + let computed_digest = Sha256::hash(&[&checkpoint.data]); if computed_digest != checkpoint.digest { return Err(Error::Invalid("Checkpoint", "Digest verification failed")); @@ -497,9 +491,7 @@ pub fn verify_checkpoint_chain_with_weak_subjectivity( // Step 2: Compute the checkpoint digest and verify it matches the last header let last_header = finalized_headers.last().unwrap(); - let mut hasher = Sha256::new(); - hasher.update(&checkpoint.data); - let computed_digest = hasher.finalize(); + let computed_digest = Sha256::hash(&[&checkpoint.data]); if last_header.header().checkpoint_hash() != computed_digest { return Err(CheckpointVerificationError::CheckpointHashMismatch); } @@ -803,6 +795,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -931,6 +924,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -988,6 +982,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1124,6 +1119,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1186,6 +1182,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1253,6 +1250,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1316,6 +1314,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1474,6 +1473,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 3, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -1602,6 +1602,7 @@ mod tests { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: 256, minimum_validator_count: 1, invalid_deposit_tax: 0, max_pending_withdrawals_per_validator: 3, @@ -1616,6 +1617,7 @@ mod tests { 3, 16, 0, + 256, 1, 0, 3, @@ -1677,8 +1679,12 @@ mod tests { .take(3) .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap()) .collect(); - let finalization = Finalization::from_finalizes(&schemes[0], &finalizes, &Sequential) - .expect("finalization should aggregate"); + let finalization = Finalization::from_finalizes( + &schemes[0], + commonware_utils::non_empty![@&finalizes], + &Sequential, + ) + .expect("finalization should aggregate"); let finalized_header = FinalizedHeader::new(header, finalization, schemes.len()) .expect("honest header is bound to its certificate"); @@ -2207,6 +2213,7 @@ mod tests { 3, 16, 0, + 256, 1, 0, 3, diff --git a/types/src/consensus_state/mod.rs b/types/src/consensus_state/mod.rs index 283e51ed..cf277111 100644 --- a/types/src/consensus_state/mod.rs +++ b/types/src/consensus_state/mod.rs @@ -6,8 +6,9 @@ use crate::execution_request::{ }; use crate::header::AddedValidator; use crate::protocol_params::{ - DEFAULT_MAX_PENDING_WITHDRAWALS_PER_VALIDATOR, DEFAULT_MINIMUM_VALIDATOR_COUNT, - MAX_INVALID_DEPOSIT_TAX, MIN_ALLOWED_TIMESTAMP_FUTURE_MS, ProtocolParam, + DEFAULT_MAX_PENDING_WITHDRAWALS_PER_VALIDATOR, DEFAULT_MAX_VALIDATOR_COUNT, + DEFAULT_MINIMUM_VALIDATOR_COUNT, MAX_INVALID_DEPOSIT_TAX, MAX_MAX_VALIDATOR_COUNT, + MIN_ALLOWED_TIMESTAMP_FUTURE_MS, MIN_MAX_VALIDATOR_COUNT, ProtocolParam, }; use crate::ssz_state_tree::SszStateTree; use crate::utils::{invalid_deposit_refund_split, parse_withdrawal_credentials}; @@ -71,6 +72,7 @@ pub struct ConsensusState { pub(crate) max_deposits_per_epoch: u64, pub(crate) max_withdrawals_per_epoch: u64, pub(crate) observers_per_validator: u32, + pub(crate) max_validator_count: u64, pub(crate) minimum_validator_count: u64, pub(crate) pending_active_validator_exits: u64, pub(crate) invalid_deposit_tax: u64, @@ -142,6 +144,7 @@ impl Default for ConsensusState { max_deposits_per_epoch: 3, max_withdrawals_per_epoch: 16, observers_per_validator: 0, + max_validator_count: DEFAULT_MAX_VALIDATOR_COUNT, minimum_validator_count: DEFAULT_MINIMUM_VALIDATOR_COUNT, pending_active_validator_exits: 0, invalid_deposit_tax: 0, @@ -188,6 +191,7 @@ impl ConsensusState { max_deposits_per_epoch: self.max_deposits_per_epoch, max_withdrawals_per_epoch: self.max_withdrawals_per_epoch, observers_per_validator: self.observers_per_validator, + max_validator_count: self.max_validator_count, minimum_validator_count: self.minimum_validator_count, pending_active_validator_exits: self.pending_active_validator_exits, invalid_deposit_tax: self.invalid_deposit_tax, @@ -221,6 +225,7 @@ impl ConsensusState { max_deposits_per_epoch: u64, max_withdrawals_per_epoch: u64, observers_per_validator: u32, + max_validator_count: u64, minimum_validator_count: u64, invalid_deposit_tax: u64, max_pending_withdrawals_per_validator: u64, @@ -246,6 +251,7 @@ impl ConsensusState { max_deposits_per_epoch, max_withdrawals_per_epoch, observers_per_validator, + max_validator_count, minimum_validator_count, pending_active_validator_exits: 0, invalid_deposit_tax, @@ -382,6 +388,91 @@ impl ConsensusState { self.ssz_tree.set_observers_per_validator(value); } + pub fn get_max_validator_count(&self) -> u64 { + self.max_validator_count + } + + pub fn set_max_validator_count(&mut self, value: u64) { + self.max_validator_count = value; + self.ssz_tree.set_max_validator_count(value); + } + + /// Returns the validator cap that will apply after queued protocol-parameter + /// changes are applied at the next epoch boundary. + pub fn prospective_max_validator_count(&self) -> u64 { + self.protocol_param_changes + .iter() + .rev() + .find_map(|param| match param { + ProtocolParam::MaxValidatorCount(value) => Some(*value), + _ => None, + }) + .unwrap_or(self.max_validator_count) + } + + /// Validate the final proposed pair as a batch, not request by request: a + /// valid coordinated raise/lowering must not depend on request ordering. + /// Run before prospective values influence exits or admissions, and again + /// when applying changes (including changes restored from a checkpoint). + fn reject_invalid_validator_count_changes(&mut self) { + let minimum = self.prospective_minimum_validator_count(); + let maximum = self.prospective_max_validator_count(); + let before = self.protocol_param_changes.len(); + + // Preserve whole-pair rejection for an initially conflicting pair. + // Otherwise, reject cap updates below existing reservations before any + // same-batch withdrawals can free slots. Equality is allowed. + if minimum <= maximum + && self + .protocol_param_changes + .iter() + .any(|param| matches!(param, ProtocolParam::MaxValidatorCount(_))) + { + let reserved = self.active_or_joining_validator_count(); + if maximum < reserved { + warn!( + maximum, + reserved, "discarding validator cap updates below active and joining count" + ); + self.protocol_param_changes + .retain(|param| !matches!(param, ProtocolParam::MaxValidatorCount(_))); + } + } + + // Recheck against the retained cap if its proposed change was rejected. + let maximum = self.prospective_max_validator_count(); + if minimum > maximum { + warn!( + minimum, + maximum, "discarding conflicting validator count updates" + ); + self.protocol_param_changes.retain(|param| { + !matches!( + param, + ProtocolParam::MinimumValidatorCount(_) | ProtocolParam::MaxValidatorCount(_) + ) + }); + } + if self.protocol_param_changes.len() != before { + self.ssz_tree + .rebuild_protocol_params(&self.protocol_param_changes); + } + } + + /// Last queued observer setting, used when sizing a restarted network. + pub fn prospective_observers_per_validator(&self) -> u32 { + self.protocol_param_changes + .iter() + .rev() + .find_map(|param| match param { + ProtocolParam::ObserversPerValidator(value) => { + Some((*value).try_into().expect("observer count exceeds u32")) + } + _ => None, + }) + .unwrap_or(self.observers_per_validator) + } + pub fn get_max_pending_withdrawals_per_validator(&self) -> u64 { self.max_pending_withdrawals_per_validator } @@ -1039,6 +1130,7 @@ impl ConsensusState { if account.status == ValidatorStatus::Inactive && account.balance >= self.get_minimum_stake() && self.withdrawal_queue.pending_count(&node_pubkey_bytes) == 0 + && self.active_or_joining_validator_count() < self.prospective_max_validator_count() { let activation_epoch = self.get_epoch() + warm_up_epochs; account.status = ValidatorStatus::Joining; @@ -1110,6 +1202,8 @@ impl ConsensusState { self.push_protocol_param_changes(protocol_param_batch); } + self.reject_invalid_validator_count_changes(); + // Second pass: route deposits, withdrawals, and malformed deposits in // order, now that protocol params are already staged. let mut withdrawal_tree_stale = false; @@ -1837,6 +1931,18 @@ impl ConsensusState { >= self.prospective_minimum_validator_count() } + pub fn active_or_joining_validator_count(&self) -> u64 { + self.validator_accounts + .values() + .filter(|account| { + matches!( + account.status, + ValidatorStatus::Active | ValidatorStatus::Joining + ) + }) + .count() as u64 + } + pub fn get_active_or_joining_validators(&self) -> Vec<(PublicKey, bls12381::PublicKey)> { let mut peers: Vec<(PublicKey, bls12381::PublicKey)> = self .validator_accounts @@ -1857,6 +1963,7 @@ impl ConsensusState { } pub fn apply_protocol_parameter_changes(&mut self) -> Result { + self.reject_invalid_validator_count_changes(); let mut minimum_stake_changed = false; for param in self.protocol_param_changes.drain(0..) { match param { @@ -1895,6 +2002,10 @@ impl ConsensusState { self.observers_per_validator = value; self.ssz_tree.set_observers_per_validator(value); } + ProtocolParam::MaxValidatorCount(value) => { + self.max_validator_count = value; + self.ssz_tree.set_max_validator_count(value); + } ProtocolParam::MinimumValidatorCount(value) => { self.minimum_validator_count = value; self.ssz_tree.set_minimum_validator_count(value); @@ -1948,6 +2059,7 @@ impl ConsensusState { self.max_deposits_per_epoch, self.max_withdrawals_per_epoch, self.observers_per_validator, + self.max_validator_count, &self.pending_execution_requests, self.pending_checkpoint.as_ref().map(|cp| cp.digest.0), &self.epocher.encode(), @@ -2006,6 +2118,7 @@ impl EncodeSize for ConsensusState { + 8 // max_deposits_per_epoch + 8 // max_withdrawals_per_epoch + 4 // observers_per_validator + + 8 // max_validator_count + 8 // minimum_validator_count + 8 // pending_active_validator_exits + 8 // invalid_deposit_tax @@ -2186,6 +2299,13 @@ impl Read for ConsensusState { "observers per validator out of bounds", )); } + let max_validator_count = buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?; + if !(MIN_MAX_VALIDATOR_COUNT..=MAX_MAX_VALIDATOR_COUNT).contains(&max_validator_count) { + return Err(Error::Invalid( + "ConsensusState", + "max validator count out of bounds", + )); + } let minimum_validator_count = buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?; if minimum_validator_count == 0 { return Err(Error::Invalid( @@ -2193,6 +2313,12 @@ impl Read for ConsensusState { "minimum validator count out of bounds", )); } + if minimum_validator_count > max_validator_count { + return Err(Error::Invalid( + "ConsensusState", + "minimum validator count exceeds max validator count", + )); + } let pending_active_validator_exits = buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?; let current_epoch_active_validator_count = validator_accounts .values() @@ -2275,6 +2401,7 @@ impl Read for ConsensusState { max_deposits_per_epoch, max_withdrawals_per_epoch, observers_per_validator, + max_validator_count, minimum_validator_count, pending_active_validator_exits, invalid_deposit_tax, @@ -2393,6 +2520,9 @@ impl Write for ConsensusState { // Write observers_per_validator buf.put_u32(self.observers_per_validator); + // Write max_validator_count + buf.put_u64(self.max_validator_count); + // Write minimum_validator_count buf.put_u64(self.minimum_validator_count); diff --git a/types/src/consensus_state/tests/codec.rs b/types/src/consensus_state/tests/codec.rs index 0d984fb4..60f834e9 100644 --- a/types/src/consensus_state/tests/codec.rs +++ b/types/src/consensus_state/tests/codec.rs @@ -79,6 +79,7 @@ fn test_serialization_deserialization_populated() { 3, 16, 0, + DEFAULT_MAX_VALIDATOR_COUNT, DEFAULT_MINIMUM_VALIDATOR_COUNT, 0, 3, @@ -399,6 +400,40 @@ fn test_decode_rejects_out_of_range_max_pending_withdrawals_per_validator() { } } +#[test] +fn test_decode_rejects_out_of_range_max_validator_count() { + use crate::protocol_params::{MAX_MAX_VALIDATOR_COUNT, MIN_MAX_VALIDATOR_COUNT}; + + for valid in [MIN_MAX_VALIDATOR_COUNT, MAX_MAX_VALIDATOR_COUNT] { + let mut state = ConsensusState::default(); + state.max_validator_count = valid; + state.set_minimum_validator_count(1); + let encoded = state.encode(); + let decoded = ConsensusState::read(&mut encoded.as_ref()) + .unwrap_or_else(|_| panic!("valid max_validator_count {valid} should decode")); + assert_eq!(decoded.max_validator_count, valid); + } + + for invalid in [0, MAX_MAX_VALIDATOR_COUNT + 1] { + let mut state = ConsensusState::default(); + state.max_validator_count = invalid; + let encoded = state.encode(); + assert!( + ConsensusState::read(&mut encoded.as_ref()).is_err(), + "max_validator_count {invalid} should be rejected on decode" + ); + } +} + +#[test] +fn test_decode_rejects_minimum_validator_count_above_maximum() { + let mut state = ConsensusState::default(); + state.set_minimum_validator_count(state.get_max_validator_count() + 1); + assert!(ConsensusState::decode(state.encode()).is_err()); + let checkpoint = crate::checkpoint::Checkpoint::new(&state); + assert!(ConsensusState::try_from(&checkpoint).is_err()); +} + #[test] fn test_decode_rejects_out_of_range_max_deposits_per_epoch() { // Genesis and runtime updates cap deposits at MAX_MAX_DEPOSITS_PER_EPOCH; diff --git a/types/src/consensus_state/tests/deposits.rs b/types/src/consensus_state/tests/deposits.rs index 23f7463b..c36b18eb 100644 --- a/types/src/consensus_state/tests/deposits.rs +++ b/types/src/consensus_state/tests/deposits.rs @@ -201,6 +201,54 @@ fn new_account_at_min_activates() { assert!(state.has_added_validators(WARM_UP)); } +// Active and joining validators both reserve slots. A valid deposit received +// while the cap is full is credited, but the new validator remains inactive. +#[test] +fn validator_cap_credits_deposit_without_scheduling_activation() { + let mut state = deposit_state(); + state.set_max_validator_count(2); + + let active_node = ed25519::PrivateKey::from_seed(101); + let active_bls = bls12381::PrivateKey::from_seed(101); + seed_account( + &mut state, + &active_node, + &active_bls, + ValidatorStatus::Active, + MIN, + ); + + let joining_node = ed25519::PrivateKey::from_seed(102); + let joining_bls = bls12381::PrivateKey::from_seed(102); + seed_account( + &mut state, + &joining_node, + &joining_bls, + ValidatorStatus::Joining, + MIN, + ); + assert_eq!(state.active_or_joining_validator_count(), 2); + + let node = ed25519::PrivateKey::from_seed(103); + let bls = bls12381::PrivateKey::from_seed(103); + let key = node_bytes(&node); + state.push_deposit(make_signed_deposit( + &node, + &bls, + eth1_credentials(3), + MIN, + 0, + test_domain(), + )); + + state.process_deposits(test_domain(), WARM_UP, WITHDRAWAL_EPOCHS); + + let account = state.get_account(&key).unwrap(); + assert_eq!(account.status, ValidatorStatus::Inactive); + assert_eq!(account.balance, MIN); + assert!(!state.has_added_validators(WARM_UP)); +} + // A top up that lifts an inactive validator to the minimum stake rejoins it. #[test] fn inactive_topup_to_min_rejoins() { diff --git a/types/src/consensus_state/tests/mod.rs b/types/src/consensus_state/tests/mod.rs index ebf6c82d..d8ee7a93 100644 --- a/types/src/consensus_state/tests/mod.rs +++ b/types/src/consensus_state/tests/mod.rs @@ -10,4 +10,5 @@ mod payouts; mod protocol_params; mod ssz; mod state; +mod validator_cap; mod withdrawals; diff --git a/types/src/consensus_state/tests/protocol_params.rs b/types/src/consensus_state/tests/protocol_params.rs index e4689d27..9748d08a 100644 --- a/types/src/consensus_state/tests/protocol_params.rs +++ b/types/src/consensus_state/tests/protocol_params.rs @@ -1,5 +1,16 @@ use super::super::*; +#[test] +fn protocol_param_batch_applies_max_validator_count_change() { + let mut state = ConsensusState::default(); + state.push_protocol_param_change(ProtocolParam::MaxValidatorCount(128)); + + assert_eq!(state.prospective_max_validator_count(), 128); + state.apply_protocol_parameter_changes().unwrap(); + + assert_eq!(state.get_max_validator_count(), 128); +} + #[test] fn protocol_param_batch_applies_minimum_stake_change() { let mut state = ConsensusState::default(); diff --git a/types/src/consensus_state/tests/ssz.rs b/types/src/consensus_state/tests/ssz.rs index 6718e569..fae5a88d 100644 --- a/types/src/consensus_state/tests/ssz.rs +++ b/types/src/consensus_state/tests/ssz.rs @@ -640,6 +640,7 @@ fn test_ssz_full_block_lifecycle_matches_rebuild() { 3, 16, 0, + DEFAULT_MAX_VALIDATOR_COUNT, DEFAULT_MINIMUM_VALIDATOR_COUNT, 0, 3, @@ -863,6 +864,7 @@ fn test_genesis_materialization_refreshes_proof_snapshot() { 3, 16, 0, + DEFAULT_MAX_VALIDATOR_COUNT, 0, 0, 3, diff --git a/types/src/consensus_state/tests/state.rs b/types/src/consensus_state/tests/state.rs index b6963aec..a5c52a40 100644 --- a/types/src/consensus_state/tests/state.rs +++ b/types/src/consensus_state/tests/state.rs @@ -102,6 +102,7 @@ fn test_clone_preserves_epoch_schedule_snapshot() { 3, 16, 0, + DEFAULT_MAX_VALIDATOR_COUNT, 0, 0, 3, diff --git a/types/src/consensus_state/tests/validator_cap.rs b/types/src/consensus_state/tests/validator_cap.rs new file mode 100644 index 00000000..ac5b675a --- /dev/null +++ b/types/src/consensus_state/tests/validator_cap.rs @@ -0,0 +1,870 @@ +//! Admission-cap regressions through the buffered request path used by the +//! finalizer at the penultimate block. Committee changes/payouts are applied +//! separately, as they are at finalized epoch boundaries. +use super::common::{create_test_validator_account, eth1_credentials, make_signed_deposit}; +use crate::account::ValidatorStatus; +use crate::checkpoint::Checkpoint; +use crate::consensus_state::ConsensusState; +use crate::execution_request::{DepositRequest, ProtocolParamRequest, WithdrawalRequest}; +use crate::protocol_params::ProtocolParam; +use crate::{Digest, PublicKey, deposit_signature_domain}; +use alloy_primitives::{Address, Bytes}; +use commonware_codec::{DecodeExt, Encode, Write}; +use commonware_cryptography::{Signer, bls12381, ed25519}; + +const MIN: u64 = 32; +const WARM_UP: u64 = 2; +const PAYOUT_DELAY: u64 = 2; + +fn domain() -> Digest { + deposit_signature_domain([9; 32], b"_TEST") +} + +fn key(seed: u64) -> PublicKey { + ed25519::PrivateKey::from_seed(seed).public_key() +} + +fn account_key(seed: u64) -> [u8; 32] { + key(seed).as_ref().try_into().unwrap() +} + +fn deposit(seed: u64, amount: u64, index: u64) -> DepositRequest { + make_signed_deposit( + &ed25519::PrivateKey::from_seed(seed), + &bls12381::PrivateKey::from_seed(seed), + eth1_credentials(1), + amount, + index, + domain(), + ) +} + +fn deposit_entry(seed: u64, amount: u64, index: u64) -> Bytes { + let mut bytes = vec![0x00]; + deposit(seed, amount, index).write(&mut bytes); + bytes.into() +} + +fn withdrawal_entry(seed: u64, amount: u64, authorized: bool) -> Bytes { + let mut bytes = vec![0x01]; + WithdrawalRequest { + validator_pubkey: account_key(seed), + source_address: Address::from([if authorized { 1 } else { 9 }; 20]), + amount, + } + .write(&mut bytes); + bytes.into() +} + +fn param_entry(id: u8, value: u64) -> Bytes { + let mut bytes = vec![0xff]; + ProtocolParamRequest { + param_id: id, + param: value.to_le_bytes().to_vec(), + } + .write(&mut bytes); + bytes.into() +} + +fn state(cap: u64) -> ConsensusState { + let mut state = ConsensusState::default(); + state.set_minimum_validator_count(1); + state.set_max_validator_count(cap); + state.set_minimum_stake(MIN); + state.set_max_deposits_per_epoch(16); + state.set_max_withdrawals_per_epoch(16); + state +} + +fn seed_active(state: &mut ConsensusState, seed: u64) { + let mut account = create_test_validator_account(1, MIN + 10); + account.consensus_public_key = bls12381::PrivateKey::from_seed(seed).public_key(); + state.set_account(account_key(seed), account); +} + +fn process(state: &mut ConsensusState, entries: &[Bytes]) { + state.buffer_execution_requests(entries); + state.process_buffered_requests(domain(), WARM_UP, PAYOUT_DELAY); +} + +// Boundary ordering mirrors the finalizer: apply params, transition committee, +// advance epoch, then retire consumed deltas. Payout tests apply terminal-block +// payouts explicitly before calling this helper. +fn advance_epoch(state: &mut ConsensusState) { + state.apply_protocol_parameter_changes().unwrap(); + state.apply_committee_transition(&key(99_999)); + let next = state.get_epoch() + 1; + state.set_epoch(next); + state.remove_added_validators_for_epoch(next); + state.clear_removed_validators(); + assert_tree_consistent(state); +} + +fn status(state: &ConsensusState, seed: u64) -> ValidatorStatus { + state + .get_account(&account_key(seed)) + .unwrap() + .status + .clone() +} + +fn assert_tree_consistent(state: &ConsensusState) { + let root = state.ssz_tree().root(); + let mut rebuilt = state.clone(); + rebuilt.rebuild_ssz_tree(); + assert_eq!(root, rebuilt.ssz_tree().root()); +} + +#[test] +fn conflicting_pair_is_discarded_before_deposit_and_exit_decisions() { + // Neither the rejected raise nor the rejected lowering may influence + // admission/exit decisions, even though params occur after other requests. + for (minimum, maximum) in [(4, 3), (2, 1)] { + let mut state = state(2); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + process( + &mut state, + &[ + deposit_entry(3, MIN, 0), + deposit_entry(4, MIN, 1), + withdrawal_entry(1, 0, true), + param_entry(0x07, minimum), + param_entry(0x0a, maximum), + param_entry(0x06, 8), + ], + ); + assert_eq!(status(&state, 1), ValidatorStatus::SubmittedExitRequest); + assert_eq!(status(&state, 3), ValidatorStatus::Joining); + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + assert_eq!(state.prospective_minimum_validator_count(), 1); + assert_eq!(state.prospective_max_validator_count(), 2); + assert_tree_consistent(&state); + state.apply_protocol_parameter_changes().unwrap(); + assert_eq!(state.get_minimum_validator_count(), 1); + assert_eq!(state.get_max_validator_count(), 2); + assert_eq!(state.get_observers_per_validator(), 8); + assert_tree_consistent(&state); + } +} + +#[test] +fn conflicting_single_updates_are_discarded_at_boundary_too() { + for params in [ + vec![ProtocolParam::MinimumValidatorCount(3)], + vec![ProtocolParam::MaxValidatorCount(1)], + vec![ + ProtocolParam::MinimumValidatorCount(4), + ProtocolParam::MaxValidatorCount(3), + ], + ] { + let mut state = state(2); + state.set_minimum_validator_count(2); + state.push_protocol_param_changes(params); + state.push_protocol_param_change(ProtocolParam::ObserversPerValidator(8)); + // A checkpoint may carry pending updates; current scalar values are valid. + let mut restored = ConsensusState::try_from(&Checkpoint::new(&state)).unwrap(); + restored.apply_protocol_parameter_changes().unwrap(); + assert_eq!(restored.get_minimum_validator_count(), 2); + assert_eq!(restored.get_max_validator_count(), 2); + assert_eq!(restored.get_observers_per_validator(), 8); + assert_tree_consistent(&restored); + } +} + +#[test] +fn valid_paired_updates_are_order_independent_and_last_values_win() { + for (old_min, old_max, new_min, new_max) in [(1, 2, 3, 4), (3, 4, 1, 2)] { + for reverse in [false, true] { + let mut state = state(old_max); + state.set_minimum_validator_count(old_min); + let mut entries = vec![param_entry(0x07, new_min), param_entry(0x0a, new_max)]; + if reverse { + entries.reverse(); + } + process(&mut state, &entries); + state.apply_protocol_parameter_changes().unwrap(); + assert_eq!(state.get_minimum_validator_count(), new_min); + assert_eq!(state.get_max_validator_count(), new_max); + } + } + for final_cap in [1, 2] { + let mut state = state(1); + seed_active(&mut state, 1); + process( + &mut state, + &[ + deposit_entry(2, MIN, 0), + param_entry(0x07, 3), // Temporarily inconsistent pair is repaired below. + param_entry(0x0a, 3), + param_entry(0x07, 1), + param_entry(0x0a, final_cap), + ], + ); + assert_eq!( + status(&state, 2), + if final_cap == 2 { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + state.apply_protocol_parameter_changes().unwrap(); + assert_eq!(state.get_max_validator_count(), final_cap); + assert_tree_consistent(&state); + } +} + +#[test] +fn deposits_compete_in_queue_order_and_raising_cap_requires_another_deposit() { + let mut state = state(1); + process( + &mut state, + &[deposit_entry(1, MIN, 0), deposit_entry(2, MIN, 1)], + ); + assert_eq!(status(&state, 1), ValidatorStatus::Joining); + assert_eq!(status(&state, 2), ValidatorStatus::Inactive); + assert_eq!(state.get_account(&account_key(2)).unwrap().balance, MIN); + assert_eq!(state.get_added_validators(WARM_UP).unwrap().len(), 1); + process(&mut state, &[param_entry(0x0a, 2)]); + state.apply_protocol_parameter_changes().unwrap(); + process(&mut state, &[]); + assert_eq!(status(&state, 2), ValidatorStatus::Inactive); + process(&mut state, &[deposit_entry(2, 1, 2)]); + assert_eq!(status(&state, 2), ValidatorStatus::Joining); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_tree_consistent(&state); +} + +#[test] +fn only_accepted_full_active_exit_frees_a_slot() { + // accepted, wrong address, exit floor, withdrawal queue full, partial + for case in 0..5 { + let mut state = state(2); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + if case == 2 { + state.set_minimum_validator_count(2); + } + if case == 3 { + state.set_max_pending_withdrawals_per_validator(1); + process(&mut state, &[withdrawal_entry(1, 1, true)]); + } + process( + &mut state, + &[ + deposit_entry(3, MIN, 0), + withdrawal_entry(1, if case == 4 { 1 } else { 0 }, case != 1), + withdrawal_entry(1, 0, case == 0), // Duplicate cannot free another slot. + deposit_entry(4, MIN, 1), + ], + ); + assert_eq!( + status(&state, 3), + if case == 0 { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + assert_eq!(state.get_removed_validators().len(), usize::from(case == 0)); + state.apply_committee_transition(&key(2)); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_tree_consistent(&state); + } +} + +#[test] +fn joining_withdrawal_cancels_reservation_only_when_accepted() { + // full, partial, wrong address, full pending-withdrawal queue + for case in 0..4 { + let mut state = state(1); + process(&mut state, &[deposit_entry(1, MIN, 0)]); + if case == 3 { + // Model an already occupied queue without invoking cancellation. + state.set_max_pending_withdrawals_per_validator(1); + state.push_withdrawal_request( + WithdrawalRequest { + validator_pubkey: account_key(1), + source_address: Address::from([1; 20]), + amount: 1, + }, + PAYOUT_DELAY, + ); + } + process( + &mut state, + &[ + deposit_entry(2, MIN, 1), + withdrawal_entry(1, if case == 1 { 1 } else { 0 }, case != 2), + ], + ); + let additions = state.get_added_validators(WARM_UP).unwrap(); + assert_eq!(additions.len(), 1); + assert_eq!(additions[0].node_key, key(if case < 2 { 2 } else { 1 })); + assert_eq!( + status(&state, 2), + if case < 2 { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + assert_eq!(state.active_or_joining_validator_count(), 1); + assert_tree_consistent(&state); + } +} + +#[test] +fn cap_blocked_account_can_withdraw_and_pending_payout_prevents_reactivation() { + for full in [false, true] { + let mut state = state(1); + seed_active(&mut state, 1); + process(&mut state, &[deposit_entry(2, MIN, 0)]); + assert_eq!(status(&state, 2), ValidatorStatus::Inactive); + process( + &mut state, + &[ + deposit_entry(2, 5, 1), + withdrawal_entry(2, if full { 0 } else { 1 }, true), + param_entry(0x0a, 2), + ], + ); + assert_eq!( + status(&state, 2), + if full { + ValidatorStatus::FullPayoutPending + } else { + ValidatorStatus::Inactive + } + ); + assert_eq!(state.get_account(&account_key(2)).unwrap().balance, MIN + 5); + assert!(!state.has_added_validators(WARM_UP)); + state.apply_protocol_parameter_changes().unwrap(); + let payouts = state.emit_withdrawal_payouts(PAYOUT_DELAY); + assert_eq!(payouts.len(), 1); + assert_eq!(payouts[0].amount, if full { MIN + 5 } else { 1 }); + state.apply_withdrawal_payouts(PAYOUT_DELAY, &payouts); + if full { + assert!(state.get_account(&account_key(2)).is_none()); + } else { + assert_eq!(status(&state, 2), ValidatorStatus::Inactive); + process(&mut state, &[deposit_entry(2, 1, 2)]); + assert_eq!(status(&state, 2), ValidatorStatus::Joining); + } + assert_tree_consistent(&state); + } +} + +#[test] +fn first_deposit_and_same_batch_withdrawal_do_not_cancel_each_other() { + let mut state = state(1); + seed_active(&mut state, 1); + process( + &mut state, + &[deposit_entry(2, MIN, 0), withdrawal_entry(2, 0, true)], + ); + // Withdrawal precedes account creation despite deposit appearing first. + assert_eq!(status(&state, 2), ValidatorStatus::Inactive); + assert_eq!(state.get_withdrawal_count_for_epoch(PAYOUT_DELAY), 0); + process(&mut state, &[withdrawal_entry(2, 0, true)]); + assert_eq!(status(&state, 2), ValidatorStatus::FullPayoutPending); + assert_eq!(state.get_withdrawal_count_for_epoch(PAYOUT_DELAY), 1); +} + +#[test] +fn cap_below_reserved_membership_is_rejected_across_codec_and_checkpoint_restore() { + let mut state = state(2); + process( + &mut state, + &[deposit_entry(1, MIN, 0), deposit_entry(2, MIN, 1)], + ); + // Restore a queued update that has not yet passed batch validation. The + // boundary fallback must also reject a cap below existing reservations. + state.push_protocol_param_change(ProtocolParam::MaxValidatorCount(1)); + state.capture_state_root(0); + for mut restored in [ + ConsensusState::decode(state.encode()).unwrap(), + ConsensusState::try_from(&Checkpoint::new(&state)).unwrap(), + ] { + restored.apply_protocol_parameter_changes().unwrap(); + assert_eq!(restored.get_max_validator_count(), 2); + assert_eq!(restored.active_or_joining_validator_count(), 2); + assert_eq!(restored.get_added_validators(WARM_UP).unwrap().len(), 2); + assert_eq!(restored.get_state_root(), state.get_state_root()); + process(&mut restored, &[deposit_entry(3, MIN, 2)]); + assert_eq!(status(&restored, 3), ValidatorStatus::Inactive); + restored.set_epoch(WARM_UP - 1); + restored.apply_committee_transition(&key(1)); + assert_eq!(restored.get_active_validators().len(), 2); + assert_tree_consistent(&restored); + } +} + +#[test] +fn prospective_raise_and_funded_inactive_accounts_survive_restore() { + let mut state = state(1); + seed_active(&mut state, 1); + process( + &mut state, + &[ + deposit_entry(2, MIN, 0), + deposit_entry(3, MIN, 1), + param_entry(0x0a, 2), + ], + ); + // More reservations than the stored cap is legitimate before boundary application. + assert_eq!(state.get_max_validator_count(), 1); + for mut restored in [ + ConsensusState::decode(state.encode()).unwrap(), + ConsensusState::try_from(&Checkpoint::new(&state)).unwrap(), + ] { + assert_eq!(restored.prospective_max_validator_count(), 2); + assert_eq!(status(&restored, 2), ValidatorStatus::Joining); + assert_eq!(status(&restored, 3), ValidatorStatus::Inactive); + assert_eq!(restored.get_account(&account_key(3)).unwrap().balance, MIN); + restored.apply_protocol_parameter_changes().unwrap(); + process(&mut restored, &[deposit_entry(4, MIN, 2)]); + assert_eq!(status(&restored, 4), ValidatorStatus::Inactive); + assert_eq!(restored.active_or_joining_validator_count(), 2); + assert_tree_consistent(&restored); + } +} + +#[test] +fn deposit_backlog_uses_processing_epoch_cap_and_preserves_fifo() { + for (old_cap, new_cap) in [(1, 2), (3, 2)] { + let mut state = state(old_cap); + state.set_max_deposits_per_epoch(1); + seed_active(&mut state, 1); + process( + &mut state, + &[deposit_entry(2, MIN, 0), deposit_entry(3, MIN, 1)], + ); + assert_eq!(state.deposit_count(), 1); + assert!(state.get_account(&account_key(3)).is_none()); + assert_eq!( + status(&state, 2), + if old_cap == 1 { + ValidatorStatus::Inactive + } else { + ValidatorStatus::Joining + } + ); + advance_epoch(&mut state); + + // The older, still-uncredited deposit competes under the new cap, ahead + // of the newly submitted deposit. The first deposit is never retried. + process( + &mut state, + &[deposit_entry(4, MIN, 2), param_entry(0x0a, new_cap)], + ); + assert_eq!( + status(&state, 3), + if old_cap == 1 { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + assert!(state.get_account(&account_key(4)).is_none()); + assert_eq!(state.deposit_count(), 1); + assert_eq!(state.get_deposit(0).unwrap().index, 2); + assert_eq!(state.get_account(&account_key(2)).unwrap().balance, MIN); + advance_epoch(&mut state); + process(&mut state, &[]); + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + assert_eq!(state.deposit_count(), 0); + advance_epoch(&mut state); + process(&mut state, &[]); + for seed in [2, 3, 4] { + assert_eq!(state.get_account(&account_key(seed)).unwrap().balance, MIN); + } + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_tree_consistent(&state); + } +} + +#[test] +fn terminal_block_cap_update_is_deferred_and_precedes_next_epoch_updates() { + for final_cap in [2, 3] { + let mut state = state(2); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + process(&mut state, &[deposit_entry(3, MIN, 0)]); // Epoch 0 penultimate. + assert_eq!(status(&state, 3), ValidatorStatus::Inactive); + // These arrive on epoch 0's terminal block: buffer, but do not process. + state.buffer_execution_requests(&[ + param_entry(0x07, 3), + param_entry(0x0a, 3), + withdrawal_entry(1, 0, true), + deposit_entry(4, MIN, 1), + ]); + advance_epoch(&mut state); + assert_eq!(state.get_max_validator_count(), 2); + assert_eq!(state.get_minimum_validator_count(), 1); + assert_eq!(status(&state, 1), ValidatorStatus::Active); + assert_eq!(status(&state, 3), ValidatorStatus::Inactive); + assert!(state.get_account(&account_key(4)).is_none()); + + let entries = if final_cap == 2 { + vec![ + param_entry(0x07, 1), + param_entry(0x0a, 2), + deposit_entry(5, MIN, 2), + ] + } else { + vec![deposit_entry(5, MIN, 2)] + }; + process(&mut state, &entries); // Epoch 1 penultimate. + // Without an override, minimum=3 rejects the exit; with the later pair, + // minimum=1 permits it. Exactly one deposit gets a slot in either case. + assert_eq!( + status(&state, 1), + if final_cap == 2 { + ValidatorStatus::SubmittedExitRequest + } else { + ValidatorStatus::Active + } + ); + assert_eq!(status(&state, 4), ValidatorStatus::Joining); + assert_eq!( + state.get_account(&account_key(4)).unwrap().joining_epoch, + 1 + WARM_UP + ); + assert_eq!(status(&state, 5), ValidatorStatus::Inactive); + assert_eq!(state.get_withdrawal_count_for_epoch(PAYOUT_DELAY), 0); + assert_eq!( + state.get_withdrawal_count_for_epoch(1 + PAYOUT_DELAY), + usize::from(final_cap == 2) + ); + advance_epoch(&mut state); + assert_eq!(state.get_max_validator_count(), final_cap); + assert_eq!( + state.get_minimum_validator_count(), + if final_cap == 2 { 1 } else { 3 } + ); + } +} + +#[test] +fn stake_enforcement_after_admission_does_not_retry_cap_blocked_deposits() { + for joining in [false, true] { + for accepted in [false, true] { + let mut state = state(3); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + if joining { + process(&mut state, &[deposit_entry(3, MIN, 0)]); + } else { + seed_active(&mut state, 3); + let mut account = state.get_account(&account_key(3)).unwrap().clone(); + account.balance = MIN; + state.set_account(account_key(3), account); + } + advance_epoch(&mut state); + process( + &mut state, + &[ + deposit_entry(4, MIN + 10, 1), + param_entry(0x07, if accepted { 2 } else { 3 }), + param_entry(0x00, MIN + 1), + ], + ); + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + state.enforce_minimum_stake(); // Production ordering: after deposits. + if joining { + assert_eq!(state.has_added_validators(WARM_UP), !accepted); + } else { + assert_eq!(state.get_removed_validators().contains(&key(3)), accepted); + } + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + advance_epoch(&mut state); + assert_eq!( + state.get_minimum_stake(), + if accepted { MIN + 1 } else { MIN } + ); + assert_eq!( + status(&state, 3), + if accepted { + ValidatorStatus::Inactive + } else { + ValidatorStatus::Active + } + ); + assert_eq!( + state.active_or_joining_validator_count(), + if accepted { 2 } else { 3 } + ); + process(&mut state, &[deposit_entry(4, 1, 2)]); + assert_eq!( + status(&state, 4), + if accepted { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + assert_tree_consistent(&state); + } + } +} + +#[test] +fn same_batch_exit_cannot_validate_reduction_but_later_update_can() { + // Both a voluntary active exit and a joining cancellation occur too late + // to make the same batch's reduction valid. + for joining in [false, true] { + let mut state = state(3); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + if joining { + process(&mut state, &[deposit_entry(3, MIN, 0)]); + } else { + seed_active(&mut state, 3); + } + process( + &mut state, + &[withdrawal_entry(3, 0, true), param_entry(0x0a, 2)], + ); + assert_eq!(state.prospective_max_validator_count(), 3); + assert_eq!(state.active_or_joining_validator_count(), 2); + advance_epoch(&mut state); + assert_eq!(state.get_max_validator_count(), 3); + + // A later reduction to the now-current count is valid. Deposits see + // the accepted smaller cap and cannot take the old spare slot. + process( + &mut state, + &[param_entry(0x0a, 2), deposit_entry(4, MIN, 1)], + ); + assert_eq!(state.prospective_max_validator_count(), 2); + assert_eq!(status(&state, 4), ValidatorStatus::Inactive); + advance_epoch(&mut state); + assert_eq!(state.get_max_validator_count(), 2); + process( + &mut state, + &[withdrawal_entry(2, 0, true), deposit_entry(4, 1, 2)], + ); + assert_eq!(status(&state, 4), ValidatorStatus::Joining); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_tree_consistent(&state); + } +} + +#[test] +fn cap_reduction_counts_active_and_joining_but_not_inactive_accounts() { + for joining_count in 0..=2 { + let mut state = state(4); + state.set_minimum_validator_count(2); + for seed in 1..=2 - joining_count { + seed_active(&mut state, seed); + } + for seed in 3 - joining_count..=2 { + process(&mut state, &[deposit_entry(seed, MIN, seed)]); + } + process( + &mut state, + &[deposit_entry(3, MIN - 1, 3), param_entry(0x0a, 2)], + ); + assert_eq!(state.prospective_max_validator_count(), 2); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_eq!(status(&state, 3), ValidatorStatus::Inactive); + state.apply_protocol_parameter_changes().unwrap(); + assert_eq!(state.get_max_validator_count(), 2); + process( + &mut state, + &[ + param_entry(0x07, 1), + param_entry(0x0a, 1), + param_entry(0x06, 8), + ], + ); + assert_eq!(state.prospective_max_validator_count(), 2); + assert_tree_consistent(&state); + state.apply_protocol_parameter_changes().unwrap(); + assert_eq!(state.get_max_validator_count(), 2); + assert_eq!(state.get_minimum_validator_count(), 1); + assert_eq!(state.get_observers_per_validator(), 8); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_tree_consistent(&state); + } +} + +#[test] +fn last_valid_cap_decides_reduction_without_falling_back_to_earlier_update() { + for last_cap in [1, 2] { + let mut state = state(3); + seed_active(&mut state, 1); + seed_active(&mut state, 2); + process( + &mut state, + &[ + param_entry(0x0a, if last_cap == 1 { 2 } else { 1 }), + param_entry(0x0a, last_cap), + deposit_entry(3, MIN, 0), + ], + ); + let accepted_cap = if last_cap == 1 { 3 } else { 2 }; + assert_eq!(state.prospective_max_validator_count(), accepted_cap); + assert_eq!( + status(&state, 3), + if last_cap == 1 { + ValidatorStatus::Joining + } else { + ValidatorStatus::Inactive + } + ); + advance_epoch(&mut state); + assert_eq!(state.get_max_validator_count(), accepted_cap); + } +} + +#[test] +fn delayed_exit_payout_topup_and_redeposit_do_not_reclaim_replacement_slot() { + let mut state = state(2); + state.set_max_withdrawals_per_epoch(1); + seed_active(&mut state, 1); // A: exits. + seed_active(&mut state, 2); // Remains active and supplies the earlier payout. + process( + &mut state, + &[ + withdrawal_entry(2, 1, true), + withdrawal_entry(1, 0, true), + deposit_entry(3, MIN, 0), // B: replacement reserves A's slot. + ], + ); + assert_eq!(status(&state, 1), ValidatorStatus::SubmittedExitRequest); + assert_eq!(status(&state, 3), ValidatorStatus::Joining); + advance_epoch(&mut state); + assert_eq!(status(&state, 1), ValidatorStatus::FullPayoutPending); + advance_epoch(&mut state); + assert_eq!(status(&state, 3), ValidatorStatus::Active); + + let first = state.emit_withdrawal_payouts(PAYOUT_DELAY); + assert_eq!(first.len(), 1); + assert_eq!(first[0].amount, 1); + state.apply_withdrawal_payouts(PAYOUT_DELAY, &first); + assert_eq!(state.get_account(&account_key(2)).unwrap().balance, MIN + 9); + assert_eq!( + state.get_account(&account_key(1)).unwrap().balance, + MIN + 10 + ); + assert_eq!(state.get_withdrawal_count_for_epoch(PAYOUT_DELAY), 1); + advance_epoch(&mut state); + + // A's payout was deferred by the payout cap; next epoch's deposit is still + // only a top-up to that exit balance, not a new admission. + process(&mut state, &[deposit_entry(1, 5, 1)]); + assert_eq!(status(&state, 1), ValidatorStatus::FullPayoutPending); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert!(!state.has_added_validators(state.get_epoch() + WARM_UP)); + let second = state.emit_withdrawal_payouts(state.get_epoch()); + assert_eq!(second.len(), 1); + assert_eq!(second[0].amount, MIN + 15); + state.apply_withdrawal_payouts(state.get_epoch(), &second); + assert!(state.get_account(&account_key(1)).is_none()); + assert_eq!(state.get_withdrawal_count_for_epoch(PAYOUT_DELAY), 0); + advance_epoch(&mut state); + + process(&mut state, &[deposit_entry(1, MIN, 2)]); + assert_eq!(status(&state, 1), ValidatorStatus::Inactive); + assert_eq!(state.get_account(&account_key(1)).unwrap().balance, MIN); + assert_eq!(status(&state, 3), ValidatorStatus::Active); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert!(state.emit_withdrawal_payouts(state.get_epoch()).is_empty()); + assert_tree_consistent(&state); +} + +#[test] +fn rejected_deposits_do_not_consume_final_slot() { + for key_mismatch in [false, true] { + let mut state = state(2); + seed_active(&mut state, 1); + let mut rejected = if key_mismatch { + make_signed_deposit( + &ed25519::PrivateKey::from_seed(2), + &bls12381::PrivateKey::from_seed(1), + eth1_credentials(1), + MIN, + 0, + domain(), + ) + } else { + deposit(2, MIN, 0) + }; + if !key_mismatch { + rejected.node_signature[0] ^= 1; + } + let mut bytes = vec![0x00]; + rejected.write(&mut bytes); + process(&mut state, &[bytes.into(), deposit_entry(3, MIN, 1)]); + assert!(state.get_account(&account_key(2)).is_none()); + assert_eq!(status(&state, 3), ValidatorStatus::Joining); + assert_eq!(state.active_or_joining_validator_count(), 2); + assert_eq!(state.get_added_validators(WARM_UP).unwrap().len(), 1); + let refunds = state.get_withdrawals_for_epoch(PAYOUT_DELAY); + assert!(!refunds.is_empty()); + assert_eq!(refunds.iter().map(|w| w.inner.amount).sum::(), MIN); + assert_tree_consistent(&state); + } +} + +#[test] +fn repeated_joining_topups_at_capacity_preserve_single_original_reservation() { + let mut state = state(2); + seed_active(&mut state, 1); + process(&mut state, &[deposit_entry(2, MIN, 0)]); + advance_epoch(&mut state); + process( + &mut state, + &[ + deposit_entry(2, 1, 1), + deposit_entry(2, 2, 2), + deposit_entry(3, MIN, 3), + ], + ); + assert_eq!(state.get_account(&account_key(2)).unwrap().balance, MIN + 3); + assert_eq!( + state.get_account(&account_key(2)).unwrap().joining_epoch, + WARM_UP + ); + assert_eq!(state.get_added_validators(WARM_UP).unwrap().len(), 1); + assert!(!state.has_added_validators(1 + WARM_UP)); + assert_eq!(status(&state, 3), ValidatorStatus::Inactive); + advance_epoch(&mut state); + assert_eq!(status(&state, 2), ValidatorStatus::Active); + assert_eq!(state.active_or_joining_validator_count(), 2); +} + +#[test] +fn invalid_cap_update_does_not_override_last_valid_pair() { + let mut malformed = vec![0xff]; + ProtocolParamRequest { + param_id: 0x0a, + param: vec![2; 7], + } + .write(&mut malformed); + for invalid in [ + param_entry(0x0a, 0), + param_entry(0x0a, 4097), + malformed.into(), + ] { + let mut state = state(1); + seed_active(&mut state, 1); + process( + &mut state, + &[ + deposit_entry(2, MIN, 0), + param_entry(0x07, 2), + param_entry(0x0a, 2), + invalid, + ], + ); + assert_eq!(status(&state, 2), ValidatorStatus::Joining); + advance_epoch(&mut state); + assert_eq!(state.get_minimum_validator_count(), 2); + assert_eq!(state.get_max_validator_count(), 2); + } +} diff --git a/types/src/consensus_state_query.rs b/types/src/consensus_state_query.rs index 11853714..d2e77273 100644 --- a/types/src/consensus_state_query.rs +++ b/types/src/consensus_state_query.rs @@ -26,6 +26,7 @@ pub enum ConsensusStateRequest { GetMaxDepositsPerEpoch, GetMaxWithdrawalsPerEpoch, GetObserversPerValidator, + GetMaxValidatorCount, GetMinimumValidatorCount, GetInvalidDepositTax, GetEpochBounds(u64), @@ -58,6 +59,7 @@ pub enum ConsensusStateResponse { MaxDepositsPerEpoch(u64), MaxWithdrawalsPerEpoch(u64), ObserversPerValidator(u32), + MaxValidatorCount(u64), MinimumValidatorCount(u64), InvalidDepositTax(u64), EpochBounds(Option<(u64, u64)>), @@ -311,6 +313,20 @@ impl ConsensusStateQuery { value } + pub async fn get_max_validator_count(&self) -> u64 { + let (tx, rx) = oneshot::channel(); + let req = ConsensusStateRequest::GetMaxValidatorCount; + let _ = self.sender.clone().send((req, tx)).await; + + let res = rx + .await + .expect("consensus state query response sender dropped"); + let ConsensusStateResponse::MaxValidatorCount(value) = res else { + unreachable!("request and response variants must match"); + }; + value + } + pub async fn get_minimum_validator_count(&self) -> u64 { let (tx, rx) = oneshot::channel(); let req = ConsensusStateRequest::GetMinimumValidatorCount; diff --git a/types/src/execution_request.rs b/types/src/execution_request.rs index e6dec97c..a13bb165 100644 --- a/types/src/execution_request.rs +++ b/types/src/execution_request.rs @@ -283,7 +283,7 @@ impl DepositRequest { left.extend_from_slice(&self.consensus_pubkey.encode()); let mut hasher = Sha256::default(); hasher.update(&left); - let pubkeys_hash = hasher.finalize(); + let pubkeys_hash = hasher.finalize().1; // Hash pubkeys_hash with withdrawal_credentials let mut left = Vec::with_capacity(64); @@ -291,7 +291,7 @@ impl DepositRequest { left.extend_from_slice(&self.withdrawal_credentials); let mut hasher = Sha256::default(); hasher.update(&left); - let left_hash = hasher.finalize(); + let left_hash = hasher.finalize().1; // Hash amount with padding let mut right = Vec::with_capacity(64); @@ -299,19 +299,19 @@ impl DepositRequest { right.extend_from_slice(&[0; 56]); let mut hasher = Sha256::default(); hasher.update(&right); - let right_hash = hasher.finalize(); + let right_hash = hasher.finalize().1; // Combine left and right let mut hasher = Sha256::default(); hasher.update(&left_hash); hasher.update(&right_hash); - let root_hash = hasher.finalize(); + let root_hash = hasher.finalize().1; // Final hash with domain let mut hasher = Sha256::default(); hasher.update(&root_hash); hasher.update(&domain); - hasher.finalize() + hasher.finalize().1 } } @@ -581,42 +581,21 @@ pub fn compute_deposit_data_root( */ // 1. consensus_pubkey_hash = sha256(consensus_pubkey || bytes16(0)) - let mut hasher = Sha256::new(); - hasher.update(consensus_pubkey); - hasher.update(&[0u8; 16]); // bytes16(0) - let consensus_pubkey_hash = hasher.finalize(); + let consensus_pubkey_hash = Sha256::hash(&[consensus_pubkey, &[0u8; 16]]); // 2. pubkey_root = sha256(node_pubkey || consensus_pubkey_hash) - let mut hasher = Sha256::new(); - hasher.update(node_pubkey); - hasher.update(&consensus_pubkey_hash); - let pubkey_root = hasher.finalize(); + let pubkey_root = Sha256::hash(&[node_pubkey, &consensus_pubkey_hash]); // 3. node_signature_hash = sha256(node_signature) - let mut hasher = Sha256::new(); - hasher.update(node_signature); - let node_signature_hash = hasher.finalize(); + let node_signature_hash = Sha256::hash(&[node_signature]); // 4. consensus_signature_hash = sha256(sha256(consensus_signature[0:64]) || sha256(consensus_signature[64:96] || bytes32(0))) - let mut hasher = Sha256::new(); - hasher.update(&consensus_signature[0..64]); - let consensus_sig_part1 = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&consensus_signature[64..96]); - hasher.update(&[0u8; 32]); // bytes32(0) - let consensus_sig_part2 = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&consensus_sig_part1); - hasher.update(&consensus_sig_part2); - let consensus_signature_hash = hasher.finalize(); + let consensus_sig_part1 = Sha256::hash(&[&consensus_signature[0..64]]); + let consensus_sig_part2 = Sha256::hash(&[&consensus_signature[64..96], &[0u8; 32]]); + let consensus_signature_hash = Sha256::hash(&[&consensus_sig_part1, &consensus_sig_part2]); // 5. signature_root = sha256(node_signature_hash || consensus_signature_hash) - let mut hasher = Sha256::new(); - hasher.update(&node_signature_hash); - hasher.update(&consensus_signature_hash); - let signature_root = hasher.finalize(); + let signature_root = Sha256::hash(&[&node_signature_hash, &consensus_signature_hash]); // 3. Convert amount to 8-byte little-endian (gwei) let amount_gwei = amount / U256::from(10).pow(U256::from(9)); // Convert wei to gwei @@ -624,21 +603,9 @@ pub fn compute_deposit_data_root( let amount_bytes = amount_u64.to_le_bytes(); // 8 bytes little-endian // 4. node = sha256(sha256(pubkey_root || withdrawal_credentials) || sha256(amount || bytes24(0) || signature_root)) - let mut hasher = Sha256::new(); - hasher.update(&pubkey_root); - hasher.update(withdrawal_credentials); - let left_node = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&amount_bytes); - hasher.update(&[0u8; 24]); // bytes24(0) - hasher.update(&signature_root); - let right_node = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(&left_node); - hasher.update(&right_node); - let deposit_data_root = hasher.finalize(); + let left_node = Sha256::hash(&[&pubkey_root, withdrawal_credentials]); + let right_node = Sha256::hash(&[&amount_bytes, &[0u8; 24], &signature_root]); + let deposit_data_root = Sha256::hash(&[&left_node, &right_node]); let digest_bytes: &[u8] = deposit_data_root.as_ref(); digest_bytes diff --git a/types/src/genesis.rs b/types/src/genesis.rs index 6f8dc4a9..a53536b7 100644 --- a/types/src/genesis.rs +++ b/types/src/genesis.rs @@ -1,7 +1,8 @@ use crate::PublicKey; use crate::protocol_params::{ - DEFAULT_MINIMUM_VALIDATOR_COUNT, MAX_INVALID_DEPOSIT_TAX, MAX_MESSAGE_SIZE_BYTES_MAX, - MAX_MESSAGE_SIZE_BYTES_MIN, MIN_MINIMUM_VALIDATOR_COUNT, ProtocolParam, + DEFAULT_MAX_VALIDATOR_COUNT, DEFAULT_MINIMUM_VALIDATOR_COUNT, DEFAULT_OBSERVERS_PER_VALIDATOR, + MAX_INVALID_DEPOSIT_TAX, MAX_MESSAGE_SIZE_BYTES_MAX, MAX_MESSAGE_SIZE_BYTES_MIN, + MIN_MINIMUM_VALIDATOR_COUNT, ProtocolParam, }; use alloy_primitives::Address; use anyhow::Context; @@ -71,6 +72,10 @@ pub struct Genesis { /// execution request. #[serde(default = "default_observers_per_validator")] pub observers_per_validator: u32, + /// Maximum number of validators that may be active or joining. Valid deposits + /// received while the cap is full are credited, but the validator remains inactive. + #[serde(default = "default_max_validator_count")] + pub max_validator_count: u64, /// Minimum number of active validators that full exits must preserve. #[serde(default = "default_minimum_validator_count")] pub minimum_validator_count: u64, @@ -99,7 +104,11 @@ fn default_max_withdrawals_per_epoch() -> u64 { } fn default_observers_per_validator() -> u32 { - 5 + DEFAULT_OBSERVERS_PER_VALIDATOR +} + +fn default_max_validator_count() -> u64 { + DEFAULT_MAX_VALIDATOR_COUNT } fn default_minimum_validator_count() -> u64 { @@ -217,10 +226,7 @@ impl Genesis { /// explicitly `#[ssz(skip_serializing)]`'d). Per-validator `ip_address` is /// skipped: it is network topology, not consensus identity. pub fn config_digest(&self) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(GENESIS_CONFIG_DOMAIN_TAG); - hasher.update(&self.as_ssz_bytes()); - hasher.finalize().0 + Sha256::hash(&[GENESIS_CONFIG_DOMAIN_TAG, &self.as_ssz_bytes()]).0 } pub fn load_from_file(path: &str) -> Result> { @@ -279,9 +285,9 @@ impl Genesis { if self.skip_timeout_views == 0 { return Err("skip_timeout_views must be greater than 0".into()); } - if self.leader_timeout_ms > self.notarization_timeout_ms { + if self.leader_timeout_ms >= self.notarization_timeout_ms { return Err( - "leader_timeout_ms must be less than or equal to notarization_timeout_ms".into(), + "leader_timeout_ms must be strictly less than notarization_timeout_ms".into(), ); } if self.skip_timeout_views > self.activity_timeout_views { @@ -297,6 +303,15 @@ impl Genesis { ProtocolParam::MaxDepositsPerEpoch(self.max_deposits_per_epoch).validate()?; ProtocolParam::MaxWithdrawalsPerEpoch(self.max_withdrawals_per_epoch).validate()?; ProtocolParam::ObserversPerValidator(u64::from(self.observers_per_validator)).validate()?; + ProtocolParam::MaxValidatorCount(self.max_validator_count).validate()?; + if self.validators.len() as u64 > self.max_validator_count { + return Err(format!( + "genesis validator count {} exceeds max_validator_count {}", + self.validators.len(), + self.max_validator_count + ) + .into()); + } ProtocolParam::MaxPendingWithdrawalsPerValidator( self.max_pending_withdrawals_per_validator, ) @@ -310,6 +325,13 @@ impl Genesis { ) .into()); } + if self.minimum_validator_count > self.max_validator_count { + return Err(format!( + "minimum_validator_count {} exceeds max_validator_count {}", + self.minimum_validator_count, self.max_validator_count + ) + .into()); + } if self.invalid_deposit_tax > MAX_INVALID_DEPOSIT_TAX { return Err(format!( "invalid_deposit_tax {} exceeds maximum {}", @@ -371,9 +393,11 @@ impl Genesis { mod tests { use super::*; use crate::protocol_params::{ - MAX_EPOCH_LENGTH, MAX_MAX_DEPOSITS_PER_EPOCH, MAX_OBSERVERS_PER_VALIDATOR, + DEFAULT_MAX_VALIDATOR_COUNT, MAX_EPOCH_LENGTH, MAX_MAX_DEPOSITS_PER_EPOCH, + MAX_MAX_VALIDATOR_COUNT, MAX_OBSERVERS_PER_VALIDATOR, MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MAX, MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MIN, MAX_WITHDRAWALS_PER_EPOCH_MAX, MAX_WITHDRAWALS_PER_EPOCH_MIN, MIN_EPOCH_LENGTH, + MIN_MAX_VALIDATOR_COUNT, }; #[test] @@ -381,6 +405,8 @@ mod tests { let genesis = Genesis::load_from_file("../example_genesis.toml").unwrap(); assert_eq!(genesis.validator_count(), 4); assert_eq!(genesis.blocks_per_epoch, 10000); + assert_eq!(genesis.observers_per_validator, 16); + assert_eq!(genesis.max_validator_count, DEFAULT_MAX_VALIDATOR_COUNT); assert_eq!( genesis.minimum_validator_count, DEFAULT_MINIMUM_VALIDATOR_COUNT @@ -542,7 +568,9 @@ mod tests { #[test] fn rejects_misordered_leader_and_notarization_timeouts() { let mut genesis = Genesis::load_from_file("../example_genesis.toml").unwrap(); - genesis.leader_timeout_ms = genesis.notarization_timeout_ms + 1; + genesis.leader_timeout_ms = genesis.notarization_timeout_ms; + assert!(genesis.validate().is_err()); + genesis.leader_timeout_ms += 1; assert!(genesis.validate().is_err()); } @@ -553,6 +581,28 @@ mod tests { assert!(genesis.validate().is_err()); } + #[test] + fn accepts_max_validator_count_at_bounds() { + let mut genesis = Genesis::load_from_file("../example_genesis.toml").unwrap(); + genesis.max_validator_count = MIN_MAX_VALIDATOR_COUNT.max(genesis.validators.len() as u64); + assert!(genesis.validate().is_ok()); + genesis.max_validator_count = MAX_MAX_VALIDATOR_COUNT; + assert!(genesis.validate().is_ok()); + } + + #[test] + fn rejects_max_validator_count_outside_bounds_or_below_genesis_set() { + let mut genesis = Genesis::load_from_file("../example_genesis.toml").unwrap(); + genesis.max_validator_count = 0; + assert!(genesis.validate().is_err()); + + genesis.max_validator_count = MAX_MAX_VALIDATOR_COUNT + 1; + assert!(genesis.validate().is_err()); + + genesis.max_validator_count = genesis.validators.len() as u64 - 1; + assert!(genesis.validate().is_err()); + } + #[test] fn accepts_observers_per_validator_at_upper_bound() { let mut genesis = Genesis::load_from_file("../example_genesis.toml").unwrap(); @@ -648,6 +698,10 @@ mod tests { "observers_per_validator", Box::new(|g| g.observers_per_validator += 1), ), + ( + "max_validator_count", + Box::new(|g| g.max_validator_count += 1), + ), ( "minimum_validator_count", Box::new(|g| g.minimum_validator_count += 1), diff --git a/types/src/header.rs b/types/src/header.rs index e020c7cc..4f88db33 100644 --- a/types/src/header.rs +++ b/types/src/header.rs @@ -182,9 +182,7 @@ impl Header { /// constructed with a mismatched seed via [`Header::new_with_digest`]. pub fn computed_digest(&self) -> Digest { let bytes = self.encode(); - let mut hasher = Sha256::new(); - hasher.update(&bytes); - hasher.finalize() + Sha256::hash(&[&bytes]) } } @@ -855,7 +853,7 @@ mod test { let finalized = Finalization { proposal, certificate: Certificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -911,7 +909,7 @@ mod test { let finalized = Finalization { proposal, certificate: Certificate:: { - signers: Signers::from(3, [0, 1, 2].map(Participant::new)), + signers: Signers::new(3, [0, 1, 2].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -976,7 +974,7 @@ mod test { let wrong_finalized = Finalization { proposal: wrong_proposal, certificate: Certificate:: { - signers: Signers::from(5, [0, 2, 4].map(Participant::new)), + signers: Signers::new(5, [0, 2, 4].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; @@ -1026,7 +1024,7 @@ mod test { let finalized = Finalization { proposal, certificate: Certificate:: { - signers: Signers::from(4, [0, 1, 2, 3].map(Participant::new)), + signers: Signers::new(4, [0, 1, 2, 3].map(Participant::new)).unwrap(), signature: create_dummy_signature().into(), }, }; diff --git a/types/src/lib.rs b/types/src/lib.rs index 7fe13d72..e9e1e886 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -63,7 +63,7 @@ pub fn chain_domain(config_digest: [u8; 32]) -> [u8; 32] { domain_data.extend_from_slice(CHAIN_DOMAIN_TAG); domain_data.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes()); domain_data.extend_from_slice(&config_digest); - Sha256::hash(&domain_data).0 + Sha256::hash(&[&domain_data]).0 } /// Folds a purpose tag, protocol version, EL genesis hash, and (length-prefixed) @@ -78,7 +78,7 @@ fn signature_domain(tag: &[u8], genesis_hash: [u8; 32], namespace: &[u8]) -> Dig // Length-prefix the variable-length namespace so the domain is unambiguous. domain_data.extend_from_slice(&(namespace.len() as u32).to_le_bytes()); domain_data.extend_from_slice(namespace); - Sha256::hash(&domain_data) + Sha256::hash(&[&domain_data]) } /// Domain for deposit-authorization signatures, bound to the full Summit diff --git a/types/src/network_oracle.rs b/types/src/network_oracle.rs index 63deba75..041abd01 100644 --- a/types/src/network_oracle.rs +++ b/types/src/network_oracle.rs @@ -18,16 +18,41 @@ pub trait NetworkOracle: Send + Sync + 'static { #[derive(Clone, Debug)] pub struct DiscoveryOracle { oracle: Oracle, + local: C, + capacity: std::num::NonZeroUsize, } impl DiscoveryOracle { - pub fn new(oracle: Oracle) -> Self { - Self { oracle } + pub fn new(oracle: Oracle, local: C, capacity: std::num::NonZeroUsize) -> Self { + Self { + oracle, + local, + capacity, + } + } + + fn check_capacity<'a>( + local: &'a C, + capacity: std::num::NonZeroUsize, + primary: impl Iterator, + secondary: impl Iterator, + ) { + let identities: std::collections::BTreeSet<_> = primary + .chain(secondary) + .chain(std::iter::once(local)) + .collect(); + assert!( + identities.len() <= capacity.get(), + "peer set requires {} identities but startup capacity is {}; capacity-exceeding updates are unsupported in this deployment", + identities.len(), + capacity + ); } } impl NetworkOracle for DiscoveryOracle { async fn track(&mut self, index: u64, primary: Vec, secondary: Vec) { + Self::check_capacity(&self.local, self.capacity, primary.iter(), secondary.iter()); let primary = OrderedSet::from_iter_dedup(primary); let secondary = OrderedSet::from_iter_dedup(secondary); let _ = self @@ -42,6 +67,10 @@ impl Blocker for DiscoveryOracle { fn block(&mut self, public_key: Self::PublicKey) -> Feedback { self.oracle.block(public_key) } + + fn blocked(&mut self) -> commonware_p2p::BlockedSubscription { + self.oracle.blocked() + } } impl Provider for DiscoveryOracle { @@ -61,6 +90,54 @@ impl Manager for DiscoveryOracle { where R: Into> + Send, { + let peers = peers.into(); + Self::check_capacity( + &self.local, + self.capacity, + peers.primary.iter(), + peers.secondary.iter(), + ); self.oracle.track(id, peers) } } + +#[cfg(test)] +mod tests { + use super::*; + use commonware_cryptography::{Signer as _, ed25519}; + use commonware_utils::NZUsize; + + fn keys() -> [ed25519::PublicKey; 3] { + std::array::from_fn(|seed| ed25519::PrivateKey::from_seed(seed as u64).public_key()) + } + + #[test] + fn capacity_deduplicates_primary_secondary_and_local_identity() { + let [local, primary, observer] = keys(); + DiscoveryOracle::check_capacity( + &local, + NZUsize!(3), + [&local, &primary].into_iter(), + [&primary, &observer].into_iter(), + ); + // The local node can be outside both sets (e.g. a joining validator). + DiscoveryOracle::check_capacity( + &local, + NZUsize!(3), + [&primary].into_iter(), + [&observer].into_iter(), + ); + } + + #[test] + #[should_panic(expected = "peer set requires 3 identities but startup capacity is 2")] + fn capacity_counts_offline_authorized_observers_and_unlisted_local_identity() { + let [local, primary, offline_observer] = keys(); + DiscoveryOracle::check_capacity( + &local, + NZUsize!(2), + [&primary].into_iter(), + [&offline_observer].into_iter(), + ); + } +} diff --git a/types/src/protocol_params.rs b/types/src/protocol_params.rs index 21ec15b2..c93abe1a 100644 --- a/types/src/protocol_params.rs +++ b/types/src/protocol_params.rs @@ -14,6 +14,7 @@ pub const MAX_WITHDRAWALS_PER_EPOCH_MIN: u64 = 1; pub const MAX_WITHDRAWALS_PER_EPOCH_MAX: u64 = 256; pub const MIN_OBSERVERS_PER_VALIDATOR: u64 = 0; pub const MAX_OBSERVERS_PER_VALIDATOR: u64 = 256; +pub const DEFAULT_OBSERVERS_PER_VALIDATOR: u32 = 16; pub const MIN_MINIMUM_VALIDATOR_COUNT: u64 = 1; pub const DEFAULT_MINIMUM_VALIDATOR_COUNT: u64 = 3; // Bounds on the genesis `max_message_size_bytes`. The floor must be large enough @@ -32,6 +33,9 @@ pub const MAX_INVALID_DEPOSIT_TAX: u64 = 100; pub const MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MIN: u64 = 1; pub const MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MAX: u64 = 256; pub const DEFAULT_MAX_PENDING_WITHDRAWALS_PER_VALIDATOR: u64 = 3; +pub const MIN_MAX_VALIDATOR_COUNT: u64 = 1; +pub const MAX_MAX_VALIDATOR_COUNT: u64 = 4_096; +pub const DEFAULT_MAX_VALIDATOR_COUNT: u64 = 128; #[derive(Clone, Debug)] pub enum ProtocolParam { @@ -45,6 +49,7 @@ pub enum ProtocolParam { MinimumValidatorCount(u64), InvalidDepositTax(u64), MaxPendingWithdrawalsPerValidator(u64), + MaxValidatorCount(u64), } /// A protocol-parameter value that fell outside its allowed bounds. @@ -64,6 +69,7 @@ pub enum ParamBoundsError { MaxWithdrawalsPerEpoch(u64), ObserversPerValidator(u64), MaxPendingWithdrawalsPerValidator(u64), + MaxValidatorCount(u64), } impl ParamBoundsError { @@ -79,6 +85,7 @@ impl ParamBoundsError { Self::MaxPendingWithdrawalsPerValidator(_) => { "max pending withdrawals per validator out of bounds" } + Self::MaxValidatorCount(_) => "max validator count out of bounds", } } } @@ -110,6 +117,10 @@ impl std::fmt::Display for ParamBoundsError { f, "max pending withdrawals per validator {v} must be between {MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MIN} and {MAX_PENDING_WITHDRAWALS_PER_VALIDATOR_MAX}" ), + Self::MaxValidatorCount(v) => write!( + f, + "max validator count {v} must be between {MIN_MAX_VALIDATOR_COUNT} and {MAX_MAX_VALIDATOR_COUNT}" + ), } } } @@ -156,6 +167,11 @@ impl ProtocolParam { { Err(ParamBoundsError::MaxPendingWithdrawalsPerValidator(v)) } + ProtocolParam::MaxValidatorCount(v) + if !(MIN_MAX_VALIDATOR_COUNT..=MAX_MAX_VALIDATOR_COUNT).contains(&v) => + { + Err(ParamBoundsError::MaxValidatorCount(v)) + } _ => Ok(()), } } @@ -295,6 +311,18 @@ impl TryFrom for ProtocolParam { param.validate().map_err(|e| anyhow!("{e}"))?; Ok(param) } + 0x0A => { + if request.param.len() != 8 { + return Err(anyhow!( + "Failed to parse max validator count protocol param, invalid length {}", + request.param.len() + )); + } + let bytes: [u8; 8] = request.param.as_slice().try_into()?; + let param = ProtocolParam::MaxValidatorCount(u64::from_le_bytes(bytes)); + param.validate().map_err(|e| anyhow!("{e}"))?; + Ok(param) + } _ => Err(anyhow!( "Failed to parse protocol param request - unknown param_id: {request:?}" )), @@ -313,7 +341,8 @@ impl EncodeSize for ProtocolParam { | ProtocolParam::ObserversPerValidator(_) | ProtocolParam::MinimumValidatorCount(_) | ProtocolParam::InvalidDepositTax(_) - | ProtocolParam::MaxPendingWithdrawalsPerValidator(_) => 1 + 8, // 1 byte tag + 8 byte value + | ProtocolParam::MaxPendingWithdrawalsPerValidator(_) + | ProtocolParam::MaxValidatorCount(_) => 1 + 8, // 1 byte tag + 8 byte value ProtocolParam::TreasuryAddress(_) => 1 + 20, // 1 byte tag + 20 byte address } } @@ -362,6 +391,10 @@ impl Write for ProtocolParam { buf.put_u8(0x09); buf.put_u64(*value); } + ProtocolParam::MaxValidatorCount(value) => { + buf.put_u8(0x0A); + buf.put_u64(*value); + } } } } @@ -450,6 +483,14 @@ impl Read for ProtocolParam { .map_err(|e| Error::Invalid("ProtocolParam", e.reason()))?; Ok(param) } + 0x0A => { + let value = buf.try_get_u64().map_err(|_| Error::EndOfBuffer)?; + let param = ProtocolParam::MaxValidatorCount(value); + param + .validate() + .map_err(|e| Error::Invalid("ProtocolParam", e.reason()))?; + Ok(param) + } _ => Err(Error::Invalid("ProtocolParam", "unknown tag")), } } @@ -968,6 +1009,58 @@ mod tests { } } + #[test] + fn test_max_validator_count_entry_points_and_codec() { + for valid in [ + MIN_MAX_VALIDATOR_COUNT, + DEFAULT_MAX_VALIDATOR_COUNT, + MAX_MAX_VALIDATOR_COUNT, + ] { + let param = ProtocolParam::MaxValidatorCount(valid); + assert!(param.validate().is_ok()); + + let request = ProtocolParamRequest { + param_id: 0x0A, + param: valid.to_le_bytes().to_vec(), + }; + assert!(matches!( + ProtocolParam::try_from(request).unwrap(), + ProtocolParam::MaxValidatorCount(value) if value == valid + )); + + let mut buf = BytesMut::new(); + param.write(&mut buf); + assert_eq!(buf.len(), param.encode_size()); + assert_eq!(buf[0], 0x0A); + assert!(matches!( + ProtocolParam::read(&mut buf.as_ref()).unwrap(), + ProtocolParam::MaxValidatorCount(value) if value == valid + )); + } + } + + #[test] + fn test_max_validator_count_rejects_out_of_bounds_values() { + for invalid in [0, MAX_MAX_VALIDATOR_COUNT + 1, u64::MAX] { + assert!( + ProtocolParam::MaxValidatorCount(invalid) + .validate() + .is_err() + ); + + let request = ProtocolParamRequest { + param_id: 0x0A, + param: invalid.to_le_bytes().to_vec(), + }; + assert!(ProtocolParam::try_from(request).is_err()); + + let mut buf = BytesMut::new(); + buf.put_u8(0x0A); + buf.put_u64(invalid); + assert!(ProtocolParam::read(&mut buf.as_ref()).is_err()); + } + } + #[test] fn test_decode_truncated_input_returns_err() { // Empty buffer — must not panic. @@ -978,7 +1071,7 @@ mod tests { )); // Tag only, no payload. - for tag in 0x00u8..=0x09 { + for tag in 0x00u8..=0x0A { let mut buf = BytesMut::new(); buf.put_u8(tag); assert!( diff --git a/types/src/ssz_hash.rs b/types/src/ssz_hash.rs index cd76f083..f0266583 100644 --- a/types/src/ssz_hash.rs +++ b/types/src/ssz_hash.rs @@ -114,6 +114,7 @@ impl SszHashTreeRoot for ProtocolParam { ProtocolParam::MinimumValidatorCount(v) => (7u64, v.hash_tree_root()), ProtocolParam::InvalidDepositTax(v) => (8u64, v.hash_tree_root()), ProtocolParam::MaxPendingWithdrawalsPerValidator(v) => (9u64, v.hash_tree_root()), + ProtocolParam::MaxValidatorCount(v) => (10u64, v.hash_tree_root()), }; merkleize(&[tag.hash_tree_root(), value_hash]) } diff --git a/types/src/ssz_state_tree.rs b/types/src/ssz_state_tree.rs index 5b15647b..88d4b430 100644 --- a/types/src/ssz_state_tree.rs +++ b/types/src/ssz_state_tree.rs @@ -1,6 +1,6 @@ //! Two-level SSZ binary Merkle tree for ConsensusState. //! -//! The top-level tree has 32 leaf slots (28 used, depth 5). Scalar fields and +//! The top-level tree has 32 leaf slots (29 used, depth 5). Scalar fields and //! collection roots are assigned to fixed leaf indices — see the field-index //! and `*_ROOT` constants below for the authoritative layout. Each collection //! root (validator accounts, deposit/withdrawal queues, protocol-param changes, @@ -61,9 +61,10 @@ pub const MINIMUM_VALIDATOR_COUNT: usize = 24; pub const PENDING_ACTIVE_VALIDATOR_EXITS: usize = 25; pub const INVALID_DEPOSIT_TAX: usize = 26; pub const MAX_PENDING_WITHDRAWALS_PER_VALIDATOR: usize = 27; +pub const MAX_VALIDATOR_COUNT: usize = 28; /// Number of used leaf slots in the top-level tree. -pub const NUM_TOP_LEAVES: usize = 28; +pub const NUM_TOP_LEAVES: usize = 29; // --- Validator field indices (within each validator's 8-leaf subtree) --- @@ -133,7 +134,7 @@ pub const ADDED_VALIDATOR_FIELDS_PER_ITEM: usize = 4; /// Two-level SSZ state tree mirroring ConsensusState. #[derive(Clone, Debug)] pub struct SszStateTree { - /// Top-level tree: 32 leaves (depth 5), 28 used. + /// Top-level tree: 32 leaves (depth 5), 29 used. top: SszTree, /// Validator accounts subtree. Rebuilt from BTreeMap on every mutation. @@ -288,6 +289,11 @@ impl SszStateTree { ); } + pub fn set_max_validator_count(&mut self, value: u64) { + self.top + .set_leaf(MAX_VALIDATOR_COUNT, value.hash_tree_root()); + } + pub fn set_treasury_address(&mut self, address: &Address) { self.top .set_leaf(TREASURY_ADDRESS, address.hash_tree_root()); @@ -718,6 +724,7 @@ impl SszStateTree { ProtocolParam::MinimumValidatorCount(v) => (7u64, v.hash_tree_root()), ProtocolParam::InvalidDepositTax(v) => (8u64, v.hash_tree_root()), ProtocolParam::MaxPendingWithdrawalsPerValidator(v) => (9u64, v.hash_tree_root()), + ProtocolParam::MaxValidatorCount(v) => (10u64, v.hash_tree_root()), }; tree.set_leaf(base + PROTOCOL_PARAM_FIELD_TAG, tag.hash_tree_root()); tree.set_leaf(base + PROTOCOL_PARAM_FIELD_VALUE, value_hash); @@ -859,6 +866,7 @@ impl SszStateTree { max_deposits_per_epoch: u64, max_withdrawals_per_epoch: u64, observers_per_validator: u32, + max_validator_count: u64, pending_execution_requests: &[alloy_primitives::Bytes], pending_checkpoint_digest: Option<[u8; 32]>, dynamic_epoch_schedule: &[u8], @@ -885,6 +893,7 @@ impl SszStateTree { self.set_max_deposits_per_epoch(max_deposits_per_epoch); self.set_max_withdrawals_per_epoch(max_withdrawals_per_epoch); self.set_observers_per_validator(observers_per_validator); + self.set_max_validator_count(max_validator_count); self.set_minimum_validator_count(minimum_validator_count); self.set_pending_active_validator_exits(pending_active_validator_exits); self.set_invalid_deposit_tax(invalid_deposit_tax); @@ -1625,6 +1634,10 @@ mod tests { tree.set_forkchoice_finalized_block_hash(&[5u8; 32]); assert_ne!(tree.root(), r10); + let r11 = tree.root(); + + tree.set_max_validator_count(256); + assert_ne!(tree.root(), r11); } #[test] @@ -1753,6 +1766,7 @@ mod tests { inc.set_max_deposits_per_epoch(3); inc.set_max_withdrawals_per_epoch(16); inc.set_observers_per_validator(5); + inc.set_max_validator_count(256); inc.set_minimum_validator_count(3); inc.set_pending_active_validator_exits(0); inc.set_invalid_deposit_tax(0); @@ -1791,6 +1805,7 @@ mod tests { 3, 16, 5, + 256, &[], None, &[], @@ -1905,6 +1920,7 @@ mod tests { 3, 16, 0, + 256, &[], None, &[], diff --git a/types/src/ssz_tree_key.rs b/types/src/ssz_tree_key.rs index 7a048e19..1f831d8b 100644 --- a/types/src/ssz_tree_key.rs +++ b/types/src/ssz_tree_key.rs @@ -83,6 +83,7 @@ pub fn parse_key(descriptor: &str) -> Result { "max_pending_withdrawals_per_validator" => Ok(SszStateKey::Scalar( ssz_state_tree::MAX_PENDING_WITHDRAWALS_PER_VALIDATOR, )), + "max_validator_count" => Ok(SszStateKey::Scalar(ssz_state_tree::MAX_VALIDATOR_COUNT)), _ => { if let Some(rest) = descriptor.strip_prefix("validator_field:") { // Format: "validator_field:0xPUBKEY:field_name" @@ -273,6 +274,10 @@ mod tests { parse_key("invalid_deposit_tax").unwrap(), SszStateKey::Scalar(ssz_state_tree::INVALID_DEPOSIT_TAX) ); + assert_eq!( + parse_key("max_validator_count").unwrap(), + SszStateKey::Scalar(ssz_state_tree::MAX_VALIDATOR_COUNT) + ); } #[test]