diff --git a/CLAUDE.md b/CLAUDE.md index 1a6c36ae..fb0adda4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,10 +4,11 @@ High-performance consensus client for EVM-based blockchains, built by [Seismic S ## Commonware -[Commonware](https://commonware.xyz) is Summit's core infrastructure layer. Summit depends on 12 Commonware crates (version pinned in root `Cargo.toml`). Nearly every component in Summit is built on top of Commonware primitives. +[Commonware](https://commonware.xyz) is Summit's core infrastructure layer. Summit depends on 14 Commonware crates (version pinned in root `Cargo.toml`). Nearly every component in Summit is built on top of Commonware primitives. | Crate | What it provides | | --- | --- | +| `commonware-actor` | Actor mailboxes with backpressure policies (`Policy`/`Overflow`), `Feedback` for sync sends | | `commonware-consensus` | Simplex BFT protocol — leader election, notarization (2/3+1), finalization | | `commonware-cryptography` | BLS12-381 multisig, Ed25519 identity, SHA256 hashing | | `commonware-runtime` | Async runtime abstractions — Clock, Spawner, Metrics, Signal/Stopper | @@ -15,6 +16,7 @@ High-performance consensus client for EVM-based blockchains, built by [Seismic S | `commonware-p2p` | Authenticated P2P connections, peer management, simulated network for tests | | `commonware-broadcast` | Buffered reliable broadcast between validators | | `commonware-codec` | Streaming encode/decode for blocks, checkpoints, and messages | +| `commonware-formatting` | Hex encoding/decoding (`hex`, `from_hex`) | | `commonware-resolver` | Request-response backfill for missing blocks from peers | | `commonware-utils` | Channels (mpsc, oneshot), ordered sets, byte utilities, non-zero types | | `commonware-macros` | `select!`/`select_loop!` async macros, `test_traced!` for instrumented tests | @@ -62,20 +64,20 @@ target/debug/summit --help ## Test -### Unit & integration tests (153 tests) +### Unit & integration tests ```bash -cargo test # default features — 153 tests -cargo test --all-features # includes e2e test harness — 170 tests +cargo test # default features +cargo test --all-features # includes e2e test harness ``` Test breakdown by crate: -- `summit` (node): 40 tests (syncer, checkpointing, execution requests, deposits, withdrawals) -- `summit-finalizer`: 19 tests (validator lifecycle, fork handling, state queries) -- `summit-syncer`: 11 tests -- `summit-types`: 75 tests (codec, consensus state, headers, withdrawals, protocol params) -- `summit-rpc`: 8 integration tests +- `summit` (node): syncer, checkpointing, execution requests, observer, and engine integration tests +- `summit-finalizer`: validator lifecycle, fork handling, and state query tests +- `summit-syncer`: delivery, recovery, durability, resolver, subscription, and forwarding tests +- `summit-types`: codec, consensus state, header, withdrawal, and protocol parameter tests +- `summit-rpc`: JSON-RPC integration tests ### CI checks (must pass before PR) diff --git a/Cargo.lock b/Cargo.lock index 08a9b4f0..f4ff9b68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -404,7 +404,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.12.22", "serde", "serde_json", "thiserror 2.0.12", @@ -472,7 +472,7 @@ dependencies = [ "alloy-transport-ipc", "futures", "pin-project", - "reqwest", + "reqwest 0.12.22", "serde", "serde_json", "tokio", @@ -568,7 +568,7 @@ dependencies = [ "async-trait", "auto_impl", "either", - "elliptic-curve", + "elliptic-curve 0.13.8", "k256", "thiserror 2.0.12", ] @@ -698,7 +698,7 @@ dependencies = [ "hyper", "hyper-tls", "hyper-util", - "reqwest", + "reqwest 0.12.22", "serde_json", "tower 0.5.2", "tracing", @@ -1028,9 +1028,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -1039,14 +1039,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -1156,6 +1157,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.21.7" @@ -1263,20 +1270,20 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -1386,7 +1393,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1396,7 +1414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1450,7 +1468,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -1521,6 +1539,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11ed919bd3bae4af5ab56372b627dfc32622aba6cec36906e8ab46746037c9d" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1537,43 +1561,69 @@ dependencies = [ "memchr", ] +[[package]] +name = "commonware-actor" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beac6262223b45c6843ab70b245b526b28836a8257b1759a509ba5fab24fa4a6" +dependencies = [ + "cfg-if", + "commonware-macros", + "commonware-runtime", + "crossbeam-queue", + "futures-util", + "parking_lot", +] + [[package]] name = "commonware-broadcast" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe7362c8942f20f0eab11756932b7d1c41f4cc99e142cb563e17a04b40095d5" +checksum = "3a2d7e56dc894ad3f55dbe5726468915492ae8e4650ad2a45c1bcfd138c2037f" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", "commonware-p2p", "commonware-runtime", "commonware-utils", - "prometheus-client 0.24.0", "thiserror 2.0.12", "tracing", ] [[package]] name = "commonware-codec" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f06e32817f35fb517ceb6102d984f9a85fde85666c96f053638e323b8597f2f7" +checksum = "e8eb7efe071cea13e8b23a0a7f584398870c401d2b2aabfb6b9ab3634ebcba3a" dependencies = [ "bytes", "cfg-if", + "commonware-codec-macros", "commonware-macros", "paste", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand_chacha 0.10.0", "thiserror 2.0.12", ] +[[package]] +name = "commonware-codec-macros" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a876bec347344381948901d0f5ec2e9443ebc8dbab99a63bc2db78c2fc2efbdd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "commonware-coding" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e60b2b324de47773c3d4af4d83bfc76d2c287ba7f2d6eb8c2aa5068f877b4bb" +checksum = "409e9997c1bb3273b9570c26ddef4f252327c9bf18968d4439a83cfd1d3e5661" dependencies = [ "bytes", "commonware-codec", @@ -1584,25 +1634,24 @@ dependencies = [ "commonware-storage", "commonware-utils", "num-rational", - "rand 0.8.5", - "rand_core 0.6.4", "rayon", - "reed-solomon-simd", "thiserror 2.0.12", ] [[package]] name = "commonware-consensus" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a67374d82c69e870105f010b895f1768952df5d0fa0d0550dedf162de16f44e" +checksum = "acf15e3402e26f062695d4dd5e4b5967f601dc7303d06b9fafdb20a6485a3a0a" dependencies = [ "bytes", "cfg-if", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-coding", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-p2p", @@ -1613,10 +1662,8 @@ dependencies = [ "commonware-utils", "futures", "pin-project", - "prometheus-client 0.24.0", - "rand 0.8.5", - "rand_core 0.6.4", - "rand_distr", + "rand 0.10.2", + "rand_core 0.10.1", "rayon", "thiserror 2.0.12", "tracing", @@ -1624,10 +1671,11 @@ dependencies = [ [[package]] name = "commonware-cryptography" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f09b55dd5510c3b7613a573606a41961c2788709ffde053d4e644bec0bff2c" +checksum = "b842f31e1aaa1af284a1f9dc4f1234761f72d07e70d53cf3f1521b0870ade094" dependencies = [ + "ahash", "anyhow", "aws-lc-rs", "blake3", @@ -1636,32 +1684,48 @@ dependencies = [ "cfg-if", "chacha20poly1305", "commonware-codec", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-parallel", "commonware-utils", + "cpufeatures 0.2.17", "crc-fast", - "ctutils", - "ecdsa", - "ed25519-consensus", + "ctutils 0.3.1", + "curve25519-dalek 5.0.0", + "ecdsa 0.17.0", + "fixedbitset", "getrandom 0.2.16", + "getrandom 0.4.3", + "hashbrown 0.16.1", "num-rational", "num-traits", + "once_cell", "p256", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "sha2 0.10.9", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rand_core 0.10.1", + "sha2 0.11.0", "thiserror 2.0.12", "x25519-dalek", "zeroize", ] +[[package]] +name = "commonware-formatting" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab01b7f2798e29b0a4f7b47da5b7f3c106881578af3e69e8d2ee9d89d7c1d4fd" +dependencies = [ + "commonware-macros", + "const-hex", +] + [[package]] name = "commonware-macros" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd313d9299e13bf995999c7a0ed8cc570eef6cd0972fcffc6e2c682cfba6663" +checksum = "39e37a17d5c99ec6711098c6614af1023fd32926aa0414ec0f0613f6bb940278" dependencies = [ "commonware-macros-impl", "tokio", @@ -1669,9 +1733,9 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc385e646d91b5397c93816985421878d627839834f7cf85a8da2ac9f8b98b7" +checksum = "7d44bceee874226efca172f0e9ec9ec4eb486788ba6be1d98710f3f159b807af" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1682,24 +1746,25 @@ dependencies = [ [[package]] name = "commonware-math" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d834ed8bf601e113b9cd2ba284dd0e95adf558933dc727f52f8879434cb286" +checksum = "593fa0c5aa8bd8d350724b9b1ad4e85072845aa85a06b39ca4598998bbec1e00" dependencies = [ "bytes", "commonware-codec", "commonware-macros", "commonware-parallel", "commonware-utils", - "rand_core 0.6.4", + "rand_core 0.10.1", ] [[package]] name = "commonware-p2p" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c93f730bf4aaeadffb589eb50e431f7a5f8495c158dda1127c61f6e74c597ab" +checksum = "47f974fcd27c0dad7ae73caeaf00c8330fd29c60f5ff1f679ccc1a51c8ed95bc" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -1713,9 +1778,8 @@ dependencies = [ "num-integer", "num-rational", "num-traits", - "prometheus-client 0.24.0", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "rand_distr", "thiserror 2.0.12", "tracing", @@ -1723,22 +1787,25 @@ dependencies = [ [[package]] name = "commonware-parallel" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db29306a40279ad54d06b42c623a05fbb5333546b5003c921796bc856b423106" +checksum = "7109817274c671f1fb0ba17cf8aecfbb70672d29379284feeac43fa1b1dc0201" dependencies = [ "cfg-if", "commonware-macros", + "dashmap", + "futures", "rayon", ] [[package]] name = "commonware-resolver" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dfe9932b33cc31a04b7c68bf543eef7e6d04b70cf6a53880d03407d60a01e6" +checksum = "1a497cc9e9a2cd205d96474d0d40c7731f29085cd467adbe9ff0f94719659582" dependencies = [ "bytes", + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -1747,40 +1814,44 @@ dependencies = [ "commonware-stream", "commonware-utils", "futures", - "prometheus-client 0.24.0", - "rand 0.8.5", + "rand 0.10.2", + "rand_core 0.10.1", "thiserror 2.0.12", "tracing", ] [[package]] name = "commonware-runtime" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ae4c804d0d9c1df615b1c7846e4e5e64fdb4228685487cb67803e67388411" +checksum = "2a84bc6cefb099b1ffce3e04705a1e61e91d895b9f09b9e1be14c8ec5823c181" dependencies = [ + "ahash", "axum 0.8.4", "bytes", "cfg-if", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-parallel", + "commonware-runtime-macros", "commonware-utils", "criterion", - "crossbeam-queue", + "crossbeam-utils", "futures", "getrandom 0.2.16", + "getrandom 0.4.3", "governor", "libc", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "prometheus-client 0.24.0", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "rayon", - "sha2 0.10.9", + "sha2 0.11.0", "sysinfo", "thiserror 2.0.12", "tokio", @@ -1789,11 +1860,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "commonware-runtime-macros" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f7b39ef9fd9f1df4fe4445c9a5ec5ba012d90d1602b12a8b8626a8563ab02c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "commonware-storage" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca1c42cf37aa27c3f83c31591cad4f1d96317eff81c1eb442e17191adcf9b413" +checksum = "eac18e2b696ab7bbecf1059bc3b8d39fc0af002180906d65c7df3112fd9385de" dependencies = [ "ahash", "anyhow", @@ -1801,14 +1884,14 @@ dependencies = [ "cfg-if", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-parallel", "commonware-runtime", "commonware-utils", "futures", "futures-util", - "prometheus-client 0.24.0", - "rayon", + "hashbrown 0.16.1", "thiserror 2.0.12", "tracing", "zstd", @@ -1816,19 +1899,19 @@ dependencies = [ [[package]] name = "commonware-stream" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c15b328d5f05fff750368a71e2307c380cce52df9712a5b30199b8af4e700c" +checksum = "8890b69e3fa209bc9e87d199ff60ca5a21c88322d32914847511887e6dc5364b" dependencies = [ "chacha20poly1305", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-runtime", "commonware-utils", "futures", - "rand 0.8.5", - "rand_core 0.6.4", + "rand_core 0.10.1", "thiserror 2.0.12", "x25519-dalek", "zeroize", @@ -1836,16 +1919,20 @@ dependencies = [ [[package]] name = "commonware-utils" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf66d7b5c89489d71b0669bda2e014e7c9ffcdf65629ae31886efe5361b1179" +checksum = "a484dd46b738d9b5ba892312425e71e6afaf4320e388d74449bd9ca0ea14cd76" dependencies = [ + "ahash", "bytes", "cfg-if", "commonware-codec", + "commonware-formatting", "commonware-macros", "futures", "getrandom 0.2.16", + "getrandom 0.3.3", + "getrandom 0.4.3", "hashbrown 0.16.1", "num-bigint", "num-integer", @@ -1853,9 +1940,10 @@ dependencies = [ "num-traits", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.10.2", "thiserror 2.0.12", "tokio", + "tracing", "zeroize", ] @@ -1868,7 +1956,7 @@ dependencies = [ "futures-core", "prost 0.13.5", "prost-types", - "tonic 0.12.3", + "tonic", "tracing-core", ] @@ -1892,7 +1980,7 @@ dependencies = [ "thread_local", "tokio", "tokio-stream", - "tonic 0.12.3", + "tonic", "tracing", "tracing-core", "tracing-subscriber", @@ -1900,15 +1988,14 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.14.1" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e22e0ed40b96a48d3db274f72fd365bd78f67af39b6bbd47e8a15e1c6207ff" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" dependencies = [ "cfg-if", - "cpufeatures", - "hex", + "cpufeatures 0.2.17", "proptest", - "serde", + "serde_core", ] [[package]] @@ -1917,6 +2004,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.34" @@ -1969,6 +2062,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1978,6 +2077,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -2106,6 +2214,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils 0.4.2", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -2117,13 +2241,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "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", + "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", + "subtle", ] [[package]] @@ -2133,9 +2278,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", "curve25519-dalek-derive", - "fiat-crypto", + "digest 0.11.3", + "fiat-crypto 0.3.0", "rustc_version 0.4.1", "subtle", "zeroize", @@ -2152,19 +2313,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - [[package]] name = "darling" version = "0.20.11" @@ -2256,7 +2404,17 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", "zeroize", ] @@ -2318,11 +2476,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", - "crypto-common", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils 0.4.2", +] + [[package]] name = "dirs" version = "5.0.1" @@ -2362,7 +2532,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2406,26 +2576,27 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "serdect 0.2.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] -name = "ed25519-consensus" -version = "2.1.0" +name = "ecdsa" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "thiserror 1.0.69", + "der 0.8.1", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] @@ -2444,16 +2615,36 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.1", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", - "serdect", + "sec1 0.7.3", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -2489,7 +2680,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa93f58bb1eb3d1e556e4f408ef1dac130bad01ac37db4e7ade45de40d1c86a" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "ring", "sha2 0.10.9", ] @@ -2582,12 +2773,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.6" @@ -2814,11 +3021,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "glob" version = "0.3.2" @@ -2900,11 +3121,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.4.11" @@ -2999,9 +3231,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-conservative" @@ -3021,6 +3250,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "http" version = "1.3.1" @@ -3073,6 +3311,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.6.0" @@ -3383,16 +3632,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3732,10 +3971,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", - "serdect", + "serdect 0.2.0", "sha2 0.10.9", ] @@ -3745,7 +3984,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -3889,11 +4128,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4058,12 +4297,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4263,9 +4501,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -4277,22 +4515,22 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", "http", "opentelemetry", - "reqwest", + "reqwest 0.13.4", ] [[package]] name = "opentelemetry-otlp" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -4300,35 +4538,33 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost 0.14.1", - "reqwest", + "reqwest 0.13.4", "thiserror 2.0.12", - "tracing", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost 0.14.1", - "tonic 0.14.2", - "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.1", "thiserror 2.0.12", "tokio", @@ -4341,22 +4577,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.0", ] [[package]] @@ -4481,8 +4712,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", ] [[package]] @@ -4525,7 +4766,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4560,13 +4801,31 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect 0.4.3", + "wnaf", ] [[package]] @@ -4720,7 +4979,7 @@ dependencies = [ "rand 0.9.1", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -4835,7 +5094,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.12", "tokio", "tracing", @@ -4872,7 +5131,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -4892,6 +5151,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4921,6 +5186,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4941,6 +5217,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -4960,14 +5246,20 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.10.2", ] [[package]] @@ -5017,12 +5309,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "readme-rustdocifier" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac" - [[package]] name = "recvmsg" version = "1.0.0" @@ -5060,18 +5346,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "reed-solomon-simd" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffef0520d30fbd4151fb20e262947ae47fb0ab276a744a19b6398438105a072" -dependencies = [ - "cpufeatures", - "fixedbitset", - "once_cell", - "readme-rustdocifier", -] - [[package]] name = "ref-cast" version = "1.0.24" @@ -5100,17 +5374,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5121,15 +5386,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -5145,9 +5404,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", - "futures-util", "h2", "http", "http-body", @@ -5182,6 +5439,37 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reth-metrics" version = "1.4.3" @@ -5213,10 +5501,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -5504,11 +5802,25 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", - "serdect", + "pkcs8 0.10.2", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils 0.4.2", + "der 0.8.1", + "hybrid-array", "subtle", "zeroize", ] @@ -5712,7 +6024,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", "serde", ] @@ -5723,32 +6045,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.9.9" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "block-buffer 0.9.0", "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest 0.10.7", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -5805,6 +6125,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simple_asn1" version = "0.6.3" @@ -5855,7 +6185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5896,7 +6226,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", ] [[package]] @@ -5945,12 +6285,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - [[package]] name = "summit" version = "0.0.3-alpha" @@ -5967,10 +6301,12 @@ dependencies = [ "alloy-signer", "anyhow", "clap", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-p2p", @@ -5997,8 +6333,8 @@ dependencies = [ "op-alloy-consensus", "procfs", "prometheus", - "rand 0.8.5", - "reqwest", + "rand 0.10.2", + "reqwest 0.12.22", "reth-metrics", "reth-tasks", "serde", @@ -6029,17 +6365,19 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", "anyhow", + "commonware-actor", "commonware-codec", "commonware-consensus", "commonware-cryptography", "commonware-macros", + "commonware-p2p", "commonware-runtime", "commonware-utils", "futures", "governor", "metrics", "prometheus-client 0.22.3", - "rand 0.8.5", + "rand 0.10.2", "summit-finalizer", "summit-syncer", "summit-types", @@ -6056,9 +6394,11 @@ dependencies = [ "alloy-rpc-types-engine", "anyhow", "bytes", + "commonware-actor", "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-math", "commonware-p2p", "commonware-parallel", @@ -6071,7 +6411,7 @@ dependencies = [ "hex", "metrics", "prometheus-client 0.24.0", - "rand 0.8.5", + "rand 0.10.2", "summit-orchestrator", "summit-syncer", "summit-types", @@ -6084,6 +6424,7 @@ name = "summit-orchestrator" version = "0.0.3-alpha" dependencies = [ "bytes", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-consensus", @@ -6098,8 +6439,8 @@ dependencies = [ "futures", "governor", "metrics", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "summit-syncer", "summit-types", "tokio-util", @@ -6119,6 +6460,7 @@ dependencies = [ "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-math", "commonware-runtime", "commonware-utils", @@ -6132,7 +6474,7 @@ dependencies = [ "jsonrpsee", "jsonrpsee-core 0.26.0", "jsonrpsee-types 0.26.0", - "rand 0.8.5", + "rand 0.10.2", "serde", "serde_json", "summit-finalizer", @@ -6150,6 +6492,7 @@ name = "summit-syncer" version = "0.0.3-alpha" dependencies = [ "bytes", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-consensus", @@ -6167,8 +6510,8 @@ dependencies = [ "metrics", "pin-project", "prometheus-client 0.24.0", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "summit-types", "tracing", "tracing-subscriber", @@ -6188,16 +6531,18 @@ dependencies = [ "alloy-transport-ipc", "anyhow", "bytes", + "commonware-actor", "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-math", "commonware-p2p", "commonware-parallel", "commonware-resolver", "commonware-runtime", "commonware-utils", - "curve25519-dalek", + "curve25519-dalek 4.1.3", "dirs 6.0.0", "ethereum_hashing", "ethereum_ssz", @@ -6205,8 +6550,8 @@ dependencies = [ "futures", "libc", "metrics", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "serde", "serde_json", "sha2 0.10.9", @@ -6634,38 +6979,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tonic" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http", - "http-body", - "http-body-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio-stream", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" -dependencies = [ - "bytes", - "prost 0.14.1", - "tonic 0.14.2", -] - [[package]] name = "tower" version = "0.4.13" @@ -6704,21 +7017,21 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tokio", "tower 0.5.2", "tower-layer", "tower-service", + "url", ] [[package]] @@ -6735,9 +7048,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -6759,9 +7072,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -6770,9 +7083,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -6801,16 +7114,13 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", - "opentelemetry_sdk", - "rustversion", "smallvec", - "thiserror 2.0.12", "tracing", "tracing-core", "tracing-log", @@ -6830,14 +7140,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -6857,9 +7167,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -6903,7 +7213,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -7678,6 +7988,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.1" @@ -7695,13 +8016,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index b0fc6603..916cc6bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,20 @@ summit-finalizer = {path = "finalizer"} summit-rpc = {path = "rpc"} summit-orchestrator = {path = "orchestrator"} -commonware-consensus = "2026.4.0" -commonware-cryptography = "2026.4.0" -commonware-storage = "2026.4.0" -commonware-runtime = "2026.4.0" -commonware-codec = "2026.4.0" -commonware-p2p = "2026.4.0" -commonware-broadcast = "2026.4.0" -commonware-utils = "2026.4.0" -commonware-resolver = "2026.4.0" -commonware-macros = "2026.4.0" -commonware-math = "2026.4.0" -commonware-parallel = "2026.4.0" +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" alloy-consensus = "1.0.12" alloy-eips = { version = "1.0.19", features = ["ssz"] } @@ -77,7 +79,7 @@ clap = { version = "4.5.40", features = ["derive"] } curve25519-dalek = "4.1.3" dirs = "6.0.0" governor = "0.10.2" -rand = "0.8.5" -rand_core = "0.6" +rand = { version = "0.10", features = ["std", "std_rng", "sys_rng", "thread_rng"] } +rand_core = "0.10" sha2 = "0.10.9" tracing = "0.1.41" diff --git a/application/Cargo.toml b/application/Cargo.toml index 83ff2572..c445da38 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -10,8 +10,10 @@ summit-finalizer.workspace = true anyhow.workspace = true tracing.workspace = true +commonware-actor.workspace = true commonware-consensus.workspace = true commonware-cryptography.workspace = true +commonware-p2p.workspace = true commonware-runtime.workspace = true commonware-macros.workspace = true commonware-utils.workspace = true diff --git a/application/src/actor.rs b/application/src/actor.rs index 4a66e451..c937cb83 100644 --- a/application/src/actor.rs +++ b/application/src/actor.rs @@ -130,7 +130,7 @@ impl< syncer: SyncerMailbox, finalizer: FinalizerMailbox, ) -> Handle<()> { - spawn_cell!(self.context, self.run(syncer, finalizer).await) + spawn_cell!(self.context, self.run(syncer, finalizer)) } pub async fn run( @@ -203,10 +203,12 @@ impl< // broadcast. let _ = response.send(digest); - self.context.with_label("proposed").spawn({ + self.context.child("proposed").spawn({ let mut syncer = syncer.clone(); move |_| async move { - syncer.proposed(round, block).await; + if !syncer.proposed(round, block).await { + warn!(?round, "syncer dropped proposed-block durability ack"); + } } }); }, @@ -250,23 +252,18 @@ impl< // Our own proposal was already handed to syncer.proposed() // at propose time (dispatched off the loop right after the // digest was returned), which caches and broadcasts it. - Plan::Propose => { + Plan::Propose { .. } => { debug!(?payload, "{rand_id} Broadcast(Propose): already broadcast at propose time"); } // Push the certified block to voters consensus has // identified as missing it (ForwardingPolicy::SilentVoters). - // Dispatch off the loop: syncer.forward enqueues into the - // bounded syncer mailbox, so awaiting it here would let a - // full or slow syncer block the application loop from - // dequeuing later Propose/Verify/Certify messages. - Plan::Forward { round, peers } => { - debug!(?round, n_peers = peers.len(), "{rand_id} Broadcast(Forward): forwarding to silent voters"); - self.context.with_label("forward").spawn({ - let syncer = syncer.clone(); - move |_| async move { - syncer.forward(round, payload, peers).await; - } - }); + // `forward` is a synchronous, non-blocking enqueue into the + // overflow-buffered syncer mailbox, so it can run directly on + // the application loop without blocking later + // Propose/Verify/Certify messages. + Plan::Forward { round, recipients } => { + debug!(?round, "{rand_id} Broadcast(Forward): forwarding to silent voters"); + let _ = syncer.forward(round, payload, recipients); } } } @@ -286,21 +283,20 @@ impl< debug!("{rand_id} application: Handling message Certify for round {} (epoch {}, view {})", round, round.epoch(), round.view()); - self.context.with_label("certify").spawn({ + self.context.child("certify").spawn({ let mut syncer = syncer.clone(); let mut finalizer_clone = finalizer.clone(); let mut engine_client = self.engine_client.clone(); let genesis_hash = self.genesis_hash; let max_message_size_bytes = self.max_message_size_bytes; move |context| async move { - // Subscribe inside the task: the enqueue goes into the - // bounded syncer mailbox, so doing it on the application - // loop would let a full syncer block later messages. - let block_request = syncer.subscribe(Some(round), payload).await; + // Subscribe inside the task; the enqueue is synchronous and + // non-blocking (overflow-buffered syncer mailbox). + let block_request = syncer.subscribe(Some(round), payload); let work = async { let Ok(block) = block_request.await else { warn!(?round, "certify: failed to receive block from syncer"); - return false; + return None; }; if let Some((block_size_bytes, max_block_size_bytes)) = @@ -317,7 +313,7 @@ impl< max_message_size_bytes, "certify: block violates P2P block size limit" ); - return false; + return Some(false); } // Wait for parent to be executed so its state is in Reth @@ -343,7 +339,7 @@ impl< ?parent_digest, "certify: finalizer did not confirm parent on its chain (superseded, fork, or digest mismatch)" ); - return false; + return Some(false); } } @@ -395,13 +391,24 @@ impl< height = block.height(), "certify: payload rejected by execution client" ); + return Some(false); + } + + // Certification permits Simplex to cast a finalize vote. + // Hold that vote until the block and any accepted + // notarization for this round are durably stored. + if !syncer.certified(round, block).await { + warn!(?round, "certify: syncer durability barrier closed"); + return None; } - valid + Some(true) }; select! { result = work => { - let _ = response.send(result); + if let Some(result) = result { + let _ = response.send(result); + } }, _ = response.closed() => { warn!("certify aborted for round {round}"); @@ -432,12 +439,12 @@ impl< // `move` closure copies this Copy `u64` for `handle_verify`). let signed_parent_view = parent.0.get(); - // Subscribe and wait for the blocks in a separate task so a full - // or slow syncer mailbox cannot block the application loop from - // dequeuing later consensus messages. The subscribe enqueues go - // into the bounded syncer mailbox, so they run off-loop too. + // Wait for the blocks in a separate task: the subscribe enqueues + // are non-blocking, but awaiting their responses on the + // application loop would block it from dequeuing later consensus + // messages until the blocks arrive. let genesis_hash = self.genesis_hash; - self.context.with_label("verify").spawn({ + self.context.child("verify").spawn({ let mut syncer = syncer.clone(); let mut finalizer_clone = finalizer.clone(); let epocher = self.epocher.clone(); @@ -453,9 +460,9 @@ impl< } else { Some(Round::new(round.epoch(), parent.0)) }; - Either::Right(syncer.subscribe(parent_round, parent.1).await) + Either::Right(syncer.subscribe(parent_round, parent.1)) }; - let block_request = syncer.subscribe(Some(round), payload).await; + let block_request = syncer.subscribe(Some(round), payload); let requester = try_join(parent_request, block_request); select! { @@ -535,7 +542,9 @@ impl< let _ = response.send(true); // persist valid block off the vote response path - syncer.verified(round, block).await; + if !syncer.verified(round, block).await { + warn!(?round, "syncer dropped verified-block durability ack"); + } } else { info!("Unsuccessful vote for round {round} because the block is invalid"); let _ = response.send(false); @@ -613,7 +622,6 @@ impl< Either::Right( syncer .subscribe(parent_round, parent.1) - .await .map(|x| x.context("parent block subscription canceled")), ) }; diff --git a/application/src/ingress.rs b/application/src/ingress.rs index 4543a4aa..1c5c1cb4 100644 --- a/application/src/ingress.rs +++ b/application/src/ingress.rs @@ -1,3 +1,4 @@ +use commonware_actor::Feedback; use commonware_consensus::types::{Epoch, Round}; use commonware_consensus::{ Automaton, CertifiableAutomaton, Relay, @@ -7,6 +8,7 @@ use commonware_consensus::{ use commonware_cryptography::PublicKey; use commonware_cryptography::sha256::Digest; use commonware_utils::channel::{mpsc, oneshot}; +use summit_types::scheme::EpochGenesisProvider; pub enum Message { Genesis { @@ -46,11 +48,13 @@ impl Mailbox

{ } } -impl Automaton for Mailbox

{ - type Context = Context; - type Digest = Digest; - - async fn genesis(&mut self, epoch: Epoch) -> Self::Digest { +impl EpochGenesisProvider for Mailbox

{ + /// Retrieve the genesis payload digest for the given epoch. + /// + /// Consensus no longer queries this through [Automaton]; the orchestrator + /// fetches it when spawning an epoch's engine and passes it via + /// `simplex::Config::floor`. + async fn genesis(&mut self, epoch: Epoch) -> Digest { let (response, receiver) = oneshot::channel(); self.sender .send(Message::Genesis { response, epoch }) @@ -58,6 +62,11 @@ impl Automaton for Mailbox

{ .expect("Failed to send genesis"); receiver.await.expect("Failed to receive genesis") } +} + +impl Automaton for Mailbox

{ + type Context = Context; + type Digest = Digest; async fn propose( &mut self, @@ -99,6 +108,9 @@ impl Automaton for Mailbox

{ } impl CertifiableAutomaton for Mailbox

{ + /// Returns `true` only after execution validation succeeds and the block is + /// durably stored by the syncer. This is the durability barrier before + /// Simplex may journal and broadcast a finalize vote. async fn certify(&mut self, round: Round, payload: Self::Digest) -> oneshot::Receiver { let (response, receiver) = oneshot::channel(); self.sender @@ -118,14 +130,15 @@ impl Relay for Mailbox

{ type PublicKey = P; type Plan = commonware_consensus::simplex::Plan

; - async fn broadcast(&mut self, digest: Self::Digest, plan: Self::Plan) { - self.sender - .send(Message::Broadcast { - payload: digest, - plan, - }) - .await - .expect("Failed to send broadcast"); + fn broadcast(&mut self, digest: Self::Digest, plan: Self::Plan) -> Feedback { + match self.sender.try_send(Message::Broadcast { + payload: digest, + plan, + }) { + Ok(()) => Feedback::Ok, + Err(mpsc::error::TrySendError::Full(_)) => Feedback::Backoff, + Err(mpsc::error::TrySendError::Closed(_)) => Feedback::Closed, + } } } @@ -134,6 +147,7 @@ mod tests { use super::*; use commonware_codec::DecodeExt as _; use commonware_cryptography::{Hasher as _, Signer as _, ed25519, sha256::Sha256}; + use commonware_p2p::Recipients; fn test_public_key(seed: u8) -> ed25519::PublicKey { ed25519::PrivateKey::decode([seed; 32].as_ref()) @@ -155,15 +169,13 @@ mod tests { let round = Round::new(Epoch::new(3), View::new(7)); let peers = vec![test_public_key(1), test_public_key(2)]; - mailbox - .broadcast( - digest, - Plan::Forward { - round, - peers: peers.clone(), - }, - ) - .await; + let _ = mailbox.broadcast( + digest, + Plan::Forward { + round, + recipients: Recipients::Some(peers.clone()), + }, + ); let Some(Message::Broadcast { payload, plan }) = rx.recv().await else { panic!("expected a Broadcast message"); }; @@ -171,20 +183,22 @@ mod tests { match plan { Plan::Forward { round: got_round, - peers: got_peers, + recipients: got_recipients, } => { assert_eq!(got_round, round); - assert_eq!(got_peers, peers); + assert!( + matches!(got_recipients, Recipients::Some(got_peers) if got_peers == peers) + ); } - Plan::Propose => panic!("Plan::Forward was lost in the relay"), + Plan::Propose { .. } => panic!("Plan::Forward was lost in the relay"), } - mailbox.broadcast(digest, Plan::Propose).await; + let _ = mailbox.broadcast(digest, Plan::Propose { round }); let Some(Message::Broadcast { payload, plan }) = rx.recv().await else { panic!("expected a Broadcast message"); }; assert_eq!(payload, digest); - assert!(matches!(plan, Plan::Propose)); + assert!(matches!(plan, Plan::Propose { .. })); }); } } diff --git a/docs/commonware-dependencies.md b/docs/commonware-dependencies.md index ea8489d7..04e9e5c9 100644 --- a/docs/commonware-dependencies.md +++ b/docs/commonware-dependencies.md @@ -44,7 +44,7 @@ Summit leverages the [Commonware library](https://commonware.xyz) extensively fo **Key Components:** - `authenticated` - Authenticated P2P connections (production) - `simulated` - In-process network (deterministic tests) -- `Manager`, `Provider`, `TrackedPeers`, `PeerSetUpdate` - Peer set management +- `Manager`, `Provider`, `TrackedPeers`, `PeerSetSubscription` - Peer set management - `Sender`/`Receiver` - Message transmission - `Blocker`, `Ingress` - Connection filtering and admission @@ -95,10 +95,13 @@ Summit leverages the [Commonware library](https://commonware.xyz) extensively fo **Key Components:** - `NZU64`, `NZUsize` - Non-zero integer types (and their constructor macros) -- `from_hex_formatted`, `hex` - Hexadecimal encoding/decoding +- `channel::{mpsc, oneshot}` - Inter-actor channels +- `vec::NonEmptyVec`, `ordered` - Non-empty vectors and ordered sets/maps - `Hostname` - Validated hostname type for bootstrap configuration - `acknowledgement::{Acknowledgement, Exact}` - Activity acknowledgement tracking +Hex encoding/decoding moved to `commonware-formatting` in 2026.5.0. + ### 7. Codec (`commonware-codec`) **Used for**: Efficient serialization and deserialization @@ -123,9 +126,11 @@ Summit leverages the [Commonware library](https://commonware.xyz) extensively fo **Used for**: Missing data resolution and backfill **Key Components:** -- `Resolver` - Generic resolution interface -- `Consumer`/`Producer` - Data request/response -- `p2p::Producer` - P2P data resolution +- `Resolver` / `TargetedResolver` - Fetch interfaces (broadcast and peer-targeted) +- `Fetch` / `Delivery` - A fetch pairs a peer-visible `Key` with a local `Subscriber` annotation; deliveries return both so the consumer knows why the data was requested +- `Consumer`/`Producer` - Data request/response; `Consumer::deliver` returns a `oneshot::Receiver` so response validity is judged off the resolver loop +- `retain(predicate)` - Prunes outstanding fetches (e.g. below the syncer's processed floor) +- `p2p::Engine` - P2P resolution engine ### 10. Macros (`commonware-macros`) @@ -154,6 +159,25 @@ Summit leverages the [Commonware library](https://commonware.xyz) extensively fo - `Strategy` - Abstraction over execution strategies - `Sequential` - Single-threaded execution strategy (used by the syncer and engine) +### 13. Actor (`commonware-actor`) + +**Used for**: Actor mailboxes with explicit backpressure policies + +**Key Components:** +- `mailbox::{new, Sender, Receiver}` - Bounded actor mailboxes with synchronous `enqueue` +- `mailbox::{Policy, Overflow}` - Per-message overflow handling when a mailbox fills (e.g. the syncer coalesces finalization hints per height instead of blocking callers) +- `Feedback` - Result of a synchronous send (`Ok`/`Backoff`/`Closed`), returned by `Reporter::report`, `Relay::broadcast`, and p2p oracle calls + +**Critical Usage:** +- **Non-blocking control loops**: The orchestrator and application enqueue into the syncer mailbox without awaiting, so a slow syncer cannot park epoch transitions or consensus message handling + +### 14. Formatting (`commonware-formatting`) + +**Used for**: Hexadecimal encoding and decoding + +**Key Components:** +- `hex` / `from_hex` - Hex encoding/decoding for keys, digests, and genesis configuration (moved out of `commonware-utils` in 2026.5.0) + ## Security Analysis ### Cryptographic Security @@ -240,16 +264,28 @@ use commonware_macros::test_traced; ### Upgrade Path -Summit pins Commonware to a versioned release in the workspace `Cargo.toml`. All 12 `commonware-*` workspace dependencies are bumped in lockstep: +Summit pins Commonware to a versioned release in the workspace `Cargo.toml`. All 14 `commonware-*` workspace dependencies are bumped in lockstep: ```toml -commonware-consensus = "2026.4.0" -commonware-cryptography = "2026.4.0" +commonware-consensus = "2026.7.0" +commonware-cryptography = "2026.7.0" # ... ``` To upgrade, bump the version across every `commonware-*` entry in the root `Cargo.toml` and run `cargo update -p commonware-consensus` (etc.). +### Syncer Durability + +Summit's syncer is a fork of Commonware marshal with additional application reporting and checkpoint behavior. It preserves marshal's durability model while reporting notarized blocks to the finalizer for speculative execution: + +- Proposed blocks are handed to the network before persistence starts so storage does not delay propagation. +- Proposed, verified, and certified writes return durability handles to the mailbox caller, which awaits them without blocking the syncer actor. +- Certified durability covers both the block and any accepted notarization for the round. +- Summit's `CertifiableAutomaton::certify` waits for the certified durability barrier before returning `true`, so Simplex cannot cast a finalize vote before the block is recoverable locally. +- Finalized blocks are not dispatched to the application until the finalized block and certificate archives are durable. +- Direct consensus notarizations start storage asynchronously and are reported through `Update::NotarizedBlock` only after both the block and notarization are durable; storage failures remain fatal. +- Resolver-delivered notarized data is made durable before repair and finalization bookkeeping advances. + ## Audit Recommendations When auditing Summit's Commonware usage: diff --git a/docs/testing.md b/docs/testing.md index 3948f1fa..fa633fb8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -19,6 +19,7 @@ tests/ ├── checkpointing/ │ ├── creation.rs # Checkpoint creation │ ├── joining.rs # Joining network with checkpoint +│ ├── startup.rs # Checkpoint startup policy and imports │ └── verification.rs # Checkpoint integrity verification ├── execution_requests/ │ ├── deposits.rs # Validator registration @@ -26,7 +27,17 @@ tests/ │ ├── protocol_params.rs # Parameter updates │ ├── deposit_withdrawal_combined.rs # Mixed operations │ └── validator_set.rs # Validator set transitions -└── syncer.rs # Block sync & cache +├── engine.rs # Engine lifecycle and configuration +├── observer.rs # Observer sync and backfill +└── syncer.rs # Block sync and cache +``` + +Test modules can be run independently with Cargo's name filter. For example: + +```bash +cargo test -p summit --lib 'tests::checkpointing::' +cargo test -p summit --lib 'tests::execution_requests::deposits::' +cargo test -p summit --lib 'tests::execution_requests::withdrawals::' ``` ## End-to-End Tests diff --git a/finalizer/Cargo.toml b/finalizer/Cargo.toml index 65a5f450..6e97c368 100644 --- a/finalizer/Cargo.toml +++ b/finalizer/Cargo.toml @@ -8,6 +8,8 @@ summit-types.workspace = true summit-syncer.workspace = true summit-orchestrator.workspace = true +commonware-actor.workspace = true +commonware-formatting.workspace = true commonware-codec.workspace = true commonware-consensus.workspace = true commonware-cryptography.workspace = true diff --git a/finalizer/benches/consensus_state_write.rs b/finalizer/benches/consensus_state_write.rs index 9f2b4b57..175084f1 100644 --- a/finalizer/benches/consensus_state_write.rs +++ b/finalizer/benches/consensus_state_write.rs @@ -95,6 +95,7 @@ fn main() { ), }, translator: EightCap, + init_cache_size: Some(NZUsize!(1024)), }; let mut db = diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index 9306cc38..ddeea9d5 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -11,16 +11,19 @@ use commonware_consensus::simplex::types::Finalization; use commonware_consensus::types::Epoch; use commonware_cryptography::bls12381::primitives::variant::Variant; use commonware_cryptography::{Digestible, Signer}; -use commonware_runtime::{Clock, ContextCell, Handle, Metrics, Spawner, Storage, spawn_cell}; +use commonware_formatting::hex; +#[cfg(debug_assertions)] +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, hex}; +use commonware_utils::{NZU64, NZUsize}; use futures::channel::{mpsc, oneshot}; use futures::{FutureExt, StreamExt as _, select_biased}; #[cfg(feature = "prom")] use metrics::{counter, histogram}; -#[cfg(debug_assertions)] -use prometheus_client::metrics::gauge::Gauge; use rand::Rng; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::marker::PhantomData; @@ -215,13 +218,14 @@ struct PendingNotarized { } pub struct Finalizer< - R: Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, + R: BufferPooler + Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, C: EngineClient, O: NetworkOracle, S: Signer, V: Variant, > { - mailbox: mpsc::Receiver, Block>>, + mailbox: mpsc::Receiver>>, + updates: mpsc::UnboundedReceiver>>, state_query: mpsc::Receiver>, pending_height_notifys: BTreeMap<(u64, Digest), Vec>>, context: ContextCell, @@ -290,7 +294,7 @@ pub struct Finalizer< } impl< - R: Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, + R: BufferPooler + Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, C: EngineClient, O: NetworkOracle, S: Signer, @@ -307,6 +311,7 @@ impl< ConsensusStateQuery>, ) { 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 { @@ -318,10 +323,11 @@ impl< page_cache: cfg.page_cache, }, translator: EightCap, + init_cache_size: Some(NZUsize!(1024)), }; let db = FinalizerState::::new( - context.with_label("finalizer_state"), + context.child("finalizer_state"), state_cfg, cfg.cancellation_token.clone(), ) @@ -351,21 +357,10 @@ impl< // Register debug gauges before moving context into ContextCell #[cfg(debug_assertions)] - let height_gauge = { - let gauge: Gauge = Gauge::default(); - context.register("height", "chain height", gauge.clone()); - gauge - }; + let height_gauge = context.gauge("height", "chain height"); #[cfg(debug_assertions)] - let consensus_state_stored_gauge = { - let gauge: Gauge = Gauge::default(); - context.register( - "consensus_state_stored", - "consensus state stored", - gauge.clone(), - ); - gauge - }; + let consensus_state_stored_gauge = + context.gauge("consensus_state_stored", "consensus state stored"); let shared_state = state.clone_with_shared_epocher(); @@ -373,6 +368,7 @@ impl< Self { context: ContextCell::new(context), mailbox: rx, + updates: updates_rx, state_query: state_query_rx, engine_client: cfg.engine_client, oracle: cfg.oracle, @@ -407,13 +403,13 @@ impl< consensus_state_stored_gauge, }, shared_state, - FinalizerMailbox::new(tx), + FinalizerMailbox::new(tx, updates_tx), state_query, ) } pub fn start(mut self, orchestrator_mailbox: summit_orchestrator::Mailbox) -> Handle<()> { - spawn_cell!(self.context, self.run(orchestrator_mailbox).await) + spawn_cell!(self.context, self.run(orchestrator_mailbox)) } pub async fn run(mut self, mut orchestrator_mailbox: summit_orchestrator::Mailbox) { @@ -443,12 +439,10 @@ impl< ) .await; - orchestrator_mailbox - .report(Message::Enter(EpochTransition { - epoch: Epoch::new(self.canonical_state.get_epoch()), - validator_keys: current_epoch_validators, - })) - .await; + let _ = orchestrator_mailbox.report(Message::Enter(EpochTransition { + epoch: Epoch::new(self.canonical_state.get_epoch()), + validator_keys: current_epoch_validators, + })); // Send initial forkchoice to the execution client so it knows the // chain head and can start P2P sync. @@ -546,11 +540,9 @@ impl< futures::pin_mut!(query_message); select_biased! { - mailbox_message = self.mailbox.next() => { - let mail = mailbox_message.expect("Finalizer mailbox closed"); - match mail { - FinalizerMessage::SyncerUpdate { update } => { - match update { + update = self.updates.next() => { + let update = update.expect("Finalizer updates channel closed"); + match update { Update::Tip(_height, _digest) => { // I don't think we need this } @@ -593,8 +585,11 @@ impl< break; } } - } - }, + } + } + mailbox_message = self.mailbox.next() => { + let mail = mailbox_message.expect("Finalizer mailbox closed"); + match mail { FinalizerMessage::NotifyAtHeight { height, block_digest, response } => { if self.canonical_state.get_latest_height() > height { // This block proposal is trying to build a block at height + 1, @@ -1129,17 +1124,18 @@ impl< #[cfg(debug_assertions)] { - let gauge: Gauge = Gauge::default(); - gauge.set(new_height as i64); - self.context.register( + let gauge = self.context.gauge( format!( "

{}
{}_finalized_header_stored", hex::encode(finalized_header.header().get_digest()), hex::encode(finalized_header.header().prev_epoch_header_hash()) ), "chain height", - gauge, ); + gauge.set(new_height as i64); + // Keep the registration alive: dropping a `Registered` handle + // removes the metric from the registry. + std::mem::forget(gauge); } // Apply pending protocol parameter changes durably at the boundary. @@ -1313,12 +1309,10 @@ impl< "signaling orchestrator to enter new epoch" ); - orchestrator_mailbox - .report(Message::Enter(EpochTransition { - epoch: Epoch::new(self.canonical_state.get_epoch()), - validator_keys: active_validators, - })) - .await; + let _ = orchestrator_mailbox.report(Message::Enter(EpochTransition { + epoch: Epoch::new(self.canonical_state.get_epoch()), + validator_keys: active_validators, + })); epoch_change = true; } else { // Every block needs to be ack'ed. @@ -1335,11 +1329,9 @@ impl< old_epoch = self.canonical_state.get_epoch() - 1, "signaling orchestrator to exit old epoch" ); - orchestrator_mailbox - .report(Message::Exit(Epoch::new( - self.canonical_state.get_epoch() - 1, - ))) - .await; + let _ = orchestrator_mailbox.report(Message::Exit(Epoch::new( + self.canonical_state.get_epoch() - 1, + ))); } let tx_count = block.payload.payload_inner.payload_inner.transactions.len(); info!( @@ -2018,7 +2010,7 @@ impl< let root = self.canonical_state.get_state_root(); let el_block_number = self.canonical_state.get_proof_el_block_number(); self.context - .with_label("state_proof") + .child("state_proof") .shared(true) .spawn(move |_| async move { let proofs = generate_state_proofs( @@ -2077,7 +2069,7 @@ impl< /// is always finalized (never notarized+nullified). async fn execute_block< C: EngineClient, - R: Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, + R: BufferPooler + Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, >( engine_client: &mut C, context: &ContextCell, @@ -2285,7 +2277,7 @@ async fn execute_block< } async fn process_execution_requests< - R: Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, + R: BufferPooler + Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, >( #[allow(unused)] context: &ContextCell, block: &Block, @@ -2317,7 +2309,7 @@ async fn process_execution_requests< } impl< - R: Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, + R: BufferPooler + Storage + Metrics + Clock + Spawner + governor::clock::Clock + Rng, C: EngineClient, O: NetworkOracle, S: Signer, diff --git a/finalizer/src/db.rs b/finalizer/src/db.rs index 968c8781..80e951dd 100644 --- a/finalizer/src/db.rs +++ b/finalizer/src/db.rs @@ -3,7 +3,7 @@ use commonware_codec::{EncodeSize, Error, Read, Write}; use commonware_consensus::simplex::scheme::bls12381_multisig; use commonware_cryptography::bls12381::primitives::variant::Variant; use commonware_cryptography::ed25519::PublicKey; -use commonware_runtime::{Clock, Metrics, Storage}; +use commonware_runtime::{BufferPooler, Clock, Metrics, Storage}; use commonware_storage::qmdb::store::db::{self, Db}; use commonware_storage::translator::EightCap; use commonware_utils::sequence::FixedBytes; @@ -26,12 +26,12 @@ 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]; -pub struct FinalizerState { +pub struct FinalizerState { store: Db, Value, EightCap>, cancellation_token: CancellationToken, } -impl FinalizerState { +impl FinalizerState { pub async fn new( context: E, cfg: Config, @@ -460,6 +460,7 @@ mod tests { ), }, translator: EightCap, + init_cache_size: Some(NZUsize!(1024)), }; FinalizerState::::new(context, config, CancellationToken::new()).await } diff --git a/finalizer/src/ingress.rs b/finalizer/src/ingress.rs index e00443fc..4cd7b373 100644 --- a/finalizer/src/ingress.rs +++ b/finalizer/src/ingress.rs @@ -1,3 +1,4 @@ +use commonware_actor::Feedback; use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::{Block as ConsensusBlock, Reporter}; use futures::{ @@ -14,7 +15,7 @@ use summit_types::{ }; #[allow(clippy::large_enum_variant)] -pub enum FinalizerMessage, B: ConsensusBlock = Block> { +pub enum FinalizerMessage> { NotifyAtHeight { height: u64, block_digest: Digest, @@ -33,19 +34,20 @@ pub enum FinalizerMessage, B: ConsensusBlock = Block> { request: ConsensusStateRequest, response: oneshot::Sender>, }, - SyncerUpdate { - update: Update, - }, } #[derive(Clone)] -pub struct FinalizerMailbox, B: ConsensusBlock = Block> { - sender: mpsc::Sender>, +pub struct FinalizerMailbox, B: ConsensusBlock = Block> { + sender: mpsc::Sender>, + updates: mpsc::UnboundedSender>, } -impl, B: ConsensusBlock> FinalizerMailbox { - pub fn new(sender: mpsc::Sender>) -> Self { - Self { sender } +impl, B: ConsensusBlock> FinalizerMailbox { + pub fn new( + sender: mpsc::Sender>, + updates: mpsc::UnboundedSender>, + ) -> Self { + Self { sender, updates } } pub async fn notify_at_height( @@ -500,13 +502,17 @@ impl, B: ConsensusBlock> FinalizerMailbox { } } -impl, B: ConsensusBlock> Reporter for FinalizerMailbox { +impl, B: ConsensusBlock> Reporter for FinalizerMailbox { type Activity = Update; - async fn report(&mut self, activity: Self::Activity) { - self.sender - .send(FinalizerMessage::SyncerUpdate { update: activity }) - .await - .expect("Unable to send syncer update to Finalizer"); + fn report(&mut self, activity: Self::Activity) -> Feedback { + // Syncer updates ride a dedicated unbounded channel: delivery must be + // reliable (a dropped `Update::FinalizedBlock` would stall the syncer's + // dispatch pipeline forever) and the syncer already bounds in-flight + // updates via `max_pending_acks`. + if self.updates.unbounded_send(activity).is_err() { + return Feedback::Closed; + } + Feedback::Ok } } diff --git a/finalizer/src/tests/fork_handling.rs b/finalizer/src/tests/fork_handling.rs index 78903123..d16494a0 100644 --- a/finalizer/src/tests/fork_handling.rs +++ b/finalizer/src/tests/fork_handling.rs @@ -11,9 +11,10 @@ use commonware_consensus::Reporter; use commonware_cryptography::bls12381::primitives::variant::MinPk; use commonware_cryptography::{Signer as _, bls12381, ed25519}; use commonware_math::algebra::Random; +use commonware_runtime::Supervisor as _; use commonware_runtime::buffer::paged::CacheRef; use commonware_runtime::deterministic::{self, Runner}; -use commonware_runtime::{Clock, Metrics, Runner as _}; +use commonware_runtime::{Clock, Runner as _}; use commonware_utils::NZUsize; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; use futures::{FutureExt as _, channel::mpsc as futures_mpsc}; @@ -151,7 +152,7 @@ fn test_orphaned_block_processed_when_parent_arrives() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -186,7 +187,7 @@ fn test_orphaned_block_processed_when_parent_arrives() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -206,11 +207,11 @@ fn test_orphaned_block_processed_when_parent_arrives() { let block2_digest = block2.digest(); // Send block2 first (orphaned - parent block1 not yet processed) - mailbox.report(Update::NotarizedBlock(block2.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block2.clone())); context.sleep(Duration::from_millis(50)).await; // Now send block1 (parent is genesis/canonical) - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(100)).await; // Verify both blocks are in fork_states @@ -247,7 +248,7 @@ fn test_fork_aux_data_does_not_finalize_unfinalized_fork_head() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -282,7 +283,7 @@ fn test_fork_aux_data_does_not_finalize_unfinalized_fork_head() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -297,7 +298,7 @@ fn test_fork_aux_data_does_not_finalize_unfinalized_fork_head() { // fork_states as a speculative fork head; canonical finalized stays at genesis. let block1 = create_test_block(genesis_digest, 1, 2, 1001); let block1_digest = block1.digest(); - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(100)).await; let in_forks = mailbox @@ -356,7 +357,7 @@ fn test_losing_height_waiter_resolves_false_on_conflicting_finalization() { let genesis_hash = [0x49u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -391,7 +392,7 @@ fn test_losing_height_waiter_resolves_false_on_conflicting_finalization() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -414,9 +415,7 @@ fn test_losing_height_waiter_resolves_false_on_conflicting_finalization() { drop(dropped_losing_notify); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((winning_block.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((winning_block.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_height().await, 1); @@ -446,7 +445,7 @@ fn test_competing_digest_waiter_stays_pending_until_finalization() { let genesis_hash = [0x50u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -481,7 +480,7 @@ fn test_competing_digest_waiter_stays_pending_until_finalization() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -499,9 +498,7 @@ fn test_competing_digest_waiter_stays_pending_until_finalization() { let pending_probe = mailbox.notify_at_height(1, block1b_digest).await; let losing_notify = mailbox.notify_at_height(1, block1b_digest).await; - mailbox - .report(Update::NotarizedBlock(block1a.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(block1a.clone())); context.sleep(Duration::from_millis(100)).await; assert_eq!( @@ -511,9 +508,7 @@ fn test_competing_digest_waiter_stays_pending_until_finalization() { ); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1a.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1a.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!( @@ -537,7 +532,7 @@ fn test_finalization_resolves_lower_waiters_and_preserves_future_waiters() { let genesis_hash = [0x51u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -572,7 +567,7 @@ fn test_finalization_resolves_lower_waiters_and_preserves_future_waiters() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -587,8 +582,8 @@ fn test_finalization_resolves_lower_waiters_and_preserves_future_waiters() { let block1_digest = block1.digest(); let block2 = create_test_block(block1_digest, 2, 3, 8202); - mailbox.report(Update::NotarizedBlock(block1.clone())).await; - mailbox.report(Update::NotarizedBlock(block2.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block2.clone())); context.sleep(Duration::from_millis(100)).await; let stale_block = create_test_block(genesis_digest, 1, 2, 8203); @@ -599,15 +594,11 @@ fn test_finalization_resolves_lower_waiters_and_preserves_future_waiters() { // Finalize sequentially (the syncer never skips a height): canonical // advances 0 -> 1 -> 2. let (ack1, _waiter1) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack1)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack1)); context.sleep(Duration::from_millis(100)).await; let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block2.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block2.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_height().await, 2); @@ -636,7 +627,7 @@ fn test_multiple_forks_tracked() { let genesis_hash = [0x43u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -671,7 +662,7 @@ fn test_multiple_forks_tracked() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -691,12 +682,8 @@ fn test_multiple_forks_tracked() { assert_ne!(block1a_digest, block1b_digest); - mailbox - .report(Update::NotarizedBlock(block1a.clone())) - .await; - mailbox - .report(Update::NotarizedBlock(block1b.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(block1a.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block1b.clone())); context.sleep(Duration::from_millis(100)).await; // Both should be in fork_states @@ -723,7 +710,7 @@ fn test_dead_fork_block_discarded() { let genesis_hash = [0x44u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -758,7 +745,7 @@ fn test_dead_fork_block_discarded() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -774,9 +761,7 @@ fn test_dead_fork_block_discarded() { let block1_digest = block1.digest(); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_height().await, 1); @@ -785,9 +770,7 @@ fn test_dead_fork_block_discarded() { let wrong_parent: Digest = [0xDEu8; 32].into(); let dead_fork_block = create_test_block(wrong_parent, 2, 3, 3002); - mailbox - .report(Update::NotarizedBlock(dead_fork_block.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(dead_fork_block.clone())); context.sleep(Duration::from_millis(100)).await; // Canonical chain should still be at height 1 @@ -797,9 +780,7 @@ fn test_dead_fork_block_discarded() { let valid_block2 = create_test_block(block1_digest, 2, 3, 3003); let valid_block2_digest = valid_block2.digest(); - mailbox - .report(Update::NotarizedBlock(valid_block2.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(valid_block2.clone())); context.sleep(Duration::from_millis(100)).await; let notify_valid = mailbox.notify_at_height(2, valid_block2_digest).await; @@ -826,7 +807,7 @@ fn test_fork_states_pruned_after_finalization() { let genesis_hash = [0x45u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -861,7 +842,7 @@ fn test_fork_states_pruned_after_finalization() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -883,9 +864,9 @@ fn test_fork_states_pruned_after_finalization() { let block3_digest = block3.digest(); // Send all as notarized (they go to fork_states) - mailbox.report(Update::NotarizedBlock(block1.clone())).await; - mailbox.report(Update::NotarizedBlock(block2.clone())).await; - mailbox.report(Update::NotarizedBlock(block3.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block2.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block3.clone())); context.sleep(Duration::from_millis(100)).await; // Verify all three are in fork_states @@ -901,16 +882,12 @@ fn test_fork_states_pruned_after_finalization() { // monotonic height order with no gaps, so the finalizer advances // canonical 0 -> 1 -> 2 rather than jumping straight to height 2. let (ack1, _waiter1) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack1)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack1)); context.sleep(Duration::from_millis(100)).await; // Now finalize block2 (height 2) let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block2.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block2.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; // Canonical height should be 2 @@ -957,7 +934,7 @@ fn test_losing_fork_descendant_rejected_after_conflicting_ancestor_finalizes() { let genesis_hash = [0x49u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -992,7 +969,7 @@ fn test_losing_fork_descendant_rejected_after_conflicting_ancestor_finalizes() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1012,12 +989,8 @@ fn test_losing_fork_descendant_rejected_after_conflicting_ancestor_finalizes() { assert_ne!(block_a1_digest, block_b1_digest); - mailbox - .report(Update::NotarizedBlock(block_a1.clone())) - .await; - mailbox - .report(Update::NotarizedBlock(block_a2.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(block_a1.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block_a2.clone())); context.sleep(Duration::from_millis(100)).await; let notify_a2 = mailbox.notify_at_height(2, block_a2_digest).await; @@ -1027,9 +1000,7 @@ fn test_losing_fork_descendant_rejected_after_conflicting_ancestor_finalizes() { ); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block_b1.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block_b1.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_height().await, 1); @@ -1081,7 +1052,7 @@ fn test_finalized_dead_fork_descendant_out_of_sequence_halts() { let genesis_hash = [0x51u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1120,7 +1091,7 @@ fn test_finalized_dead_fork_descendant_out_of_sequence_halts() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1141,23 +1112,15 @@ fn test_finalized_dead_fork_descendant_out_of_sequence_halts() { let block_b1 = create_test_block(genesis_digest, 1, 5, 9004); assert_ne!(block_a1_digest, block_b1.digest()); - mailbox - .report(Update::NotarizedBlock(block_a1.clone())) - .await; - mailbox - .report(Update::NotarizedBlock(block_a2.clone())) - .await; - mailbox - .report(Update::NotarizedBlock(block_a3.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(block_a1.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block_a2.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block_a3.clone())); context.sleep(Duration::from_millis(100)).await; // Finalize the conflicting ancestor B1; this prunes the A-fork and // advances canonical to height 1. let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block_b1.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block_b1.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_height().await, 1); assert!( @@ -1168,9 +1131,7 @@ fn test_finalized_dead_fork_descendant_out_of_sequence_halts() { // Deliver the pruned descendant A3 (height 3) as a finalized block while // canonical is only at height 1. The strict guard must reject it. let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block_a3.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block_a3.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; assert!( @@ -1199,7 +1160,7 @@ fn test_orphaned_blocks_pruned_after_finalization() { let genesis_hash = [0x46u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1234,7 +1195,7 @@ fn test_orphaned_blocks_pruned_after_finalization() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1260,24 +1221,16 @@ fn test_orphaned_blocks_pruned_after_finalization() { let orphan_digest = orphan_block.digest(); // Send the orphan first (goes to orphaned_blocks) - mailbox - .report(Update::NotarizedBlock(orphan_block.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(orphan_block.clone())); context.sleep(Duration::from_millis(50)).await; // Finalize blocks 1, 2, 3 on the canonical chain let (ack1, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack1)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack1)); let (ack2, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block2.clone(), None), ack2)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block2.clone(), None), ack2)); let (ack3, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block3.clone(), None), ack3)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block3.clone(), None), ack3)); context.sleep(Duration::from_millis(100)).await; // Canonical height should be 3 @@ -1313,7 +1266,7 @@ fn test_fork_state_reused_when_notarized_then_finalized() { let genesis_hash = [0x47u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1348,7 +1301,7 @@ fn test_fork_state_reused_when_notarized_then_finalized() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1364,7 +1317,7 @@ fn test_fork_state_reused_when_notarized_then_finalized() { let block1_digest = block1.digest(); // Step 1: Send as notarized - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(100)).await; // Step 2: Verify it's in fork_states @@ -1383,9 +1336,7 @@ fn test_fork_state_reused_when_notarized_then_finalized() { // Step 3: Now finalize the same block let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; // Step 4: Verify block1 is now canonical @@ -1423,7 +1374,7 @@ fn test_competing_fork_pruned_on_finalization() { let genesis_hash = [0x48u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1458,7 +1409,7 @@ fn test_competing_fork_pruned_on_finalization() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1482,12 +1433,8 @@ fn test_competing_fork_pruned_on_finalization() { ); // Both notarized - mailbox - .report(Update::NotarizedBlock(block1a.clone())) - .await; - mailbox - .report(Update::NotarizedBlock(block1b.clone())) - .await; + let _ = mailbox.report(Update::NotarizedBlock(block1a.clone())); + let _ = mailbox.report(Update::NotarizedBlock(block1b.clone())); context.sleep(Duration::from_millis(100)).await; // Both should be in fork_states @@ -1498,9 +1445,7 @@ fn test_competing_fork_pruned_on_finalization() { // Finalize block1a let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1a.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1a.clone(), None), ack)); context.sleep(Duration::from_millis(100)).await; // block1a should be canonical @@ -1537,7 +1482,7 @@ fn test_finalized_epoch_mismatch_rejected_before_el_adoption() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1575,7 +1520,7 @@ fn test_finalized_epoch_mismatch_rejected_before_el_adoption() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1591,9 +1536,7 @@ fn test_finalized_epoch_mismatch_rejected_before_el_adoption() { assert_eq!(bad.epoch(), 1, "test block must declare a mismatched epoch"); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((bad, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((bad, None), ack)); context.sleep(Duration::from_millis(300)).await; assert!( diff --git a/finalizer/src/tests/mocks.rs b/finalizer/src/tests/mocks.rs index 05cce39d..804e9bd3 100644 --- a/finalizer/src/tests/mocks.rs +++ b/finalizer/src/tests/mocks.rs @@ -6,6 +6,7 @@ use alloy_rpc_types_engine::{ ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus, PayloadStatusEnum, }; +use commonware_actor::Feedback; use commonware_consensus::simplex::scheme::bls12381_multisig; use commonware_consensus::simplex::types::{Finalization, Finalize, Proposal}; use commonware_consensus::types::{Epoch, Round, View}; @@ -282,7 +283,9 @@ impl NetworkOracle for MockNetworkOracle { impl commonware_p2p::Blocker for MockNetworkOracle { type PublicKey = PublicKey; - async fn block(&mut self, _public_key: Self::PublicKey) {} + fn block(&mut self, _public_key: Self::PublicKey) -> Feedback { + Feedback::Ok + } } /// A single recorded `track` call: the epoch and the peer tiers handed to the @@ -320,5 +323,7 @@ impl NetworkOracle for RecordingNetworkOracle { impl commonware_p2p::Blocker for RecordingNetworkOracle { type PublicKey = PublicKey; - async fn block(&mut self, _public_key: Self::PublicKey) {} + fn block(&mut self, _public_key: Self::PublicKey) -> Feedback { + Feedback::Ok + } } diff --git a/finalizer/src/tests/state_queries.rs b/finalizer/src/tests/state_queries.rs index b0ff938f..70c10949 100644 --- a/finalizer/src/tests/state_queries.rs +++ b/finalizer/src/tests/state_queries.rs @@ -12,9 +12,10 @@ use commonware_consensus::types::Epoch; use commonware_cryptography::bls12381::primitives::variant::MinPk; use commonware_cryptography::{Signer as _, bls12381, ed25519}; use commonware_math::algebra::Random; +use commonware_runtime::Supervisor as _; use commonware_runtime::buffer::paged::CacheRef; use commonware_runtime::deterministic::{self, Runner}; -use commonware_runtime::{Clock, Metrics, Runner as _}; +use commonware_runtime::{Clock, Runner as _}; use commonware_utils::NZUsize; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; use futures::channel::mpsc as futures_mpsc; @@ -185,7 +186,7 @@ fn test_generate_state_proof_preserves_batch_cardinality_for_missing_keys() { let genesis_hash = [0x56u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -220,7 +221,7 @@ fn test_generate_state_proof_preserves_batch_cardinality_for_missing_keys() { let (finalizer, _state, mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -280,7 +281,7 @@ fn test_get_latest_epoch() { let genesis_hash = [0x51u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -315,7 +316,7 @@ fn test_get_latest_epoch() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -341,9 +342,7 @@ fn test_get_latest_epoch() { parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -364,9 +363,7 @@ fn test_get_latest_epoch() { let block4_digest = block4.digest(); let finalization4 = make_finalization(block4_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(100)).await; // Now should be epoch 1 @@ -428,11 +425,11 @@ fn test_epoch_boundary_resets_persisted_view() { _variant_marker: PhantomData, }; - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -448,9 +445,7 @@ fn test_epoch_boundary_resets_persisted_view() { create_test_block_with_epoch(parent_digest, height, height + 1, 20000 + height, 0); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -461,9 +456,7 @@ fn test_epoch_boundary_resets_persisted_view() { let block4_digest = block4.digest(); let finalization4 = make_finalization(block4_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!( @@ -508,7 +501,7 @@ fn test_epoch_boundary_resets_persisted_view() { }; let (_finalizer2, reloaded_state, _mailbox2, _state_query2) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_reloaded"), + context.child("finalizer_reloaded"), reload_cfg, ) .await; @@ -541,7 +534,7 @@ fn test_first_post_epoch_boundary_aux_data_uses_post_transition_state_root() { let initial_state = create_test_initial_state(genesis_hash, epoch_length); let mut expected_state = initial_state.clone(); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -576,7 +569,7 @@ fn test_first_post_epoch_boundary_aux_data_uses_post_transition_state_root() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -594,9 +587,7 @@ fn test_first_post_epoch_boundary_aux_data_uses_post_transition_state_root() { mirror_empty_block_execution_for_root(&mut expected_state, &block); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -617,12 +608,10 @@ fn test_first_post_epoch_boundary_aux_data_uses_post_transition_state_root() { let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock( - (boundary_block, Some(finalization)), - ack, - )) - .await; + let _ = mailbox.report(Update::FinalizedBlock( + (boundary_block, Some(finalization)), + ack, + )); context.sleep(Duration::from_millis(100)).await; assert_eq!( @@ -672,7 +661,7 @@ fn test_epoch_boundary_post_transition_root_survives_restart() { std::num::NonZero::new(4096).unwrap(), NZUsize!(100), ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let finalizer_cfg = FinalizerConfig:: { @@ -700,7 +689,7 @@ fn test_epoch_boundary_post_transition_root_survives_restart() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -715,9 +704,7 @@ fn test_epoch_boundary_post_transition_root_survives_restart() { create_test_block_with_epoch(parent_digest, height, height + 1, 60000 + height, 0); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -727,9 +714,7 @@ fn test_epoch_boundary_post_transition_root_survives_restart() { let boundary_digest = boundary.digest(); let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); context.sleep(Duration::from_millis(100)).await; // Live post-transition root advertised to the first block of the new epoch. @@ -749,7 +734,7 @@ fn test_epoch_boundary_post_transition_root_survives_restart() { // same post-transition root, proving it was persisted post re-capture. let (restarted, reloaded_state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_restart"), + context.child("finalizer_restart"), FinalizerConfig { mailbox_size: 100, db_prefix, @@ -805,7 +790,7 @@ fn test_get_epoch_genesis_hash() { let genesis_hash = [0x53u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -840,7 +825,7 @@ fn test_get_epoch_genesis_hash() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -865,9 +850,7 @@ fn test_get_epoch_genesis_hash() { parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -881,9 +864,7 @@ fn test_get_epoch_genesis_hash() { let block4_digest = block4.digest(); let finalization4 = make_finalization(block4_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(100)).await; // Now in epoch 1, the epoch genesis hash should be block4's digest @@ -909,7 +890,7 @@ fn test_get_epoch_genesis_hash_for_past_epoch() { let genesis_hash = [0x57u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -942,7 +923,7 @@ fn test_get_epoch_genesis_hash_for_past_epoch() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -960,18 +941,14 @@ fn test_get_epoch_genesis_hash_for_past_epoch() { create_test_block_with_epoch(parent_digest, height, height + 1, 13000 + height, 0); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } let block4 = create_test_block_with_epoch(parent_digest, 4, 5, 13004, 0); let epoch1_genesis = block4.digest(); // genesis of epoch 1 == last block of epoch 0 let finalization4 = make_finalization(epoch1_genesis, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(100)).await; parent_digest = epoch1_genesis; assert_eq!(mailbox.get_latest_epoch().await, 1, "should be epoch 1"); @@ -982,18 +959,14 @@ fn test_get_epoch_genesis_hash_for_past_epoch() { create_test_block_with_epoch(parent_digest, height, height + 1, 13000 + height, 1); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } let block9 = create_test_block_with_epoch(parent_digest, 9, 10, 13009, 1); let epoch2_genesis = block9.digest(); // genesis of epoch 2 == last block of epoch 1 let finalization9 = make_finalization(epoch2_genesis, 9, 8, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block9, Some(finalization9)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block9, Some(finalization9)), ack)); context.sleep(Duration::from_millis(100)).await; assert_eq!(mailbox.get_latest_epoch().await, 2, "should be epoch 2"); @@ -1032,7 +1005,7 @@ fn test_get_epoch_genesis_hash_for_future_epoch() { let genesis_hash = [0x58u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1065,7 +1038,7 @@ fn test_get_epoch_genesis_hash_for_future_epoch() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1095,7 +1068,7 @@ fn test_get_aux_data_from_canonical_chain() { let genesis_hash = [0x54u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1130,7 +1103,7 @@ fn test_get_aux_data_from_canonical_chain() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1171,7 +1144,7 @@ fn test_get_aux_data_returns_none_for_invalid_parent() { let genesis_hash = [0x55u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1206,7 +1179,7 @@ fn test_get_aux_data_returns_none_for_invalid_parent() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; diff --git a/finalizer/src/tests/syncing.rs b/finalizer/src/tests/syncing.rs index bab2aa7c..59dbeae1 100644 --- a/finalizer/src/tests/syncing.rs +++ b/finalizer/src/tests/syncing.rs @@ -11,9 +11,10 @@ use commonware_consensus::Reporter; use commonware_cryptography::bls12381::primitives::variant::MinPk; use commonware_cryptography::{Signer as _, bls12381, ed25519}; use commonware_math::algebra::Random; +use commonware_runtime::Supervisor as _; use commonware_runtime::buffer::paged::CacheRef; use commonware_runtime::deterministic::{self, Runner}; -use commonware_runtime::{Clock, Metrics, Runner as _}; +use commonware_runtime::{Clock, Runner as _}; use commonware_utils::NZUsize; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; use futures::channel::mpsc as futures_mpsc; @@ -167,7 +168,7 @@ fn test_initial_startup_sync_waits_for_valid() { let initial_state = create_checkpoint_initial_state(checkpoint_hash, 5, 0, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -205,7 +206,7 @@ fn test_initial_startup_sync_waits_for_valid() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -220,9 +221,7 @@ fn test_initial_startup_sync_waits_for_valid() { // Height 6, epoch = 6/10 = 0, matches state.epoch let block = create_test_block(checkpoint_hash.into(), 6, 6, 2001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(100)).await; // Verify the block was processed by checking the height advanced @@ -247,7 +246,7 @@ fn test_initial_startup_sync_zero_forkchoice_skips_sync() { let genesis_hash = [0u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -279,7 +278,7 @@ fn test_initial_startup_sync_zero_forkchoice_skips_sync() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -292,9 +291,7 @@ fn test_initial_startup_sync_zero_forkchoice_skips_sync() { let genesis_block = Block::genesis(genesis_hash); let block = create_test_block(genesis_block.digest(), 1, 1, 3001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(100)).await; let height = mailbox.get_latest_height().await; @@ -318,7 +315,7 @@ fn test_execute_block_retries_on_syncing() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -354,7 +351,7 @@ fn test_execute_block_retries_on_syncing() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -366,9 +363,7 @@ fn test_execute_block_retries_on_syncing() { let genesis_block = Block::genesis(genesis_hash); let block1 = create_test_block(genesis_block.digest(), 1, 1, 4001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack)); // With 3 SYNCING retries at 5s each, need ~15s for the retries to complete context.sleep(Duration::from_secs(17)).await; @@ -382,9 +377,7 @@ fn test_execute_block_retries_on_syncing() { // Send a second block to verify the finalizer continues normally let block2 = create_test_block(block1.digest(), 2, 2, 4002); let (ack2, _waiter2) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block2, None), ack2)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block2, None), ack2)); context.sleep(Duration::from_millis(100)).await; let height = mailbox.get_latest_height().await; @@ -408,7 +401,7 @@ fn test_notarized_block_retries_on_syncing() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -444,7 +437,7 @@ fn test_notarized_block_retries_on_syncing() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -456,7 +449,7 @@ fn test_notarized_block_retries_on_syncing() { let genesis_block = Block::genesis(genesis_hash); let block1 = create_test_block(genesis_block.digest(), 1, 1, 5001); let block1_digest = block1.digest(); - mailbox.report(Update::NotarizedBlock(block1)).await; + let _ = mailbox.report(Update::NotarizedBlock(block1)); // Wait for SYNCING retries to complete (2 retries * 5s = 10s) context.sleep(Duration::from_secs(12)).await; @@ -488,7 +481,7 @@ fn test_checkpoint_startup_full_flow() { let initial_state = create_checkpoint_initial_state(checkpoint_hash, 5, 0, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -526,7 +519,7 @@ fn test_checkpoint_startup_full_flow() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -539,9 +532,7 @@ fn test_checkpoint_startup_full_flow() { // Send first block after checkpoint (height 6, epoch 0) let block6 = create_test_block(checkpoint_hash.into(), 6, 6, 6001); let (ack6, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block6.clone(), None), ack6)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block6.clone(), None), ack6)); // Wait for the check_payload SYNCING retry (1 * 5s) context.sleep(Duration::from_secs(7)).await; @@ -555,9 +546,7 @@ fn test_checkpoint_startup_full_flow() { // Send second block — no more SYNCING, should be immediate let block7 = create_test_block(block6.digest(), 7, 7, 6002); let (ack7, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block7.clone(), None), ack7)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block7.clone(), None), ack7)); context.sleep(Duration::from_millis(100)).await; let height = mailbox.get_latest_height().await; @@ -566,9 +555,7 @@ fn test_checkpoint_startup_full_flow() { // Send third block — also immediate let block8 = create_test_block(block7.digest(), 8, 8, 6003); let (ack8, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block8, None), ack8)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block8, None), ack8)); context.sleep(Duration::from_millis(100)).await; let height = mailbox.get_latest_height().await; @@ -610,7 +597,7 @@ fn test_finalizer_mailbox_responsive_under_persistent_syncing() { let genesis_hash = [0xAAu8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -648,7 +635,7 @@ fn test_finalizer_mailbox_responsive_under_persistent_syncing() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -663,9 +650,7 @@ fn test_finalizer_mailbox_responsive_under_persistent_syncing() { let genesis_block = Block::genesis(genesis_hash); let block1 = create_test_block(genesis_block.digest(), 1, 1, 7001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); // Give the finalizer enough virtual time to dequeue the // FinalizedBlock update and enter execute_block's SYNCING loop. @@ -734,7 +719,7 @@ fn test_finalizer_mailbox_responsive_during_startup_syncing() { let initial_state = create_checkpoint_initial_state(checkpoint_hash, 5, 0, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -771,7 +756,7 @@ fn test_finalizer_mailbox_responsive_during_startup_syncing() { let (finalizer, _state, mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -823,7 +808,7 @@ fn test_finalizer_shuts_down_when_pending_notarized_cap_is_reached() { let genesis_hash = [0xEFu8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -857,7 +842,7 @@ fn test_finalizer_shuts_down_when_pending_notarized_cap_is_reached() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -870,9 +855,9 @@ fn test_finalizer_shuts_down_when_pending_notarized_cap_is_reached() { let block_b = create_test_block(genesis_block.digest(), 1, 1, 17002); let block_c = create_test_block(genesis_block.digest(), 1, 1, 17003); - mailbox.report(Update::NotarizedBlock(block_a)).await; - mailbox.report(Update::NotarizedBlock(block_b)).await; - mailbox.report(Update::NotarizedBlock(block_c)).await; + let _ = mailbox.report(Update::NotarizedBlock(block_a)); + let _ = mailbox.report(Update::NotarizedBlock(block_b)); + let _ = mailbox.report(Update::NotarizedBlock(block_c)); context.sleep(Duration::from_millis(50)).await; @@ -922,7 +907,7 @@ fn test_finalizer_finalized_buffer_drains_in_order() { let genesis_hash = [0u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -961,7 +946,7 @@ fn test_finalizer_finalized_buffer_drains_in_order() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -976,17 +961,13 @@ fn test_finalizer_finalized_buffer_drains_in_order() { let block_b = create_test_block(block_a.digest(), 2, 2, 13002); let (ack_a, _waiter_a) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block_a, None), ack_a)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block_a, None), ack_a)); // Small gap so A's mailbox path runs (and buffers) before B arrives. context.sleep(Duration::from_millis(10)).await; let (ack_b, _waiter_b) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block_b, None), ack_b)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block_b, None), ack_b)); // Give the drain timer multiple ticks to exhaust the queued SYNCING // responses and apply both blocks. Three SYNCING responses at 50ms @@ -1022,7 +1003,7 @@ fn test_duplicate_finalized_delivery_is_idempotent() { let genesis_hash = [0u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1057,7 +1038,7 @@ fn test_duplicate_finalized_delivery_is_idempotent() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1069,9 +1050,7 @@ fn test_duplicate_finalized_delivery_is_idempotent() { // First (legitimate) finalized delivery: the block is applied and executed once. let (ack1, _waiter1) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack1)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack1)); context.sleep(Duration::from_millis(200)).await; assert_eq!( mailbox.get_latest_height().await, @@ -1086,9 +1065,7 @@ fn test_duplicate_finalized_delivery_is_idempotent() { // Duplicate finalized delivery of the SAME block (at-least-once contract). let (ack2, waiter2) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1.clone(), None), ack2)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1.clone(), None), ack2)); context.sleep(Duration::from_millis(200)).await; // The duplicate must be acknowledged so the syncer's pending-ack pipeline does not @@ -1128,7 +1105,7 @@ fn test_finalized_commit_hash_syncing_buffers_and_retries() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1162,7 +1139,7 @@ fn test_finalized_commit_hash_syncing_buffers_and_retries() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1172,9 +1149,7 @@ fn test_finalized_commit_hash_syncing_buffers_and_retries() { let genesis_block = Block::genesis(genesis_hash); let block1 = create_test_block(genesis_block.digest(), 1, 1, 4001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); // Must NOT advance while the forkchoice is still SYNCING. context.sleep(Duration::from_millis(50)).await; @@ -1207,7 +1182,7 @@ fn test_finalized_commit_hash_invalid_shuts_down() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1244,7 +1219,7 @@ fn test_finalized_commit_hash_invalid_shuts_down() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1254,9 +1229,7 @@ fn test_finalized_commit_hash_invalid_shuts_down() { let genesis_block = Block::genesis(genesis_hash); let block1 = create_test_block(genesis_block.digest(), 1, 1, 4001); let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); context.sleep(Duration::from_millis(500)).await; assert!( @@ -1278,7 +1251,7 @@ fn test_notarized_commit_hash_invalid_discards_fork() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1315,7 +1288,7 @@ fn test_notarized_commit_hash_invalid_discards_fork() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1326,7 +1299,7 @@ fn test_notarized_commit_hash_invalid_discards_fork() { let block1 = create_test_block(genesis_block.digest(), 1, 1, 4001); // Notarized block whose forkchoice update is INVALID → fork discarded. - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(500)).await; assert!( !token.is_cancelled(), @@ -1336,9 +1309,7 @@ fn test_notarized_commit_hash_invalid_discards_fork() { // The finalizer must keep working: finalize the same block (commit_hash now // VALID) and confirm it advances. let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); context.sleep(Duration::from_millis(500)).await; assert_eq!( mailbox.get_latest_height().await, @@ -1367,7 +1338,7 @@ fn test_finalized_reuse_path_commits_finalized_forkchoice_and_shuts_down_on_inva let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1404,7 +1375,7 @@ fn test_finalized_reuse_path_commits_finalized_forkchoice_and_shuts_down_on_inva let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1416,7 +1387,7 @@ fn test_finalized_reuse_path_commits_finalized_forkchoice_and_shuts_down_on_inva let block1_digest = block1.digest(); // Notarize first → block lands in fork_states (forkchoice VALID). - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(200)).await; let notify = mailbox.notify_at_height(1, block1_digest).await; assert!( @@ -1431,9 +1402,7 @@ fn test_finalized_reuse_path_commits_finalized_forkchoice_and_shuts_down_on_inva // Finalize the same block → reuse path sends the finalized forkchoice, which // the EL rejects as INVALID → fatal shutdown. let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); context.sleep(Duration::from_millis(500)).await; assert!( @@ -1456,7 +1425,7 @@ fn test_finalized_reuse_path_buffers_on_syncing() { let genesis_hash = [0x42u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(10).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let node_key = ed25519::PrivateKey::from_seed(0); @@ -1492,7 +1461,7 @@ fn test_finalized_reuse_path_buffers_on_syncing() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1505,7 +1474,7 @@ fn test_finalized_reuse_path_buffers_on_syncing() { // Notarize first → block lands in fork_states (forkchoice VALID). Notarization // does not advance the finalized height. - mailbox.report(Update::NotarizedBlock(block1.clone())).await; + let _ = mailbox.report(Update::NotarizedBlock(block1.clone())); context.sleep(Duration::from_millis(200)).await; let notify = mailbox.notify_at_height(1, block1_digest).await; assert!( @@ -1520,9 +1489,7 @@ fn test_finalized_reuse_path_buffers_on_syncing() { // Finalize the same block → reuse path forkchoice is SYNCING: must buffer. let (ack, _waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block1, None), ack)); context.sleep(Duration::from_millis(50)).await; assert_eq!( mailbox.get_latest_height().await, diff --git a/finalizer/src/tests/validator_lifecycle.rs b/finalizer/src/tests/validator_lifecycle.rs index af1a08a8..bd597a98 100644 --- a/finalizer/src/tests/validator_lifecycle.rs +++ b/finalizer/src/tests/validator_lifecycle.rs @@ -14,9 +14,10 @@ use commonware_consensus::Reporter; use commonware_cryptography::bls12381::primitives::variant::MinPk; use commonware_cryptography::{Signer as _, bls12381, ed25519}; use commonware_math::algebra::Random; +use commonware_runtime::Supervisor as _; use commonware_runtime::buffer::paged::CacheRef; use commonware_runtime::deterministic::{self, Runner}; -use commonware_runtime::{Clock, Metrics, Runner as _}; +use commonware_runtime::{Clock, Runner as _}; use commonware_utils::NZUsize; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; use futures::{StreamExt as _, channel::mpsc as futures_mpsc}; @@ -228,7 +229,7 @@ fn test_checkpoint_restart_keeps_submitted_exit_request_validator_in_current_epo initial_state.set_account(exiting_pubkey_bytes, exiting_account); initial_state.push_removed_validator(exiting_node_pubkey.clone()); - let (orchestrator_tx, mut orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, mut orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let finalizer_cfg = FinalizerConfig:: { @@ -260,7 +261,7 @@ fn test_checkpoint_restart_keeps_submitted_exit_request_validator_in_current_epo let (finalizer, _state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -317,7 +318,7 @@ fn test_validator_exit_triggers_cancellation() { create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); initial_state.push_removed_validator(node_pubkey.clone()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); @@ -353,7 +354,7 @@ fn test_validator_exit_triggers_cancellation() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -381,9 +382,7 @@ fn test_validator_exit_triggers_cancellation() { parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -401,9 +400,7 @@ fn test_validator_exit_triggers_cancellation() { parent_digest = block4_digest; let finalization4 = make_finalization(block4_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(100)).await; // Token still should not be cancelled (we're at block 4, not first of new epoch) @@ -416,9 +413,7 @@ fn test_validator_exit_triggers_cancellation() { // This should trigger the cancellation let block5 = create_test_block_with_epoch(parent_digest, 5, 6, 13005, 1); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block5, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block5, None), ack)); context.sleep(Duration::from_millis(100)).await; // Now the token should be cancelled @@ -449,7 +444,7 @@ fn test_finalizer_rejects_finalized_block_with_wrong_parent() { let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); @@ -484,7 +479,7 @@ fn test_finalizer_rejects_finalized_block_with_wrong_parent() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -500,9 +495,7 @@ fn test_finalizer_rejects_finalized_block_with_wrong_parent() { create_test_block_with_epoch(parent_digest, height, height + 1, 13000 + height, 0); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } assert!( @@ -518,9 +511,7 @@ fn test_finalizer_rejects_finalized_block_with_wrong_parent() { assert_ne!(wrong_parent, parent_digest); let bad_block = create_test_block_with_epoch(wrong_parent, 3, 4, 13003, 0); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((bad_block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((bad_block, None), ack)); context.sleep(Duration::from_millis(150)).await; assert!( @@ -554,7 +545,7 @@ fn test_finalizer_rejects_block_certificate_digest_mismatch() { // that can cancel the token is the digest-binding guard. let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); @@ -595,7 +586,7 @@ fn test_finalizer_rejects_block_certificate_digest_mismatch() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -614,9 +605,7 @@ fn test_finalizer_rejects_block_certificate_digest_mismatch() { create_test_block_with_epoch(parent_digest, height, height + 1, 13000 + height, 0); parent_digest = block.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); context.sleep(Duration::from_millis(50)).await; } assert!( @@ -639,12 +628,10 @@ fn test_finalizer_rejects_block_certificate_digest_mismatch() { assert_ne!(block4.digest(), wrong_digest); let mismatched_finalization = make_finalization(wrong_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock( - (block4, Some(mismatched_finalization)), - ack, - )) - .await; + let _ = mailbox.report(Update::FinalizedBlock( + (block4, Some(mismatched_finalization)), + ack, + )); context.sleep(Duration::from_millis(150)).await; assert!( @@ -718,7 +705,7 @@ fn test_joining_validator_peer_tier_follows_activation() { let oracle = RecordingNetworkOracle::new(); let track_calls = oracle.calls.clone(); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let finalizer_cfg = FinalizerConfig:: { @@ -754,7 +741,7 @@ fn test_joining_validator_peer_tier_follows_activation() { RecordingNetworkOracle, ed25519::PrivateKey, MinPk, - >::new(context.with_label("finalizer"), finalizer_cfg) + >::new(context.child("finalizer"), finalizer_cfg) .await; let _handle = finalizer.start(orchestrator_mailbox); @@ -784,9 +771,7 @@ fn test_joining_validator_peer_tier_follows_activation() { let finalization = (height % 5 == 4) .then(|| make_finalization(block_digest, height, height + 1, &schemes, quorum)); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, finalization), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, finalization), ack)); context.sleep(Duration::from_millis(50)).await; } @@ -869,7 +854,7 @@ fn epoch_transition_deltas_are_cleared_before_persisted_state_ack() { std::num::NonZero::new(4096).unwrap(), NZUsize!(100), ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); @@ -898,7 +883,7 @@ fn epoch_transition_deltas_are_cleared_before_persisted_state_ack() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -913,9 +898,7 @@ fn epoch_transition_deltas_are_cleared_before_persisted_state_ack() { create_test_block_with_epoch(parent_digest, height, height + 1, 58000 + height, 0); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("non-boundary block must be acked"); } @@ -925,9 +908,7 @@ fn epoch_transition_deltas_are_cleared_before_persisted_state_ack() { let boundary_digest = boundary.digest(); let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); ack_waiter.await.expect("epoch boundary block must be acked"); drop(mailbox); @@ -936,7 +917,7 @@ fn epoch_transition_deltas_are_cleared_before_persisted_state_ack() { let (restarted, reloaded_state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_restart"), + context.child("finalizer_restart"), FinalizerConfig { mailbox_size: 100, db_prefix, @@ -1042,7 +1023,7 @@ fn epoch_boundary_commit_failure_withholds_ack_and_shuts_down() { std::num::NonZero::new(4096).unwrap(), NZUsize!(100), ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); @@ -1076,7 +1057,7 @@ fn epoch_boundary_commit_failure_withholds_ack_and_shuts_down() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1092,9 +1073,7 @@ fn epoch_boundary_commit_failure_withholds_ack_and_shuts_down() { create_test_block_with_epoch(parent_digest, height, height + 1, 59000 + height, 0); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("non-boundary block must be acked"); } @@ -1107,9 +1086,7 @@ fn epoch_boundary_commit_failure_withholds_ack_and_shuts_down() { let boundary_digest = boundary.digest(); let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); // Ack must be WITHHELD: the finalizer errors on the failed commit before // acknowledging, so the Exact waiter resolves Err (sender dropped). @@ -1132,7 +1109,7 @@ fn epoch_boundary_commit_failure_withholds_ack_and_shuts_down() { // Restart from the same DB: the epoch must NOT have durably advanced. let (restarted, reloaded_state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_restart"), + context.child("finalizer_restart"), FinalizerConfig { mailbox_size: 100, db_prefix, @@ -1244,7 +1221,7 @@ fn joining_validator_withdrawal_excludes_it_from_oracle_tracking() { let oracle = RecordingOracle::default(); let tracks = oracle.tracks.clone(); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let finalizer_cfg = FinalizerConfig:: { @@ -1276,7 +1253,7 @@ fn joining_validator_withdrawal_excludes_it_from_oracle_tracking() { let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, RecordingOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -1305,9 +1282,7 @@ fn joining_validator_withdrawal_excludes_it_from_oracle_tracking() { ); parent_digest = b1.digest(); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((b1, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((b1, None), ack)); context.sleep(Duration::from_millis(30)).await; // Blocks 2-3: empty filler to reach the last block of epoch 0. @@ -1316,7 +1291,7 @@ fn joining_validator_withdrawal_excludes_it_from_oracle_tracking() { create_test_block_with_epoch(parent_digest, height, height + 1, 19000 + height, 0); parent_digest = b.digest(); let (ack, _) = Exact::handle(); - mailbox.report(Update::FinalizedBlock((b, None), ack)).await; + let _ = mailbox.report(Update::FinalizedBlock((b, None), ack)); context.sleep(Duration::from_millis(30)).await; } @@ -1326,9 +1301,7 @@ fn joining_validator_withdrawal_excludes_it_from_oracle_tracking() { let b4_digest = b4.digest(); let finalization4 = make_finalization(b4_digest, 4, 3, &schemes, quorum); let (ack, _) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((b4, Some(finalization4)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((b4, Some(finalization4)), ack)); context.sleep(Duration::from_millis(50)).await; // The canceled joining validator's account must leave `Joining` (to @@ -1430,12 +1403,12 @@ fn restart_mid_warmup_preserves_pending_joining_validator() { std::num::NonZero::new(4096).unwrap(), NZUsize!(100), ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg( &db_prefix, page_cache.clone(), @@ -1458,9 +1431,7 @@ fn restart_mid_warmup_preserves_pending_joining_validator() { create_test_block_with_epoch(parent_digest, height, height + 1, 77000 + height, 0); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("non-boundary block must be acked"); } @@ -1470,9 +1441,7 @@ fn restart_mid_warmup_preserves_pending_joining_validator() { let boundary_digest = boundary.digest(); let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); ack_waiter .await .expect("epoch boundary block must be acked"); @@ -1485,7 +1454,7 @@ fn restart_mid_warmup_preserves_pending_joining_validator() { let (restarted, reloaded_state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_restart"), + context.child("finalizer_restart"), finalizer_cfg( &db_prefix, page_cache, @@ -1595,12 +1564,12 @@ fn restart_preserves_pending_full_exit_payout() { std::num::NonZero::new(4096).unwrap(), NZUsize!(100), ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg( &db_prefix, page_cache.clone(), @@ -1624,9 +1593,7 @@ fn restart_preserves_pending_full_exit_payout() { create_test_block_with_epoch(parent_digest, height, height + 1, 88000 + height, 0); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("non-boundary block must be acked"); } @@ -1636,9 +1603,7 @@ fn restart_preserves_pending_full_exit_payout() { let boundary_digest = boundary.digest(); let finalization = make_finalization(boundary_digest, 4, 3, &schemes, quorum); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); ack_waiter .await .expect("epoch boundary block must be acked"); @@ -1650,7 +1615,7 @@ fn restart_preserves_pending_full_exit_payout() { let (restarted, reloaded_state, _mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer_restart"), + context.child("finalizer_restart"), finalizer_cfg( &db_prefix, page_cache, diff --git a/finalizer/src/tests/withdrawals.rs b/finalizer/src/tests/withdrawals.rs index 69afff1f..1d3be4e6 100644 --- a/finalizer/src/tests/withdrawals.rs +++ b/finalizer/src/tests/withdrawals.rs @@ -21,9 +21,10 @@ use commonware_consensus::Reporter; use commonware_cryptography::bls12381::primitives::variant::MinPk; use commonware_cryptography::{Signer as _, bls12381, ed25519}; use commonware_math::algebra::Random; +use commonware_runtime::Supervisor as _; use commonware_runtime::buffer::paged::CacheRef; use commonware_runtime::deterministic::{self, Runner}; -use commonware_runtime::{Clock, Metrics, Runner as _}; +use commonware_runtime::{Clock, Runner as _}; use commonware_utils::NZUsize; use commonware_utils::acknowledgement::{Acknowledgement, Exact}; use futures::channel::mpsc as futures_mpsc; @@ -199,7 +200,7 @@ fn finalized_non_terminal_block_with_withdrawals_is_fatal() { let genesis_hash = [0x61u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); let engine_client = MockEngineClient::new(); @@ -219,7 +220,7 @@ fn finalized_non_terminal_block_with_withdrawals_is_fatal() { ); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -240,9 +241,7 @@ fn finalized_non_terminal_block_with_withdrawals_is_fatal() { let commits_before = engine_client.commit_hash_call_count(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); // Fail stop: the ack is withheld and the finalizer shuts down. assert!( @@ -279,7 +278,7 @@ fn finalized_terminal_block_with_tampered_withdrawals_is_fatal() { let genesis_hash = [0x62u8; 32]; let initial_state = create_test_initial_state(genesis_hash, NonZeroU64::new(5).unwrap()); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); let engine_client = MockEngineClient::new(); @@ -299,7 +298,7 @@ fn finalized_terminal_block_with_tampered_withdrawals_is_fatal() { ); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -320,9 +319,7 @@ fn finalized_terminal_block_with_tampered_withdrawals_is_fatal() { ); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("clean block must be acked"); } @@ -341,9 +338,7 @@ fn finalized_terminal_block_with_tampered_withdrawals_is_fatal() { let commits_before = engine_client.commit_hash_call_count(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); assert!( ack_waiter.await.is_err(), @@ -405,7 +400,7 @@ fn finalized_terminal_block_with_matching_withdrawals_applies() { "sanity: the partial should be emitted in full" ); - let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::channel(100); + let (orchestrator_tx, _orchestrator_rx) = futures_mpsc::unbounded(); let orchestrator_mailbox = summit_orchestrator::Mailbox::new(orchestrator_tx); let cancellation_token = CancellationToken::new(); let engine_client = MockEngineClient::new(); @@ -425,7 +420,7 @@ fn finalized_terminal_block_with_matching_withdrawals_applies() { ); let (finalizer, _state, mut mailbox, _state_query) = Finalizer::<_, MockEngineClient, MockNetworkOracle, ed25519::PrivateKey, MinPk>::new( - context.with_label("finalizer"), + context.child("finalizer"), finalizer_cfg, ) .await; @@ -445,9 +440,7 @@ fn finalized_terminal_block_with_matching_withdrawals_applies() { ); parent_digest = block.digest(); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((block, None), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((block, None), ack)); ack_waiter.await.expect("clean block must be acked"); } @@ -457,9 +450,7 @@ fn finalized_terminal_block_with_matching_withdrawals_applies() { create_test_block_with_withdrawals(parent_digest, 4, 5, 63004, 0, expected_payouts); let finalization = make_finalization(boundary.digest(), 4, 3, &schemes, 3); let (ack, ack_waiter) = Exact::handle(); - mailbox - .report(Update::FinalizedBlock((boundary, Some(finalization)), ack)) - .await; + let _ = mailbox.report(Update::FinalizedBlock((boundary, Some(finalization)), ack)); ack_waiter .await .expect("terminal block with matching withdrawals must be acked"); diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 37ebc388..9f038ef1 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -245,7 +245,7 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "foldhash 0.2.0", + "foldhash", "hashbrown 0.16.1", "indexmap 2.14.0", "itoa", @@ -293,7 +293,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.13.2", + "reqwest", "serde", "serde_json", "thiserror 2.0.18", @@ -361,7 +361,7 @@ dependencies = [ "alloy-transport-ipc", "futures", "pin-project", - "reqwest 0.13.2", + "reqwest", "serde", "serde_json", "tokio", @@ -445,7 +445,7 @@ dependencies = [ "async-trait", "auto_impl", "either", - "elliptic-curve", + "elliptic-curve 0.13.8", "k256", "thiserror 2.0.18", ] @@ -556,7 +556,7 @@ dependencies = [ "hyper-tls", "hyper-util", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest", "serde_json", "tower", "tracing", @@ -988,6 +988,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" @@ -1072,20 +1078,20 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -1213,6 +1219,17 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chacha20poly1305" version = "0.10.1" @@ -1220,7 +1237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1271,7 +1288,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -1312,9 +1329,15 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.0-pre.0" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d5ce5728ecb5285a5dd35f02a6a8e34e0828e0b38e8e632e249a3fe3f320211" + +[[package]] +name = "cmov" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417da527aa9bf6a1e10a781231effd1edd3ee82f27d5f8529ac9b279babce96" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "combine" @@ -1326,43 +1349,69 @@ dependencies = [ "memchr", ] +[[package]] +name = "commonware-actor" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beac6262223b45c6843ab70b245b526b28836a8257b1759a509ba5fab24fa4a6" +dependencies = [ + "cfg-if", + "commonware-macros", + "commonware-runtime", + "crossbeam-queue", + "futures-util", + "parking_lot", +] + [[package]] name = "commonware-broadcast" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe7362c8942f20f0eab11756932b7d1c41f4cc99e142cb563e17a04b40095d5" +checksum = "3a2d7e56dc894ad3f55dbe5726468915492ae8e4650ad2a45c1bcfd138c2037f" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", "commonware-p2p", "commonware-runtime", "commonware-utils", - "prometheus-client", "thiserror 2.0.18", "tracing", ] [[package]] name = "commonware-codec" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f06e32817f35fb517ceb6102d984f9a85fde85666c96f053638e323b8597f2f7" +checksum = "e8eb7efe071cea13e8b23a0a7f584398870c401d2b2aabfb6b9ab3634ebcba3a" dependencies = [ "bytes", "cfg-if", + "commonware-codec-macros", "commonware-macros", "paste", - "rand 0.8.6", - "rand_chacha 0.3.1", + "rand_chacha 0.10.0", "thiserror 2.0.18", ] +[[package]] +name = "commonware-codec-macros" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a876bec347344381948901d0f5ec2e9443ebc8dbab99a63bc2db78c2fc2efbdd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "commonware-coding" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e60b2b324de47773c3d4af4d83bfc76d2c287ba7f2d6eb8c2aa5068f877b4bb" +checksum = "409e9997c1bb3273b9570c26ddef4f252327c9bf18968d4439a83cfd1d3e5661" dependencies = [ "bytes", "commonware-codec", @@ -1373,25 +1422,24 @@ dependencies = [ "commonware-storage", "commonware-utils", "num-rational", - "rand 0.8.6", - "rand_core 0.6.4", "rayon", - "reed-solomon-simd", "thiserror 2.0.18", ] [[package]] name = "commonware-consensus" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a67374d82c69e870105f010b895f1768952df5d0fa0d0550dedf162de16f44e" +checksum = "acf15e3402e26f062695d4dd5e4b5967f601dc7303d06b9fafdb20a6485a3a0a" dependencies = [ "bytes", "cfg-if", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-coding", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-p2p", @@ -1402,10 +1450,8 @@ dependencies = [ "commonware-utils", "futures", "pin-project", - "prometheus-client", - "rand 0.8.6", - "rand_core 0.6.4", - "rand_distr", + "rand 0.10.2", + "rand_core 0.10.1", "rayon", "thiserror 2.0.18", "tracing", @@ -1413,10 +1459,11 @@ dependencies = [ [[package]] name = "commonware-cryptography" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f09b55dd5510c3b7613a573606a41961c2788709ffde053d4e644bec0bff2c" +checksum = "b842f31e1aaa1af284a1f9dc4f1234761f72d07e70d53cf3f1521b0870ade094" dependencies = [ + "ahash", "anyhow", "aws-lc-rs", "blake3", @@ -1425,32 +1472,48 @@ dependencies = [ "cfg-if", "chacha20poly1305", "commonware-codec", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-parallel", "commonware-utils", + "cpufeatures 0.2.17", "crc-fast", - "ctutils", - "ecdsa", - "ed25519-consensus", + "ctutils 0.3.1", + "curve25519-dalek 5.0.0", + "ecdsa 0.17.0", + "fixedbitset", "getrandom 0.2.17", + "getrandom 0.4.3", + "hashbrown 0.16.1", "num-rational", "num-traits", + "once_cell", "p256", - "rand 0.8.6", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "sha2 0.10.9", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rand_core 0.10.1", + "sha2 0.11.0", "thiserror 2.0.18", "x25519-dalek", "zeroize", ] +[[package]] +name = "commonware-formatting" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab01b7f2798e29b0a4f7b47da5b7f3c106881578af3e69e8d2ee9d89d7c1d4fd" +dependencies = [ + "commonware-macros", + "const-hex", +] + [[package]] name = "commonware-macros" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd313d9299e13bf995999c7a0ed8cc570eef6cd0972fcffc6e2c682cfba6663" +checksum = "39e37a17d5c99ec6711098c6614af1023fd32926aa0414ec0f0613f6bb940278" dependencies = [ "commonware-macros-impl", "tokio", @@ -1458,9 +1521,9 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc385e646d91b5397c93816985421878d627839834f7cf85a8da2ac9f8b98b7" +checksum = "7d44bceee874226efca172f0e9ec9ec4eb486788ba6be1d98710f3f159b807af" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1471,24 +1534,25 @@ dependencies = [ [[package]] name = "commonware-math" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d834ed8bf601e113b9cd2ba284dd0e95adf558933dc727f52f8879434cb286" +checksum = "593fa0c5aa8bd8d350724b9b1ad4e85072845aa85a06b39ca4598998bbec1e00" dependencies = [ "bytes", "commonware-codec", "commonware-macros", "commonware-parallel", "commonware-utils", - "rand_core 0.6.4", + "rand_core 0.10.1", ] [[package]] name = "commonware-p2p" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c93f730bf4aaeadffb589eb50e431f7a5f8495c158dda1127c61f6e74c597ab" +checksum = "47f974fcd27c0dad7ae73caeaf00c8330fd29c60f5ff1f679ccc1a51c8ed95bc" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -1502,9 +1566,8 @@ dependencies = [ "num-integer", "num-rational", "num-traits", - "prometheus-client", - "rand 0.8.6", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "rand_distr", "thiserror 2.0.18", "tracing", @@ -1512,22 +1575,25 @@ dependencies = [ [[package]] name = "commonware-parallel" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db29306a40279ad54d06b42c623a05fbb5333546b5003c921796bc856b423106" +checksum = "7109817274c671f1fb0ba17cf8aecfbb70672d29379284feeac43fa1b1dc0201" dependencies = [ "cfg-if", "commonware-macros", + "dashmap", + "futures", "rayon", ] [[package]] name = "commonware-resolver" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dfe9932b33cc31a04b7c68bf543eef7e6d04b70cf6a53880d03407d60a01e6" +checksum = "1a497cc9e9a2cd205d96474d0d40c7731f29085cd467adbe9ff0f94719659582" dependencies = [ "bytes", + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -1536,40 +1602,44 @@ dependencies = [ "commonware-stream", "commonware-utils", "futures", - "prometheus-client", - "rand 0.8.6", + "rand 0.10.2", + "rand_core 0.10.1", "thiserror 2.0.18", "tracing", ] [[package]] name = "commonware-runtime" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ae4c804d0d9c1df615b1c7846e4e5e64fdb4228685487cb67803e67388411" +checksum = "2a84bc6cefb099b1ffce3e04705a1e61e91d895b9f09b9e1be14c8ec5823c181" dependencies = [ + "ahash", "axum", "bytes", "cfg-if", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-parallel", + "commonware-runtime-macros", "commonware-utils", "criterion", - "crossbeam-queue", + "crossbeam-utils", "futures", "getrandom 0.2.17", + "getrandom 0.4.3", "governor", "libc", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "prometheus-client", - "rand 0.8.6", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "rayon", - "sha2 0.10.9", + "sha2 0.11.0", "sysinfo", "thiserror 2.0.18", "tokio", @@ -1578,11 +1648,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "commonware-runtime-macros" +version = "2026.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f7b39ef9fd9f1df4fe4445c9a5ec5ba012d90d1602b12a8b8626a8563ab02c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "commonware-storage" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca1c42cf37aa27c3f83c31591cad4f1d96317eff81c1eb442e17191adcf9b413" +checksum = "eac18e2b696ab7bbecf1059bc3b8d39fc0af002180906d65c7df3112fd9385de" dependencies = [ "ahash", "anyhow", @@ -1590,14 +1672,14 @@ dependencies = [ "cfg-if", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-parallel", "commonware-runtime", "commonware-utils", "futures", "futures-util", - "prometheus-client", - "rayon", + "hashbrown 0.16.1", "thiserror 2.0.18", "tracing", "zstd", @@ -1605,19 +1687,19 @@ dependencies = [ [[package]] name = "commonware-stream" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c15b328d5f05fff750368a71e2307c380cce52df9712a5b30199b8af4e700c" +checksum = "8890b69e3fa209bc9e87d199ff60ca5a21c88322d32914847511887e6dc5364b" dependencies = [ "chacha20poly1305", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-runtime", "commonware-utils", "futures", - "rand 0.8.6", - "rand_core 0.6.4", + "rand_core 0.10.1", "thiserror 2.0.18", "x25519-dalek", "zeroize", @@ -1625,16 +1707,20 @@ dependencies = [ [[package]] name = "commonware-utils" -version = "2026.4.0" +version = "2026.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf66d7b5c89489d71b0669bda2e014e7c9ffcdf65629ae31886efe5361b1179" +checksum = "a484dd46b738d9b5ba892312425e71e6afaf4320e388d74449bd9ca0ea14cd76" dependencies = [ + "ahash", "bytes", "cfg-if", "commonware-codec", + "commonware-formatting", "commonware-macros", "futures", "getrandom 0.2.17", + "getrandom 0.3.4", + "getrandom 0.4.3", "hashbrown 0.16.1", "num-bigint", "num-integer", @@ -1642,9 +1728,10 @@ dependencies = [ "num-traits", "parking_lot", "pin-project", - "rand 0.8.6", + "rand 0.10.2", "thiserror 2.0.18", "tokio", + "tracing", "zeroize", ] @@ -1666,6 +1753,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.36" @@ -1728,6 +1821,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1856,6 +1955,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils 0.4.2", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1867,13 +1982,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + [[package]] name = "ctutils" -version = "0.3.2" +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 = "758e5ed90be3c8abff7f9a6f37ab7f6d8c59c2210d448b81f3f508134aec84e4" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "cmov", + "cmov 0.5.4", + "subtle", ] [[package]] @@ -1885,7 +2021,23 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "fiat-crypto", + "fiat-crypto 0.2.9", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", "rustc_version 0.4.1", "subtle", "zeroize", @@ -1902,19 +2054,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - [[package]] name = "darling" version = "0.20.11" @@ -2005,7 +2144,17 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", "zeroize", ] @@ -2080,11 +2229,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", - "crypto-common", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils 0.4.2", +] + [[package]] name = "dirs" version = "6.0.0" @@ -2147,26 +2308,27 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "serdect 0.2.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] -name = "ed25519-consensus" -version = "2.1.0" +name = "ecdsa" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "thiserror 1.0.69", + "der 0.8.1", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] @@ -2197,16 +2359,36 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.1", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", - "serdect", + "sec1 0.7.3", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -2336,12 +2518,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2372,12 +2570,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -2560,15 +2752,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -2606,11 +2799,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.4.13" @@ -2653,15 +2857,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -2670,7 +2865,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", "serde", "serde_core", ] @@ -2717,6 +2912,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "http" version = "1.4.0" @@ -2762,6 +2966,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.9.0" @@ -2947,12 +3162,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -3184,10 +3393,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", - "serdect", + "serdect 0.2.0", "sha2 0.10.9", ] @@ -3231,12 +3440,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.185" @@ -3574,9 +3777,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -3588,22 +3791,22 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", "http", "opentelemetry", - "reqwest 0.12.28", + "reqwest", ] [[package]] name = "opentelemetry-otlp" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -3611,35 +3814,33 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest 0.12.28", + "reqwest", "thiserror 2.0.18", - "tracing", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", - "tonic", - "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.4", "thiserror 2.0.18", "tokio", @@ -3654,14 +3855,15 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.0", ] [[package]] @@ -3785,8 +3987,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", ] [[package]] @@ -3865,22 +4077,30 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "primefield" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ - "proc-macro2", - "syn 2.0.117", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", ] [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect 0.4.3", + "wnaf", ] [[package]] @@ -4126,6 +4346,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4146,6 +4377,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -4165,14 +4406,20 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand 0.8.6", + "rand 0.10.2", ] [[package]] @@ -4222,12 +4469,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "readme-rustdocifier" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac" - [[package]] name = "recvmsg" version = "1.0.0" @@ -4254,18 +4495,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "reed-solomon-simd" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffef0520d30fbd4151fb20e262947ae47fb0ab276a744a19b6398438105a072" -dependencies = [ - "cpufeatures 0.2.17", - "fixedbitset", - "once_cell", - "readme-rustdocifier", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -4315,40 +4544,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest" version = "0.13.2" @@ -4357,7 +4552,9 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -4392,10 +4589,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -4650,11 +4857,25 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", - "serdect", + "pkcs8 0.10.2", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils 0.4.2", + "der 0.8.1", + "hybrid-array", "subtle", "zeroize", ] @@ -4839,21 +5060,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", "serde", ] [[package]] -name = "sha2" -version = "0.9.9" +name = "serdect" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.9.0", - "opaque-debug", + "base16ct 1.0.0", + "serde", ] [[package]] @@ -4867,6 +5085,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.9" @@ -4922,6 +5151,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simple_asn1" version = "0.6.4" @@ -4981,7 +5220,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", ] [[package]] @@ -5029,12 +5278,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - [[package]] name = "summit-fuzz" version = "0.0.0" @@ -5060,23 +5303,25 @@ dependencies = [ "alloy-transport-ipc", "anyhow", "bytes", + "commonware-actor", "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-math", "commonware-p2p", "commonware-parallel", "commonware-resolver", "commonware-runtime", "commonware-utils", - "curve25519-dalek", + "curve25519-dalek 4.1.3", "dirs", "ethereum_hashing", "ethereum_ssz", "ethereum_ssz_derive", "futures", - "rand 0.8.6", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", "serde", "sha2 0.10.9", "tokio", @@ -5186,7 +5431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5449,38 +5694,6 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "base64", - "bytes", - "http", - "http-body", - "http-body-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio-stream", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" -dependencies = [ - "bytes", - "prost", - "tonic", -] - [[package]] name = "tower" version = "0.5.3" @@ -5573,9 +5786,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -5678,7 +5891,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -5771,16 +5984,7 @@ version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -5838,40 +6042,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver 1.0.28", -] - [[package]] name = "wasmtimer" version = "0.4.3" @@ -6351,15 +6521,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -6367,82 +6528,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" +name = "wnaf" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver 1.0.28", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", ] [[package]] @@ -6462,13 +6555,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", "zeroize", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4cdb845b..4004d11b 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.4.0" -commonware-cryptography = "2026.4.0" +commonware-codec = "2026.7.0" +commonware-cryptography = "2026.7.0" arbitrary = { version = "1", features = ["derive"] } bytes = "1.10.1" diff --git a/node/Cargo.toml b/node/Cargo.toml index 3a6e5e44..5d8f9a3c 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -55,6 +55,8 @@ summit-syncer.workspace = true summit-finalizer.workspace = true summit-orchestrator.workspace = true +commonware-actor.workspace = true +commonware-formatting.workspace = true commonware-broadcast.workspace = true commonware-codec.workspace = true commonware-consensus.workspace = true diff --git a/node/src/args.rs b/node/src/args.rs index ede376b0..3f4ea758 100644 --- a/node/src/args.rs +++ b/node/src/args.rs @@ -11,7 +11,8 @@ use clap::{Args, Parser, Subcommand}; use commonware_codec::Read; use commonware_cryptography::{Signer, certificate::Scheme}; use commonware_p2p::{Ingress, authenticated}; -use commonware_runtime::{Handle, Metrics as _, Runner, Spawner, tokio}; +use commonware_runtime::Supervisor as _; +use commonware_runtime::{Handle, Runner, Spawner, tokio}; use summit_rpc::{ DEFAULT_RPC_BODY_LIMIT_BYTES, DEFAULT_RPC_MAX_BATCH_SIZE, DEFAULT_RPC_REQUEST_TIMEOUT_SECS, PathSender, RpcBodyLimits, start_rpc_server, start_rpc_server_for_genesis, @@ -20,7 +21,7 @@ use tokio_util::sync::CancellationToken; use alloy_primitives::{Address, B256}; use alloy_rpc_types_engine::ForkchoiceState; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; use futures::{FutureExt, channel::oneshot}; use governor::Quota; use ssz::Decode; @@ -342,7 +343,7 @@ async fn acquire_genesis(context: &tokio::Context, flags: &RunFlags) -> Genesis }; let rpc_genesis_path = genesis_path.clone(); let _rpc_handle = context - .with_label("rpc_genesis") + .child("rpc_genesis") .spawn(move |_context| async move { let genesis_sender = PathSender::new(rpc_genesis_path, Some(genesis_tx)); if let Err(e) = start_rpc_server_for_genesis( @@ -489,7 +490,7 @@ async fn run_node_inner( key_store: KeyStore, mut loaded: LoadedCheckpoint, ) { - let context = context.with_label("summit_cw"); + let context = context.child("summit_cw"); // Initialize telemetry first, before genesis acquisition. First-boot // provisioning can block in acquire_genesis waiting for the genesis RPC, so the @@ -657,7 +658,7 @@ async fn run_node_inner( let listen_addr = format!("{}:{}", flags.prom_ip, flags.prom_port) .parse::() .unwrap(); - let config = MetricServerConfig::new(listen_addr, hooks, Some(context.clone())); + let config = MetricServerConfig::new(listen_addr, hooks, Some(context.child("prom"))); let stop_signal = context.stopped(); MetricServer::new(config).serve(stop_signal).await.unwrap(); } @@ -700,7 +701,7 @@ async fn run_node_inner( ); p2p_cfg.mailbox_size = MAILBOX_SIZE; start_network_and_engine( - context.clone(), + context.child("node"), p2p_cfg, engine_client, key_store, @@ -725,7 +726,7 @@ async fn run_node_inner( ); p2p_cfg.mailbox_size = MAILBOX_SIZE; start_network_and_engine( - context.clone(), + context.child("node"), p2p_cfg, engine_client, key_store, @@ -776,7 +777,7 @@ async fn run_node_local_inner( checkpoint: Option, checkpoint_parent_block: Option, ) -> anyhow::Result<()> { - let context = context.with_label("summit_cw"); + let context = context.child("summit_cw"); let genesis = acquire_genesis(&context, &flags).await; @@ -864,7 +865,7 @@ async fn run_node_local_inner( ); p2p_cfg.mailbox_size = MAILBOX_SIZE; start_network_and_engine( - context.clone(), + context.child("node"), p2p_cfg, engine_client, key_store, @@ -889,7 +890,7 @@ async fn run_node_local_inner( ); p2p_cfg.mailbox_size = MAILBOX_SIZE; start_network_and_engine( - context.clone(), + context.child("node"), p2p_cfg, engine_client, key_store, @@ -917,7 +918,7 @@ async fn run_node_local_inner( .parse::() .unwrap(); let stop_signal = context.stopped(); - let config = MetricServerConfig::new(listen_addr, hooks, Some(context.clone())); + let config = MetricServerConfig::new(listen_addr, hooks, Some(context.child("prom"))); MetricServer::new(config).serve(stop_signal).await.unwrap(); } @@ -1012,7 +1013,7 @@ where EC: EngineClient, { let (mut network, oracle) = - authenticated::discovery::Network::new(context.with_label("network"), p2p_cfg); + authenticated::discovery::Network::new(context.child("network"), p2p_cfg); let oracle = DiscoveryOracle::new(oracle); @@ -1059,7 +1060,7 @@ where let genesis_hash = config.genesis_hash; let namespace = config.namespace.as_bytes().to_vec(); - let engine: Engine<_, _, _, _> = Engine::new(context.with_label("engine"), config).await; + let engine: Engine<_, _, _, _> = Engine::new(context.child("engine"), config).await; #[cfg(feature = "permissioned")] let paused = engine.paused.clone(); @@ -1083,7 +1084,7 @@ where max_batch_size: flags.rpc_max_batch_size, }; let stop_signal = context.stopped(); - let rpc_handle = context.with_label("rpc").spawn(move |_context| async move { + let rpc_handle = context.child("rpc").spawn(move |_context| async move { if let Err(e) = start_rpc_server( finalizer_state_query, key_store_path, @@ -1113,7 +1114,7 @@ fn get_initial_state( ) -> ConsensusState { let epoch_length = NonZeroU64::new(genesis.blocks_per_epoch).expect("blocks_per_epoch must be nonzero"); - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) + let genesis_hash: [u8; 32] = from_hex(&genesis.eth_genesis_hash) .map(|hash_bytes| hash_bytes.try_into()) .expect("bad eth_genesis_hash") .expect("bad eth_genesis_hash"); @@ -1192,8 +1193,8 @@ fn weak_subjectivity_from_flags( } fn parse_digest_arg(value: &str, arg_name: &str) -> summit_types::Digest { - let bytes = from_hex_formatted(value) - .unwrap_or_else(|| panic!("{arg_name} must be a 32-byte hex digest")); + let bytes = + from_hex(value).unwrap_or_else(|| panic!("{arg_name} must be a 32-byte hex digest")); let bytes: [u8; 32] = bytes .try_into() .unwrap_or_else(|_| panic!("{arg_name} must be a 32-byte hex digest")); @@ -1405,14 +1406,14 @@ mod supervision_tests { let executor = deterministic::Runner::from(deterministic::Config::default()); executor.start(|context| async move { let p2p = context - .with_label("p2p") + .child("p2p") .spawn(|_| async move { futures::future::pending::<()>().await }); let rpc = context - .with_label("rpc") + .child("rpc") .spawn(|_| async move { futures::future::pending::<()>().await }); // Engine returns Ok immediately, simulating a clean stop (e.g. committee exit). let engine = context - .with_label("engine") + .child("engine") .spawn(|_| async move { Ok::<(), anyhow::Error>(()) }); let outcome = supervise_node_tasks(&context, p2p, engine, rpc).await; @@ -1428,13 +1429,13 @@ mod supervision_tests { let executor = deterministic::Runner::from(deterministic::Config::default()); executor.start(|context| async move { let p2p = context - .with_label("p2p") + .child("p2p") .spawn(|_| async move { futures::future::pending::<()>().await }); let rpc = context - .with_label("rpc") + .child("rpc") .spawn(|_| async move { futures::future::pending::<()>().await }); // Engine surfaces a tracked-actor failure as Err — the node must exit non-zero. - let engine = context.with_label("engine").spawn(|_| async move { + let engine = context.child("engine").spawn(|_| async move { Err::<(), anyhow::Error>(anyhow::anyhow!("tracked actor failed")) }); @@ -1455,19 +1456,17 @@ mod supervision_tests { let executor = deterministic::Runner::from(deterministic::Config::default()); executor.start(|context| async move { let p2p = context - .with_label("p2p") + .child("p2p") .spawn(|_| async move { futures::future::pending::<()>().await }); let rpc = context - .with_label("rpc") + .child("rpc") .spawn(|_| async move { futures::future::pending::<()>().await }); let engine = context - .with_label("engine") + .child("engine") .spawn(|_| async move { futures::future::pending::>().await }); // Request a runtime stop from a background task so `context.stopped()` resolves. - let stopper = context.clone(); - context - .with_label("stopper") - .spawn(move |_| async move { stopper.stop(0, None).await }); + let stopper = context.child("stopper"); + stopper.spawn(move |stopper| async move { stopper.stop(0, None).await }); let outcome = supervise_node_tasks(&context, p2p, engine, rpc).await; assert!( diff --git a/node/src/bin/execute_blocks.rs b/node/src/bin/execute_blocks.rs index 7be35dfe..cf7f70cc 100644 --- a/node/src/bin/execute_blocks.rs +++ b/node/src/bin/execute_blocks.rs @@ -1,7 +1,7 @@ use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV4, ForkchoiceState}; use anyhow::Result; use clap::{Arg, Command}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; use std::path::PathBuf; use summit_types::engine_client::EngineClient; #[cfg(feature = "bench")] @@ -69,10 +69,7 @@ async fn main() -> Result<()> { let mut client = EthereumHistoricalEngineClient::new(engine_ipc_path, block_dir).await; // Load and commit blocks to Reth - let genesis_hash: [u8; 32] = from_hex_formatted(genesis_hash_str) - .unwrap() - .try_into() - .unwrap(); + let genesis_hash: [u8; 32] = from_hex(genesis_hash_str).unwrap().try_into().unwrap(); let mut forkchoice = ForkchoiceState { head_block_hash: genesis_hash.into(), diff --git a/node/src/bin/observer.rs b/node/src/bin/observer.rs index 875d21eb..f4b3c3d1 100644 --- a/node/src/bin/observer.rs +++ b/node/src/bin/observer.rs @@ -24,8 +24,8 @@ Flow: use clap::Parser; use commonware_codec::{DecodeExt, Encode}; use commonware_cryptography::{Signer, bls12381, ed25519::PrivateKey}; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::http_client::HttpClientBuilder; use ssz::Decode; @@ -150,7 +150,7 @@ fn main() -> Result<(), Box> { let mut reth = reth_builder.spawn(); let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -189,7 +189,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. @@ -239,7 +239,7 @@ fn main() -> Result<(), Box> { // Fresh BLS key for the observer — not in the validator set, so its // Simplex signatures are not accepted and don't collide with validator 1. let observer_bls_key = bls12381::PrivateKey::from_seed(0xDEAD_BEEF); - let observer_bls_encoded = commonware_utils::hex(&observer_bls_key.encode()); + let observer_bls_encoded = commonware_formatting::hex(&observer_bls_key.encode()); fs::write( format!("{}/consensus_key.pem", observer_key_dir), observer_bls_encoded, @@ -263,7 +263,7 @@ fn main() -> Result<(), Box> { let mut observer_reth = observer_reth_builder.spawn(); let observer_stdout = observer_reth.stdout().expect("Failed to get observer stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(observer_stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/observer.log", dir)) @@ -306,7 +306,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // the observer is a required participant: a genuine core task failure // (err) must fail the scenario rather than be masked as a clean exit. if let Err(e) = run_node_local(ctx, observer_flags, None, None) @@ -400,7 +400,7 @@ fn main() -> Result<(), Box> { )) .expect("failed to read master node key"); let master_key_bytes = - from_hex_formatted(&master_key_hex).expect("invalid hex in master node key"); + from_hex(&master_key_hex).expect("invalid hex in master node key"); let master_priv_key = PrivateKey::decode(&master_key_bytes[..]) .expect("failed to decode master private key"); // Observer child keys are separated by domain (#335) under the chain diff --git a/node/src/bin/protocol_params.rs b/node/src/bin/protocol_params.rs index c6250d6d..ac3a5504 100644 --- a/node/src/bin/protocol_params.rs +++ b/node/src/bin/protocol_params.rs @@ -16,7 +16,7 @@ use alloy::rpc::types::TransactionRequest; use alloy::signers::local::PrivateKeySigner; use alloy_primitives::Address; use clap::Parser; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::http_client::HttpClientBuilder; use std::collections::VecDeque; @@ -133,7 +133,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -177,7 +177,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. diff --git a/node/src/bin/stake_and_checkpoint.rs b/node/src/bin/stake_and_checkpoint.rs index c1cd74e2..d4c5ddc5 100644 --- a/node/src/bin/stake_and_checkpoint.rs +++ b/node/src/bin/stake_and_checkpoint.rs @@ -16,8 +16,8 @@ use alloy::signers::local::PrivateKeySigner; use alloy_primitives::{Address, U256}; use clap::Parser; use commonware_cryptography::{Signer, bls12381, ed25519::PrivateKey}; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::http_client::HttpClientBuilder; use ssz::Decode; @@ -98,7 +98,7 @@ fn main() -> Result<(), Box> { let mut genesis = Genesis::load_from_file(GENESIS_PATH).expect("Failed to load genesis file"); genesis.blocks_per_epoch = E2E_BLOCKS_PER_EPOCH; - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) + let genesis_hash: [u8; 32] = from_hex(&genesis.eth_genesis_hash) .expect("bad eth_genesis_hash") .try_into() .expect("bad eth_genesis_hash"); @@ -150,7 +150,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -199,7 +199,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. @@ -434,7 +434,7 @@ fn main() -> Result<(), Box> { // executor.start(|node_context| async move { // let flags = get_node_flags(source_node); - // let node_handle = node_context.clone().spawn(move |ctx| async move { + // let node_handle = node_context.child("node").spawn(move |ctx| async move { // run_node_with_runtime(ctx, flags, None).await.unwrap(); // }); @@ -473,7 +473,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -509,11 +509,11 @@ fn main() -> Result<(), Box> { let consensus_key_path = format!("{}/node{}/data/consensus_key.pem", args.data_dir, x); // Write node key (hex encoded) - let encoded_node_key = commonware_utils::hex(&ed25519_private_key.encode()); + let encoded_node_key = commonware_formatting::hex(&ed25519_private_key.encode()); fs::write(&node_key_path, encoded_node_key).expect("Unable to write node key to disk"); // Write consensus key (hex encoded) - let encoded_consensus_key = commonware_utils::hex(&bls_private_key.encode()); + let encoded_consensus_key = commonware_formatting::hex(&bls_private_key.encode()); fs::write(&consensus_key_path, encoded_consensus_key).expect("Unable to write consensus key to disk"); flags.key_store_path = format!("{}/node{}/data", args.data_dir, x); @@ -537,7 +537,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // the checkpoint restarted node is a required participant: a genuine // core task failure (err) must fail the scenario, not be masked. if let Err(e) = diff --git a/node/src/bin/stake_and_join_with_outdated_ckpt.rs b/node/src/bin/stake_and_join_with_outdated_ckpt.rs index 4decf3bc..fdba3bd0 100644 --- a/node/src/bin/stake_and_join_with_outdated_ckpt.rs +++ b/node/src/bin/stake_and_join_with_outdated_ckpt.rs @@ -17,8 +17,8 @@ use alloy::signers::local::PrivateKeySigner; use alloy_primitives::{Address, U256, keccak256}; use clap::Parser; use commonware_cryptography::{Signer, bls12381, ed25519::PrivateKey}; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::http_client::HttpClientBuilder; use ssz::Decode; @@ -102,7 +102,7 @@ fn main() -> Result<(), Box> { let mut genesis = Genesis::load_from_file(GENESIS_PATH).expect("Failed to load genesis file"); genesis.blocks_per_epoch = E2E_BLOCKS_PER_EPOCH; - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) + let genesis_hash: [u8; 32] = from_hex(&genesis.eth_genesis_hash) .expect("bad eth_genesis_hash") .try_into() .expect("bad eth_genesis_hash"); @@ -153,7 +153,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -202,7 +202,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. @@ -456,7 +456,7 @@ fn main() -> Result<(), Box> { // executor.start(|node_context| async move { // let flags = get_node_flags(source_node); - // let node_handle = node_context.clone().spawn(move |ctx| async move { + // let node_handle = node_context.child("node").spawn(move |ctx| async move { // run_node_with_runtime(ctx, flags, None).await.unwrap(); // }); @@ -517,7 +517,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -553,11 +553,11 @@ fn main() -> Result<(), Box> { let consensus_key_path = format!("{}/node{}/data/consensus_key.pem", args.data_dir, x); // Write node key (hex encoded) - let encoded_node_key = commonware_utils::hex(&ed25519_private_key.encode()); + let encoded_node_key = commonware_formatting::hex(&ed25519_private_key.encode()); fs::write(&node_key_path, encoded_node_key).expect("Unable to write node key to disk"); // Write consensus key (hex encoded) - let encoded_consensus_key = commonware_utils::hex(&bls_private_key.encode()); + let encoded_consensus_key = commonware_formatting::hex(&bls_private_key.encode()); fs::write(&consensus_key_path, encoded_consensus_key).expect("Unable to write consensus key to disk"); flags.key_store_path = format!("{}/node{}/data", args.data_dir, x); @@ -581,7 +581,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // the checkpoint restarted node is a required participant: a genuine // core task failure (err) must fail the scenario, not be masked. if let Err(e) = diff --git a/node/src/bin/sync_from_genesis.rs b/node/src/bin/sync_from_genesis.rs index a2304218..2afd48f6 100644 --- a/node/src/bin/sync_from_genesis.rs +++ b/node/src/bin/sync_from_genesis.rs @@ -38,8 +38,8 @@ use alloy_primitives::{Address, U256}; use clap::Parser; use commonware_codec::DecodeExt; use commonware_cryptography::{Signer, bls12381, ed25519::PrivateKey}; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::core::ClientError; use jsonrpsee::http_client::HttpClientBuilder; @@ -114,7 +114,7 @@ fn main() -> Result<(), Box> { let mut genesis = Genesis::load_from_file(GENESIS_PATH).expect("Failed to load genesis file"); genesis.blocks_per_epoch = E2E_BLOCKS_PER_EPOCH; - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) + let genesis_hash: [u8; 32] = from_hex(&genesis.eth_genesis_hash) .expect("bad eth_genesis_hash") .try_into() .expect("bad eth_genesis_hash"); @@ -165,7 +165,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -214,7 +214,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. @@ -273,7 +273,7 @@ fn main() -> Result<(), Box> { .connect_http(node0_url.parse().expect("Invalid URL")); let withdrawal_contract_address = Address::from_str("0x00000961Ef480Eb55e80D19ad83579A64c007002").unwrap(); - let pub_key_bytes = from_hex_formatted("f205c8c88d5d1753843dd0fc9810390efd00d6f752dd555c0ad4000bfcac2226").ok_or("PublicKey bad format").unwrap(); + let pub_key_bytes = from_hex("f205c8c88d5d1753843dd0fc9810390efd00d6f752dd555c0ad4000bfcac2226").ok_or("PublicKey bad format").unwrap(); let pub_key_bytes_ar: [u8; 32] = pub_key_bytes.try_into().unwrap(); let _public_key = PublicKey::decode(&pub_key_bytes_ar[..]).map_err(|_| "Unable to decode Public Key").unwrap(); // Amount 0 is a full exit (EIP-7002): the validator leaves the @@ -474,7 +474,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -501,10 +501,10 @@ fn main() -> Result<(), Box> { let node_key_path = format!("{}/node{}/data/node_key.pem", args.data_dir, x); let consensus_key_path = format!("{}/node{}/data/consensus_key.pem", args.data_dir, x); - let encoded_node_key = commonware_utils::hex(&ed25519_private_key.encode()); + let encoded_node_key = commonware_formatting::hex(&ed25519_private_key.encode()); fs::write(&node_key_path, encoded_node_key).expect("Unable to write node key to disk"); - let encoded_consensus_key = commonware_utils::hex(&bls_private_key.encode()); + let encoded_consensus_key = commonware_formatting::hex(&bls_private_key.encode()); fs::write(&consensus_key_path, encoded_consensus_key).expect("Unable to write consensus key to disk"); // Start the joining node - syncing from genesis (no checkpoint) @@ -525,7 +525,7 @@ fn main() -> Result<(), Box> { .expect("Failed to load genesis"); let validators = genesis.get_validators().expect("Failed to get validators"); let bootstrap_validator = &validators[0]; - let bootstrap_pk_hex = commonware_utils::hex(bootstrap_validator.node_public_key.as_ref()); + let bootstrap_pk_hex = commonware_formatting::hex(bootstrap_validator.node_public_key.as_ref()); let bootstrap_addr = bootstrap_validator.ip_address; let bootstrappers_path = format!("{}/bootstrappers.toml", args.data_dir); @@ -559,7 +559,7 @@ address = "{}" let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // no checkpoint, sync from genesis. a coordinated shutdown (graceful // stop or committee exit) returns ok; a genuine core task failure // returns err and must fail the scenario instead of being masked. @@ -612,7 +612,7 @@ address = "{}" } // Verify the new validator is in the consensus state - let new_validator_pubkey = commonware_utils::hex(&ed25519_pubkey_bytes); + let new_validator_pubkey = commonware_formatting::hex(&ed25519_pubkey_bytes); let new_validator_balance = get_validator_balance(new_node_rpc_port, new_validator_pubkey.clone()).await; match new_validator_balance { Ok(balance) => { diff --git a/node/src/bin/testnet.rs b/node/src/bin/testnet.rs index 8b4b1e18..c2d10511 100644 --- a/node/src/bin/testnet.rs +++ b/node/src/bin/testnet.rs @@ -17,7 +17,8 @@ use std::{ use alloy_node_bindings::Reth; use clap::Parser; -use commonware_runtime::{Metrics as _, Runner as _, Spawner as _, tokio}; +use commonware_runtime::Supervisor as _; +use commonware_runtime::{Runner as _, Spawner as _, tokio}; use futures::future::try_join_all; use summit::args::{RunFlags, run_node_local}; @@ -101,7 +102,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -156,8 +157,12 @@ fn main() -> Result<(), Box> { } // Start our consensus engine - let handle = - run_node_local(context.with_label(&format!("node{x}")), flags, None, None); + let handle = run_node_local( + context.child("node").with_attribute("index", x), + flags, + None, + None, + ); consensus_handles.push(handle); } diff --git a/node/src/bin/verify_consensus_state_proof.rs b/node/src/bin/verify_consensus_state_proof.rs index d86afd7a..0a124021 100644 --- a/node/src/bin/verify_consensus_state_proof.rs +++ b/node/src/bin/verify_consensus_state_proof.rs @@ -4,7 +4,7 @@ use alloy::rpc::types::TransactionRequest; use alloy::signers::local::PrivateKeySigner; use alloy_primitives::{Address, Bytes, U256, address, keccak256}; use clap::Parser; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::http_client::HttpClientBuilder; use std::collections::VecDeque; @@ -116,7 +116,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -146,7 +146,7 @@ fn main() -> Result<(), Box> { .with_catch_panics(true); let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. diff --git a/node/src/bin/withdraw_and_exit.rs b/node/src/bin/withdraw_and_exit.rs index 10eb2be5..3c8fb2a6 100644 --- a/node/src/bin/withdraw_and_exit.rs +++ b/node/src/bin/withdraw_and_exit.rs @@ -16,8 +16,8 @@ use alloy::signers::local::PrivateKeySigner; use alloy_primitives::{Address, U256}; use clap::Parser; use commonware_codec::DecodeExt; -use commonware_runtime::{Clock, Runner as _, Spawner as _, tokio as cw_tokio}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; +use commonware_runtime::{Clock, Runner as _, Spawner as _, Supervisor as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; use jsonrpsee::core::ClientError; use jsonrpsee::http_client::HttpClientBuilder; @@ -138,7 +138,7 @@ fn main() -> Result<(), Box> { let stdout = reth.stdout().expect("Failed to get stdout"); let log_dir = args.log_dir.clone(); - context.clone().spawn(async move |_| { + context.child("reth").spawn(async move |_| { let reader = BufReader::new(stdout); let mut log_file = log_dir.as_ref().map(|dir| { fs::File::create(format!("{}/node{}.log", dir, x)) @@ -187,7 +187,7 @@ fn main() -> Result<(), Box> { let executor = cw_tokio::Runner::new(cfg); executor.start(|node_context| async move { - let node_handle = node_context.clone().spawn(move |ctx| async move { + let node_handle = node_context.child("node").spawn(move |ctx| async move { // a coordinated shutdown (graceful stop or committee exit) returns // ok; a genuine core task failure returns err and must fail the // scenario instead of being masked as a clean node exit. @@ -246,7 +246,7 @@ fn main() -> Result<(), Box> { .connect_http(node0_url.parse().expect("Invalid URL")); let withdrawal_contract_address = Address::from_str("0x00000961Ef480Eb55e80D19ad83579A64c007002").unwrap(); - let pub_key_bytes = from_hex_formatted("f205c8c88d5d1753843dd0fc9810390efd00d6f752dd555c0ad4000bfcac2226").ok_or("PublicKey bad format").unwrap(); + let pub_key_bytes = from_hex("f205c8c88d5d1753843dd0fc9810390efd00d6f752dd555c0ad4000bfcac2226").ok_or("PublicKey bad format").unwrap(); let pub_key_bytes_ar: [u8; 32] = pub_key_bytes.try_into().unwrap(); let _public_key = PublicKey::decode(&pub_key_bytes_ar[..]).map_err(|_| "Unable to decode Public Key").unwrap(); // Amount 0 is a full exit (EIP-7002): the validator leaves the diff --git a/node/src/config.rs b/node/src/config.rs index 80e34003..afaa8fe6 100644 --- a/node/src/config.rs +++ b/node/src/config.rs @@ -16,7 +16,10 @@ pub const RECOVERED_CHANNEL: u64 = 1; pub const RESOLVER_CHANNEL: u64 = 2; pub const BROADCASTER_CHANNEL: u64 = 3; pub const BACKFILLER_CHANNEL: u64 = 4; -pub const MAILBOX_SIZE: usize = 16384; +use commonware_utils::NZUsize; +use std::num::NonZeroUsize; + +pub const MAILBOX_SIZE: NonZeroUsize = NZUsize!(16384); /// How often the finalizer retries applying blocks that were deferred because /// the execution layer returned `SYNCING`. See [`summit_finalizer::FinalizerConfig`]. pub const FINALIZER_DRAIN_INTERVAL: Duration = Duration::from_secs(5); @@ -42,7 +45,7 @@ pub struct EngineConfig, pub participants: Vec<(PublicKey, bls12381::PublicKey)>, - pub mailbox_size: usize, + pub mailbox_size: NonZeroUsize, pub finalizer_pending_notarized_max: usize, pub backfill_quota: Quota, pub deque_size: usize, diff --git a/node/src/engine.rs b/node/src/engine.rs index e34706a6..80614d1b 100644 --- a/node/src/engine.rs +++ b/node/src/engine.rs @@ -21,6 +21,7 @@ use governor::clock::Clock as GClock; use rand::{CryptoRng, Rng}; use std::marker::PhantomData; use std::num::NonZero; +use std::num::NonZeroUsize; #[cfg(feature = "permissioned")] use std::sync::Arc; #[cfg(feature = "permissioned")] @@ -114,7 +115,7 @@ pub struct Engine< orchestrator_mailbox: summit_orchestrator::Mailbox, oracle: O, node_public_key: PublicKey, - mailbox_size: usize, + mailbox_size: NonZeroUsize, fetch_timeout: Duration, sync_start: SyncStart, checkpoint: Option>, @@ -173,9 +174,9 @@ where // create finalizer let (finalizer, initial_state, finalizer_mailbox, finalizer_state_query) = Finalizer::new( - context.with_label("finalizer"), + context.child("finalizer"), FinalizerConfig { - mailbox_size: cfg.mailbox_size, + mailbox_size: cfg.mailbox_size.get(), db_prefix: cfg.partition_prefix.clone(), engine_client: cfg.engine_client.clone(), oracle: cfg.oracle.clone(), @@ -206,10 +207,10 @@ where // create application let (application, application_mailbox) = summit_application::Actor::new( - context.with_label("application"), + context.child("application"), ApplicationConfig { engine_client: cfg.engine_client, - mailbox_size: cfg.mailbox_size, + mailbox_size: cfg.mailbox_size.get(), partition_prefix: cfg.partition_prefix.clone(), genesis_hash: cfg.genesis_hash, max_message_size_bytes: cfg.max_message_size_bytes, @@ -224,7 +225,7 @@ where // create the buffer let (buffer, buffer_mailbox) = buffered::Engine::new( - context.with_label("buffer"), + context.child("buffer"), buffered::Config { public_key: node_public_key.clone(), mailbox_size: cfg.mailbox_size, @@ -238,7 +239,7 @@ where // create the syncer // Initialize finalizations by height archive let finalizations_by_height = immutable::Archive::init( - context.with_label("finalizations_by_height"), + context.child("finalizations_by_height"), immutable::Config { metadata_partition: format!( "{}-finalizations-by-height-metadata", @@ -279,7 +280,7 @@ where // Initialize finalized blocks archive let finalized_blocks = immutable::Archive::init( - context.with_label("finalized_blocks"), + context.child("finalized_blocks"), immutable::Config { metadata_partition: format!("{}-finalized_blocks-metadata", cfg.partition_prefix), freezer_table_partition: format!( @@ -331,7 +332,7 @@ where }; let (syncer, syncer_mailbox) = summit_syncer::Actor::init( - context.with_label("syncer"), + context.child("syncer"), finalizations_by_height, finalized_blocks, syncer_config, @@ -340,15 +341,15 @@ where // create orchestrator let (orchestrator, orchestrator_mailbox) = summit_orchestrator::Actor::new( - context.with_label("orchestrator"), + context.child("orchestrator"), summit_orchestrator::Config { oracle: cfg.oracle.clone(), application: application_mailbox.clone(), scheme_provider: scheme_provider.clone(), syncer_mailbox: syncer_mailbox.clone(), namespace: consensus_domain.clone(), - muxer_size: cfg.mailbox_size, - mailbox_size: cfg.mailbox_size, + muxer_size: cfg.mailbox_size.get(), + mailbox_size: cfg.mailbox_size.get(), epocher: epocher.clone(), partition_prefix: cfg.partition_prefix.clone(), leader_timeout: cfg.leader_timeout, @@ -456,7 +457,7 @@ where impl Receiver, ), ) -> Handle> { - self.context.clone().spawn(|_| { + self.context.child("engine_run").spawn(|_| { self.run( pending_network, recovered_network, @@ -504,8 +505,11 @@ where let buffer_handle = self.buffer.start(broadcast_network); // Initialize resolver for backfill - let (resolver_rx, resolver) = - summit_syncer::resolver::p2p::init(&self.context, resolver_config, backfill_network); + let (resolver_rx, resolver) = summit_syncer::resolver::p2p::init( + self.context.child("backfill"), + resolver_config, + backfill_network, + ); let finalizer_handle = self.finalizer.start(self.orchestrator_mailbox); // start the syncer diff --git a/node/src/genesis.rs b/node/src/genesis.rs index 3f6202b1..9a69b85b 100644 --- a/node/src/genesis.rs +++ b/node/src/genesis.rs @@ -1,5 +1,5 @@ use clap::Subcommand; -use commonware_utils::from_hex_formatted; +use commonware_formatting::{from_hex, hex}; use std::fs; use summit_types::{Genesis, GenesisValidator}; @@ -82,7 +82,7 @@ impl GenesisSubCmd { } GenesisSubCmd::Digest { genesis_path } => { let genesis = load(genesis_path); - println!("0x{}", commonware_utils::hex(&genesis.config_digest())); + println!("0x{}", hex(&genesis.config_digest())); } } } @@ -107,7 +107,7 @@ fn fill_template( let mut keyed = validators .into_iter() .map(|validator| { - let key = from_hex_formatted(&validator.node_public_key).ok_or_else(|| { + let key = from_hex(&validator.node_public_key).ok_or_else(|| { format!( "validator node_public_key is not valid hex: {:?}", validator.node_public_key @@ -173,7 +173,7 @@ mod tests { let keys: Vec> = built .validators .iter() - .map(|v| from_hex_formatted(&v.node_public_key).unwrap()) + .map(|v| from_hex(&v.node_public_key).unwrap()) .collect(); assert_eq!(keys.len(), example.validators.len()); assert!( diff --git a/node/src/keys.rs b/node/src/keys.rs index 4ba357f9..44fd58f2 100644 --- a/node/src/keys.rs +++ b/node/src/keys.rs @@ -101,16 +101,16 @@ impl KeySubCmd { create_keystore_dir(&keystore_dir).expect("Unable to create keystore directory"); // Generate ed25519 node key - let node_private_key = PrivateKey::random(&mut rand::thread_rng()); + let node_private_key = PrivateKey::random(commonware_utils::sys_rng()); let node_pub_key = node_private_key.public_key(); - let encoded_node_key = commonware_utils::hex(&node_private_key.encode()); + let encoded_node_key = commonware_formatting::hex(&node_private_key.encode()); write_private_key_file(&node_key_path, &encoded_node_key) .expect("Unable to write node key to disk"); // Generate BLS consensus key - let consensus_private_key = BlsPrivateKey::random(&mut rand::thread_rng()); + let consensus_private_key = BlsPrivateKey::random(commonware_utils::sys_rng()); let consensus_pub_key = consensus_private_key.public_key(); - let encoded_consensus_key = commonware_utils::hex(&consensus_private_key.encode()); + let encoded_consensus_key = commonware_formatting::hex(&consensus_private_key.encode()); write_private_key_file(&consensus_key_path, &encoded_consensus_key) .expect("Unable to write consensus key to disk"); diff --git a/node/src/prom/server.rs b/node/src/prom/server.rs index fd16a148..054baee7 100644 --- a/node/src/prom/server.rs +++ b/node/src/prom/server.rs @@ -16,7 +16,9 @@ pub struct MetricServerConfig { listen_addr: SocketAddr, hooks: Hooks, /// Optional commonware runtime context for merging runtime metrics into the response. - cw_context: Option, + /// Arc-wrapped so every connection handler can encode the registry: the 2026.5.0 + /// runtime context is not Clone. + cw_context: Option>, } impl MetricServerConfig { @@ -25,7 +27,7 @@ impl MetricServerConfig { Self { listen_addr, hooks, - cw_context, + cw_context: cw_context.map(Arc::new), } } } @@ -77,7 +79,7 @@ impl MetricServer { &self, listen_addr: SocketAddr, hook: Arc, - cw_context: Option, + cw_context: Option>, stop_signal: Signal, ) -> eyre::Result<()> { let listener = tokio::net::TcpListener::bind(listen_addr) diff --git a/node/src/test_harness/common.rs b/node/src/test_harness/common.rs index d19551cd..5df300a1 100644 --- a/node/src/test_harness/common.rs +++ b/node/src/test_harness/common.rs @@ -7,14 +7,17 @@ use crate::{config::EngineConfig, engine::Engine}; use alloy_eips::eip7685::Requests; use alloy_primitives::{Address, B256, Bytes}; use alloy_rpc_types_engine::ForkchoiceState; +use commonware_actor::Feedback; use commonware_codec::Write; +use commonware_formatting::from_hex; use commonware_p2p::simulated::{self, Link, Network, Oracle, Receiver, Sender}; -use commonware_p2p::{Blocker, Manager, PeerSetUpdate, Provider, TrackedPeers}; +use commonware_p2p::{Blocker, Manager, PeerSetSubscription, Provider, TrackedPeers}; +use commonware_runtime::Supervisor as _; use commonware_runtime::{ Clock, Metrics, Runner as _, deterministic::{self, Runner}, }; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use governor::Quota; use rand::SeedableRng; use rand::rngs::StdRng; @@ -34,7 +37,6 @@ use summit_types::keystore::KeyStore; use summit_types::network_oracle::NetworkOracle; use summit_types::scheme::MultisigScheme; use summit_types::{Block, Digest, EngineClient, PrivateKey, PublicKey, deposit_signature_domain}; -use tokio::sync::mpsc; pub const DEFAULT_BLOCKS_PER_EPOCH: u64 = 10; @@ -147,7 +149,7 @@ pub fn run_until_height( executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -185,7 +187,7 @@ pub fn run_until_height( link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = from_hex_formatted(GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -218,7 +220,11 @@ pub fn run_until_height( initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -237,27 +243,21 @@ pub fn run_until_height( // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } // If ends with contiguous_height, ensure it is at least required_container - if metric.ends_with("finalizer_height") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let value = sample.value.parse::().unwrap(); if value >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() as u32 == n { success = true; break; @@ -361,7 +361,7 @@ pub async fn assert_state_root_consensus_skip( } pub fn get_domain() -> Digest { - let genesis_hash = from_hex_formatted(GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -421,6 +421,46 @@ pub fn get_initial_state( }) } +/// One Prometheus sample line parsed from `context.encode()`. +/// +/// Commonware 2026.5.0 renders dynamic context labels as Prometheus label +/// attributes instead of metric name prefixes, so the legacy sample +/// `validator__engine_finalizer_height 21` is now encoded as +/// `engine_finalizer_height{uid="validator_"} 21`. +pub struct MetricSample { + /// The metric name, without labels. + pub name: String, + /// The `uid` label value identifying the node that emitted the sample. + pub uid: String, + /// The raw sample value. + pub value: String, +} + +/// Parses one sample line from `context.encode()`. +/// +/// # Returns +/// * `Some(MetricSample)` for samples carrying a `uid` label +/// * `None` for descriptor/EOF lines and samples without a `uid` label +pub fn parse_metric(line: &str) -> Option { + if line.starts_with('#') { + return None; + } + let (sample, value) = line.rsplit_once(' ')?; + let (name, labels) = match sample.split_once('{') { + Some((name, labels)) => (name, labels.strip_suffix('}')?), + None => (sample, ""), + }; + let uid = labels.split(',').find_map(|label| { + let (key, value) = label.split_once('=')?; + (key == "uid").then(|| value.trim_matches('"').to_string()) + })?; + Some(MetricSample { + name: name.to_string(), + uid, + value: value.to_string(), + }) +} + /// Parse a substring from a metric name using XML-like tags /// /// # Arguments @@ -447,35 +487,6 @@ pub fn parse_metric_substring(metric: &str, tag: &str) -> Option { Some(metric[substring_start..end].to_string()) } -/// Extracts the validator id from a metric string. -/// -/// # Arguments -/// * `metric` - The metric name to parse from -/// -/// # Returns -/// * `Some(String)` if the validator id is contained in the string -/// * `None` if the validator if doesn't exist -/// ``` -pub fn extract_validator_id(metric: &str) -> Option { - // Metric format is: validator_{pubkey}_{component}_{metric_name} - // We need to extract validator_{pubkey} - let prefix = "validator_"; - - if !metric.starts_with(prefix) { - return None; - } - - // Find the position after "validator_" - let start = prefix.len(); - - // Find the next underscore after "validator_" - let remaining = &metric[start..]; - let end_offset = remaining.find('_')?; - - // Extract from beginning to the second underscore (start + end_offset) - Some(metric[..start + end_offset].to_string()) -} - /// Create a single DepositRequest for testing with valid ED25519 and BLS signatures /// /// This function creates a test deposit request with all required fields, including @@ -689,7 +700,7 @@ where namespace, key_store, participants, - mailbox_size: 1024, + mailbox_size: NZUsize!(1024), finalizer_pending_notarized_max: 1000, deque_size: 10, backfill_quota: Quota::per_second(NonZeroU32::new(512).unwrap()), @@ -712,11 +723,18 @@ where } } -#[derive(Clone)] pub struct SimulatedOracle { inner: simulated::Manager, } +impl Clone for SimulatedOracle { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + impl Debug for SimulatedOracle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SimulatedOracle").finish() @@ -736,18 +754,19 @@ impl NetworkOracle for SimulatedOracle { use commonware_utils::ordered::Set; let primary = Set::try_from(primary).expect("primary peers should be unique"); let secondary = Set::try_from(secondary).expect("secondary peers should be unique"); - self.inner - .track(index, TrackedPeers::new(primary, secondary)) - .await + let _ = self + .inner + .track(index, TrackedPeers::new(primary, secondary)); } } impl Blocker for SimulatedOracle { type PublicKey = PublicKey; - async fn block(&mut self, _public_key: Self::PublicKey) { + fn block(&mut self, _public_key: Self::PublicKey) -> Feedback { // Simulated oracle doesn't support blocking individual peers // This is only used in production for misbehaving peers + Feedback::Ok } } @@ -758,16 +777,16 @@ impl Provider for SimulatedOracle { self.inner.peer_set(id).await } - async fn subscribe(&mut self) -> mpsc::UnboundedReceiver> { + async fn subscribe(&mut self) -> PeerSetSubscription { self.inner.subscribe().await } } impl Manager for SimulatedOracle { - async fn track(&mut self, id: u64, peers: R) + fn track(&mut self, id: u64, peers: R) -> Feedback where R: Into> + Send, { - self.inner.track(id, peers).await + self.inner.track(id, peers) } } diff --git a/node/src/test_harness/mock_engine_client.rs b/node/src/test_harness/mock_engine_client.rs index 52c2f7db..7dc17d4c 100644 --- a/node/src/test_harness/mock_engine_client.rs +++ b/node/src/test_harness/mock_engine_client.rs @@ -7,7 +7,7 @@ use alloy_rpc_types_engine::{ ExecutionPayloadV2, ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus, PayloadStatusEnum, }; -use rand::RngCore; +use rand::Rng as _; use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use summit_types::{Block, EngineClient}; @@ -393,7 +393,7 @@ impl EngineClient for MockEngineClient { // Generate unique payload ID let payload_id = { - let mut rng = rand::thread_rng(); + let mut rng = commonware_utils::sys_rng(); let mut bytes = [0u8; 8]; rng.fill_bytes(&mut bytes); PayloadId::new(bytes) diff --git a/node/src/tests/checkpointing/creation.rs b/node/src/tests/checkpointing/creation.rs index 3fa7ea35..0e971b23 100644 --- a/node/src/tests/checkpointing/creation.rs +++ b/node/src/tests/checkpointing/creation.rs @@ -6,13 +6,15 @@ use crate::test_harness::mock_engine_client::MockEngineNetworkBuilder; use commonware_consensus::types::FixedEpocher; use commonware_cryptography::Signer; use commonware_cryptography::bls12381; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::simulated; use commonware_p2p::simulated::{Link, Network}; +use commonware_runtime::Supervisor as _; use commonware_runtime::deterministic::Runner; use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use rand::SeedableRng; use rand::rngs::StdRng; use std::collections::{HashMap, HashSet}; @@ -39,7 +41,7 @@ fn test_checkpoint_created() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -73,8 +75,7 @@ fn test_checkpoint_created() { // Link all validators common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -111,7 +112,11 @@ fn test_checkpoint_created() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -131,43 +136,37 @@ fn test_checkpoint_created() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("consensus_state_stored") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("consensus_state_stored") { + let height = sample.value.parse::().unwrap(); // Height should be the last block of an epoch if height > 0 { assert_eq!((height + 1) % DEFAULT_BLOCKS_PER_EPOCH, 0); } - state_stored.insert(metric.to_string()); + state_stored.insert(sample.uid.clone()); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } - if metric.ends_with("finalized_header_stored") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalized_header_stored") { + let height = sample.value.parse::().unwrap(); // Height should be the last block of an epoch assert_eq!((height + 1) % DEFAULT_BLOCKS_PER_EPOCH, 0); - header_stored.insert(metric.to_string()); + header_stored.insert(sample.uid.clone()); } if header_stored.len() as u32 >= n && state_stored.len() as u32 == n @@ -236,7 +235,7 @@ fn test_previous_header_hash_matches() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -270,8 +269,7 @@ fn test_previous_header_hash_matches() { // Link all validators common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -308,7 +306,11 @@ fn test_previous_header_hash_matches() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -328,37 +330,30 @@ fn test_previous_header_hash_matches() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } - if metric.ends_with("finalized_header_stored") { - let height = value.parse::().unwrap(); - let header = - common::parse_metric_substring(metric, "header").expect("header missing"); - let prev_header = common::parse_metric_substring(metric, "prev_header") + if sample.name.ends_with("finalized_header_stored") { + let height = sample.value.parse::().unwrap(); + let header = common::parse_metric_substring(&sample.name, "header") + .expect("header missing"); + let prev_header = common::parse_metric_substring(&sample.name, "prev_header") .expect("prev_header missing"); - let validator_id = - common::extract_validator_id(metric).expect("failed to parse validator id"); + let validator_id = sample.uid.clone(); if is_last_block_of_epoch( &FixedEpocher::new(NonZeroU64::new(DEFAULT_BLOCKS_PER_EPOCH).unwrap()), diff --git a/node/src/tests/checkpointing/joining.rs b/node/src/tests/checkpointing/joining.rs index 9d81f3da..37a1bef3 100644 --- a/node/src/tests/checkpointing/joining.rs +++ b/node/src/tests/checkpointing/joining.rs @@ -5,13 +5,15 @@ use crate::test_harness::common::{SimulatedOracle, get_default_engine_config, ge use crate::test_harness::mock_engine_client::MockEngineNetworkBuilder; use commonware_cryptography::Signer; use commonware_cryptography::bls12381; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::simulated; use commonware_p2p::simulated::{Link, Network}; +use commonware_runtime::Supervisor as _; use commonware_runtime::deterministic::Runner; use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use rand::SeedableRng; use rand::rngs::StdRng; use std::collections::{HashMap, HashSet}; @@ -35,7 +37,7 @@ fn test_single_engine_with_checkpoint() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -73,8 +75,7 @@ fn test_single_engine_with_checkpoint() { // Link validator common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -105,7 +106,11 @@ fn test_single_engine_with_checkpoint() { consensus_state, ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; let finalizer_mailbox = engine.finalizer_mailbox.clone(); // Get networking let (pending, recovered, resolver, orchestrator, broadcast) = @@ -150,7 +155,7 @@ fn test_node_joins_later_with_checkpoint() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -189,8 +194,7 @@ fn test_node_joins_later_with_checkpoint() { common::register_validators(&mut oracle, initial_node_public_keys).await; common::link_validators(&mut oracle, initial_node_public_keys, link.clone(), None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -229,7 +233,11 @@ fn test_node_joins_later_with_checkpoint() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -299,7 +307,11 @@ fn test_node_joins_later_with_checkpoint() { validators.clone(), consensus_state, ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; // Get networking from late registrations let (pending, recovered, resolver, orchestrator, broadcast) = @@ -316,26 +328,20 @@ fn test_node_joins_later_with_checkpoint() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let value = sample.value.parse::().unwrap(); if value >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() as u32 == n { success = true; break; @@ -399,7 +405,7 @@ fn test_checkpoint_join_replays_and_seeds_finalized_header() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -433,8 +439,7 @@ fn test_checkpoint_join_replays_and_seeds_finalized_header() { common::register_validators(&oracle, initial_node_public_keys).await; common::link_validators(&mut oracle, initial_node_public_keys, link.clone(), None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -461,7 +466,11 @@ fn test_checkpoint_join_replays_and_seeds_finalized_header() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = registrations.remove(&public_key).unwrap(); @@ -534,7 +543,11 @@ 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(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; let joiner_query = engine.finalizer_mailbox.clone(); let (pending, recovered, resolver, orchestrator, broadcast) = late_registrations.remove(&public_key).unwrap(); @@ -596,7 +609,7 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -636,8 +649,7 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { common::register_validators(&mut oracle, initial_node_public_keys).await; common::link_validators(&mut oracle, initial_node_public_keys, link.clone(), None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -676,7 +688,11 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { initial_validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -746,7 +762,11 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { initial_validators, consensus_state, ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; // Get networking from late registrations let (pending, recovered, resolver, orchestrator, broadcast) = @@ -765,26 +785,20 @@ fn test_node_joins_later_with_checkpoint_not_in_genesis() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let value = sample.value.parse::().unwrap(); if value >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() == n as usize { success = true; break; diff --git a/node/src/tests/checkpointing/verification.rs b/node/src/tests/checkpointing/verification.rs index 91e20432..db8b6246 100644 --- a/node/src/tests/checkpointing/verification.rs +++ b/node/src/tests/checkpointing/verification.rs @@ -7,13 +7,15 @@ use alloy_primitives::Address; use commonware_codec::Encode as _; use commonware_cryptography::Signer; use commonware_cryptography::bls12381; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::simulated; use commonware_p2p::simulated::{Link, Network}; +use commonware_runtime::Supervisor as _; use commonware_runtime::deterministic::Runner; use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use rand::SeedableRng; use rand::rngs::StdRng; use std::collections::{HashMap, HashSet}; @@ -69,7 +71,7 @@ fn test_checkpoint_verification_fixed_committee() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -101,8 +103,8 @@ fn test_checkpoint_verification_fixed_committee() { .iter() .enumerate() .map(|(i, (node_pk, consensus_pk))| { - let node_pub_hex = commonware_utils::hex(node_pk.as_ref()); - let consensus_pub_hex = commonware_utils::hex(&consensus_pk.encode()); + let node_pub_hex = commonware_formatting::hex(node_pk.as_ref()); + let consensus_pub_hex = commonware_formatting::hex(&consensus_pk.encode()); GenesisValidator { node_public_key: format!("0x{node_pub_hex}"), consensus_public_key: format!("0x{consensus_pub_hex}"), @@ -138,7 +140,7 @@ fn test_checkpoint_verification_fixed_committee() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -172,7 +174,7 @@ fn test_checkpoint_verification_fixed_committee() { // `genesis`. Align the engine's chain-bound consensus domain with the // verifier by deriving it from the same genesis config digest. config.config_digest = genesis.config_digest(); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new(context.child("engine").with_attribute("uid", uid.clone()), config).await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -186,17 +188,14 @@ fn test_checkpoint_verification_fixed_committee() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } if height_reached.len() as u32 >= n { @@ -445,7 +444,7 @@ fn test_checkpoint_verification_dynamic_committee() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -477,8 +476,8 @@ fn test_checkpoint_verification_dynamic_committee() { .iter() .enumerate() .map(|(i, (node_pk, consensus_pk))| { - let node_pub_hex = commonware_utils::hex(node_pk.as_ref()); - let consensus_pub_hex = commonware_utils::hex(&consensus_pk.encode()); + let node_pub_hex = commonware_formatting::hex(node_pk.as_ref()); + let consensus_pub_hex = commonware_formatting::hex(&consensus_pk.encode()); GenesisValidator { node_public_key: format!("0x{node_pub_hex}"), consensus_public_key: format!("0x{consensus_pub_hex}"), @@ -513,8 +512,7 @@ fn test_checkpoint_verification_dynamic_committee() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -576,7 +574,11 @@ fn test_checkpoint_verification_dynamic_committee() { // `genesis`. Align the engine's chain-bound consensus domain with the // verifier by deriving it from the same genesis config digest. config.config_digest = genesis.config_digest(); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -591,21 +593,18 @@ fn test_checkpoint_verification_dynamic_committee() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { + if sample.name.ends_with("finalizer_height") { // Skip the withdrawing validator — it will exit consensus - if metric.starts_with(&withdrawing_uid) { + if sample.uid.starts_with(&withdrawing_uid) { continue; } - let height = value.parse::().unwrap(); + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } if height_reached.len() as u32 >= n - 1 { diff --git a/node/src/tests/engine.rs b/node/src/tests/engine.rs index a196b534..bf51a6c8 100644 --- a/node/src/tests/engine.rs +++ b/node/src/tests/engine.rs @@ -6,18 +6,21 @@ use crate::test_harness::common::{ register_validators, }; use crate::test_harness::mock_engine_client::MockEngineNetwork; +use commonware_actor::{Feedback, Unreliable}; use commonware_cryptography::{Signer, bls12381}; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::{ CheckedSender, LimitedSender, Message, Receiver, Recipients, simulated::{self, Network}, }; +use commonware_runtime::Supervisor as _; use commonware_runtime::{ - Clock, IoBufs, Metrics, Runner as _, + Clock, IoBufs, Runner as _, deterministic::{self, Runner}, }; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use futures::FutureExt; use rand::SeedableRng; use rand::rngs::StdRng; @@ -36,7 +39,7 @@ fn test_backfill_resolver_inherits_fetch_timeout() { let executor = Runner::from(deterministic::Config::default()); executor.start(|context| async move { let (network, oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -54,7 +57,7 @@ fn test_backfill_resolver_inherits_fetch_timeout() { consensus_key, }; - let genesis_hash: [u8; 32] = from_hex_formatted(GENESIS_HASH) + let genesis_hash: [u8; 32] = from_hex(GENESIS_HASH) .expect("failed to decode genesis hash") .try_into() .expect("failed to convert genesis hash"); @@ -76,7 +79,7 @@ fn test_backfill_resolver_inherits_fetch_timeout() { let fetch_timeout = Duration::from_secs(7); config.fetch_timeout = fetch_timeout; - let engine = Engine::new(context.with_label("engine"), config).await; + let engine = Engine::new(context.child("engine"), config).await; let resolver_config = engine.backfill_resolver_config(); assert_eq!(resolver_config.timeout, fetch_timeout); }); @@ -90,14 +93,13 @@ struct DeadBackfillCheckedSender; impl CheckedSender for DeadBackfillCheckedSender { type PublicKey = PublicKey; - type Error = std::io::Error; - async fn send( - self, - _message: impl Into + Send, - _priority: bool, - ) -> Result, Self::Error> { - Ok(Vec::new()) + fn recipients(&self) -> Vec { + Vec::new() + } + + fn send(self, _message: impl Into + Send, _priority: bool) -> Unreliable { + Unreliable::Outcome(Feedback::Ok) } } @@ -105,7 +107,7 @@ impl LimitedSender for DeadBackfillSender { type PublicKey = PublicKey; type Checked<'a> = DeadBackfillCheckedSender; - async fn check( + fn check( &mut self, _recipients: Recipients, ) -> Result, SystemTime> { @@ -140,7 +142,7 @@ fn test_engine_detects_single_actor_clean_exit() { let executor = Runner::from(deterministic::Config::default()); executor.start(|context| async move { let (network, oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -164,7 +166,7 @@ fn test_engine_detects_single_actor_clean_exit() { let (pending, recovered, resolver, broadcast, _backfill) = registrations.remove(&node_public_key).unwrap(); - let genesis_hash: [u8; 32] = from_hex_formatted(GENESIS_HASH) + let genesis_hash: [u8; 32] = from_hex(GENESIS_HASH) .expect("failed to decode genesis hash") .try_into() .expect("failed to convert genesis hash"); @@ -184,7 +186,7 @@ fn test_engine_detects_single_actor_clean_exit() { initial_state, ); - let engine = Engine::new(context.with_label("engine"), config).await; + let engine = Engine::new(context.child("engine"), config).await; let engine_handle = engine.start( pending, recovered, diff --git a/node/src/tests/execution_requests/deposit_withdrawal_combined.rs b/node/src/tests/execution_requests/deposit_withdrawal_combined.rs index 7e756b63..cc01cdf5 100644 --- a/node/src/tests/execution_requests/deposit_withdrawal_combined.rs +++ b/node/src/tests/execution_requests/deposit_withdrawal_combined.rs @@ -1,5 +1,6 @@ use super::*; use alloy_primitives::hex; +use commonware_runtime::Supervisor as _; #[test_traced("INFO")] fn test_deposit_and_withdrawal_request_single() { @@ -21,7 +22,7 @@ fn test_deposit_and_withdrawal_request_single() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -58,8 +59,7 @@ fn test_deposit_and_withdrawal_request_single() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -138,7 +138,11 @@ fn test_deposit_and_withdrawal_request_single() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -157,14 +161,11 @@ fn test_deposit_and_withdrawal_request_single() { loop { let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -228,7 +229,7 @@ fn test_deposit_and_withdrawal_request_multiple() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -265,8 +266,7 @@ fn test_deposit_and_withdrawal_request_multiple() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -356,7 +356,11 @@ fn test_deposit_and_withdrawal_request_multiple() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -375,14 +379,11 @@ fn test_deposit_and_withdrawal_request_multiple() { loop { let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -466,7 +467,7 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -501,8 +502,7 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -574,7 +574,11 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -587,14 +591,11 @@ fn test_invalid_deposit_refund_does_not_merge_with_later_withdrawal() { loop { let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -665,7 +666,7 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -700,8 +701,7 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -762,7 +762,11 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -775,14 +779,11 @@ fn test_invalid_deposit_refund_applies_invalid_deposit_tax() { loop { let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -842,7 +843,7 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -876,7 +877,7 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -952,7 +953,7 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new(context.child("engine").with_attribute("uid", uid.clone()), config).await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -965,18 +966,14 @@ fn test_invalid_deposit_refunds_do_not_delay_validator_exit_withdrawal() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1045,7 +1042,7 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1080,8 +1077,7 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1152,7 +1148,11 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1167,18 +1167,14 @@ fn test_partial_withdrawal_at_floor_dropped_while_topup_is_credited() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1251,7 +1247,7 @@ fn test_deposit_and_withdrawal_same_block() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1287,8 +1283,7 @@ fn test_deposit_and_withdrawal_same_block() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1362,7 +1357,11 @@ fn test_deposit_and_withdrawal_same_block() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1377,18 +1376,14 @@ fn test_deposit_and_withdrawal_same_block() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1469,7 +1464,7 @@ fn test_inactive_withdrawals_then_rejoin_panics_epoch_boundary() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1509,8 +1504,7 @@ fn test_inactive_withdrawals_then_rejoin_panics_epoch_boundary() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1596,7 +1590,11 @@ fn test_inactive_withdrawals_then_rejoin_panics_epoch_boundary() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = diff --git a/node/src/tests/execution_requests/deposits.rs b/node/src/tests/execution_requests/deposits.rs index b8776660..81848df7 100644 --- a/node/src/tests/execution_requests/deposits.rs +++ b/node/src/tests/execution_requests/deposits.rs @@ -1,4 +1,5 @@ use super::*; +use commonware_runtime::Supervisor as _; #[test_traced("INFO")] fn test_deposit_request_single() { @@ -18,7 +19,7 @@ fn test_deposit_request_single() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -52,8 +53,7 @@ fn test_deposit_request_single() { // Link all validators common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -102,7 +102,11 @@ fn test_deposit_request_single() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -120,14 +124,11 @@ fn test_deposit_request_single() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -184,7 +185,7 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -221,8 +222,7 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -287,7 +287,11 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -305,14 +309,11 @@ fn test_deposit_less_than_min_stake_creates_inactive_account() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -399,7 +400,7 @@ fn test_duplicate_bls_consensus_key_rejected() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -432,8 +433,7 @@ fn test_duplicate_bls_consensus_key_rejected() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -524,7 +524,11 @@ fn test_duplicate_bls_consensus_key_rejected() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -538,18 +542,14 @@ fn test_duplicate_bls_consensus_key_rejected() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -643,7 +643,7 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -676,8 +676,7 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -766,7 +765,11 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -780,18 +783,14 @@ fn test_top_up_deposit_with_mismatched_bls_key_rejected() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } diff --git a/node/src/tests/execution_requests/malformed_requests.rs b/node/src/tests/execution_requests/malformed_requests.rs index 2de3efc9..5052d382 100644 --- a/node/src/tests/execution_requests/malformed_requests.rs +++ b/node/src/tests/execution_requests/malformed_requests.rs @@ -1,6 +1,7 @@ use super::*; use alloy_eips::eip7685::Requests; use alloy_primitives::Bytes; +use commonware_runtime::Supervisor as _; /// A single-byte execution request (a bare type byte with no request_data) /// mirrors the testnet PoC, where a malicious proposer replaces the request @@ -25,7 +26,7 @@ fn test_single_byte_execution_request_block_is_rejected() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -58,8 +59,7 @@ fn test_single_byte_execution_request_block_is_rejected() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -99,7 +99,11 @@ fn test_single_byte_execution_request_block_is_rejected() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = diff --git a/node/src/tests/execution_requests/mod.rs b/node/src/tests/execution_requests/mod.rs index ab2b7daa..8d5ef5cb 100644 --- a/node/src/tests/execution_requests/mod.rs +++ b/node/src/tests/execution_requests/mod.rs @@ -17,13 +17,14 @@ pub(crate) use alloy_primitives::Address; pub(crate) use commonware_consensus::types::{Epoch, Epocher, FixedEpocher}; pub(crate) use commonware_cryptography::Signer; pub(crate) use commonware_cryptography::bls12381; +pub(crate) use commonware_formatting::from_hex; pub(crate) use commonware_macros::test_traced; pub(crate) use commonware_math::algebra::Random; pub(crate) use commonware_p2p::simulated; pub(crate) use commonware_p2p::simulated::{Link, Network}; pub(crate) use commonware_runtime::deterministic::Runner; pub(crate) use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -pub(crate) use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; pub(crate) use rand::SeedableRng; pub(crate) use rand::rngs::StdRng; pub(crate) use std::collections::{HashMap, HashSet}; diff --git a/node/src/tests/execution_requests/protocol_params.rs b/node/src/tests/execution_requests/protocol_params.rs index 7d3b6d67..287fdc03 100644 --- a/node/src/tests/execution_requests/protocol_params.rs +++ b/node/src/tests/execution_requests/protocol_params.rs @@ -1,4 +1,5 @@ use super::*; +use commonware_runtime::Supervisor as _; use summit_types::execution_request::ProtocolParamRequest; use summit_types::protocol_params::MAX_MAX_DEPOSITS_PER_EPOCH; @@ -18,7 +19,7 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -51,8 +52,7 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -93,7 +93,11 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -107,14 +111,11 @@ fn test_grouped_protocol_param_requests_in_single_eip7685_entry() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -165,7 +166,7 @@ fn test_protocol_param_allowed_timestamp_future() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -198,8 +199,7 @@ fn test_protocol_param_allowed_timestamp_future() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -244,7 +244,11 @@ fn test_protocol_param_allowed_timestamp_future() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -260,14 +264,11 @@ fn test_protocol_param_allowed_timestamp_future() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -325,7 +326,7 @@ fn test_protocol_param_treasury_address() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -359,7 +360,7 @@ fn test_protocol_param_treasury_address() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -416,7 +417,7 @@ fn test_protocol_param_treasury_address() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new(context.child("engine").with_attribute("uid", uid.clone()), config).await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -431,23 +432,19 @@ fn test_protocol_param_treasury_address() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -515,7 +512,7 @@ fn test_protocol_param_max_deposits_per_epoch() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -548,8 +545,7 @@ fn test_protocol_param_max_deposits_per_epoch() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -590,7 +586,11 @@ fn test_protocol_param_max_deposits_per_epoch() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -604,23 +604,19 @@ fn test_protocol_param_max_deposits_per_epoch() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -671,7 +667,7 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -704,8 +700,7 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -747,7 +742,11 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -761,23 +760,19 @@ fn test_protocol_param_max_deposits_per_epoch_rejected_above_max() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -839,7 +834,7 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -872,8 +867,7 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -943,7 +937,11 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -960,23 +958,19 @@ fn test_removed_validators_at_epoch_boundary_stake_bound() { let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1064,7 +1058,7 @@ fn test_stake_increase_topup_keeps_active_validator() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1097,8 +1091,7 @@ fn test_stake_increase_topup_keeps_active_validator() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1173,7 +1166,11 @@ fn test_stake_increase_topup_keeps_active_validator() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1189,23 +1186,19 @@ fn test_stake_increase_topup_keeps_active_validator() { let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1291,7 +1284,7 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1324,8 +1317,7 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1409,7 +1401,11 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1426,23 +1422,19 @@ fn test_joining_validator_activation_cancelled_on_stake_bound_force_removal() { let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } diff --git a/node/src/tests/execution_requests/validator_set.rs b/node/src/tests/execution_requests/validator_set.rs index d46d4198..d24fbf4d 100644 --- a/node/src/tests/execution_requests/validator_set.rs +++ b/node/src/tests/execution_requests/validator_set.rs @@ -1,4 +1,5 @@ use super::*; +use commonware_runtime::Supervisor as _; /// Test that verifies added_validators is correctly populated in block headers at epoch boundaries. /// @@ -26,7 +27,7 @@ fn test_added_validators_at_epoch_boundary() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -59,8 +60,7 @@ fn test_added_validators_at_epoch_boundary() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -124,7 +124,11 @@ fn test_added_validators_at_epoch_boundary() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -140,14 +144,11 @@ fn test_added_validators_at_epoch_boundary() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -241,7 +242,7 @@ fn test_removed_validators_at_epoch_boundary() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -274,8 +275,7 @@ fn test_removed_validators_at_epoch_boundary() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -344,7 +344,11 @@ fn test_removed_validators_at_epoch_boundary() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -360,14 +364,11 @@ fn test_removed_validators_at_epoch_boundary() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } diff --git a/node/src/tests/execution_requests/withdrawals.rs b/node/src/tests/execution_requests/withdrawals.rs index cee8c8ad..eca415c9 100644 --- a/node/src/tests/execution_requests/withdrawals.rs +++ b/node/src/tests/execution_requests/withdrawals.rs @@ -2,6 +2,7 @@ use super::*; use alloy_eips::eip7685::Requests; use alloy_primitives::Bytes; use commonware_codec::Write; +use commonware_runtime::Supervisor as _; #[test_traced("INFO")] fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { @@ -20,7 +21,7 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -51,8 +52,7 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -137,7 +137,11 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -151,14 +155,11 @@ fn test_grouped_withdrawal_requests_in_single_eip7685_entry() { // a metric check. let metrics = context.encode(); for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { - assert_eq!(value.parse::().unwrap(), 0); + }; + if sample.name.ends_with("_peers_blocked") { + assert_eq!(sample.value.parse::().unwrap(), 0); } } @@ -230,7 +231,7 @@ fn test_full_exit_withdrawal_removes_validator_and_pays_out() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -266,8 +267,7 @@ fn test_full_exit_withdrawal_removes_validator_and_pays_out() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -322,7 +322,11 @@ fn test_full_exit_withdrawal_removes_validator_and_pays_out() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -416,7 +420,7 @@ fn test_multiple_partial_withdrawals_paid_out_clamped_to_minimum() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -457,8 +461,7 @@ fn test_multiple_partial_withdrawals_paid_out_clamped_to_minimum() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -538,7 +541,11 @@ fn test_multiple_partial_withdrawals_paid_out_clamped_to_minimum() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -619,7 +626,7 @@ fn test_withdrawal_wrong_source_address_rejected() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -655,8 +662,7 @@ fn test_withdrawal_wrong_source_address_rejected() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -712,7 +718,11 @@ fn test_withdrawal_wrong_source_address_rejected() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -784,7 +794,7 @@ fn test_withdrawal_nonexistent_validator_ignored() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -819,8 +829,7 @@ fn test_withdrawal_nonexistent_validator_ignored() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -875,7 +884,11 @@ fn test_withdrawal_nonexistent_validator_ignored() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -890,18 +903,14 @@ fn test_withdrawal_nonexistent_validator_ignored() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -967,7 +976,7 @@ fn test_withdrawal_during_onboarding_aborts() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1000,8 +1009,7 @@ fn test_withdrawal_during_onboarding_aborts() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1078,7 +1086,11 @@ fn test_withdrawal_during_onboarding_aborts() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1093,18 +1105,14 @@ fn test_withdrawal_during_onboarding_aborts() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1177,7 +1185,7 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1209,8 +1217,7 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1267,7 +1274,11 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1280,23 +1291,19 @@ fn test_minimum_validator_count_blocks_excess_active_validator_exits() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1383,7 +1390,7 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1419,8 +1426,7 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1494,7 +1500,11 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1509,18 +1519,14 @@ fn test_withdrawal_on_last_block_of_epoch_deferred() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1613,7 +1619,7 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1645,8 +1651,7 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1708,7 +1713,11 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1721,18 +1730,14 @@ fn test_grouped_withdrawal_on_last_block_of_epoch_only_requeues_deferred_request let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -1823,7 +1828,7 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -1855,8 +1860,7 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -1922,7 +1926,11 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -1936,18 +1944,14 @@ fn test_duplicate_last_block_exit_does_not_consume_active_exit_budget() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -2074,7 +2078,7 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -2105,8 +2109,7 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { let mut registrations = common::register_validators(&oracle, &node_public_keys).await; common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -2172,7 +2175,11 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -2185,23 +2192,19 @@ fn test_withdrawal_overflow_rescheduled_to_next_epoch() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } + }; - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -2303,7 +2306,7 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -2336,8 +2339,7 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -2408,7 +2410,11 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -2422,18 +2428,14 @@ fn test_joining_validator_withdrawal_on_last_block_keeps_header_consistent() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } @@ -2537,7 +2539,7 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { let executor = Runner::from(cfg); executor.start(|context| async move { let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -2571,7 +2573,7 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { common::link_validators(&mut oracle, &node_public_keys, link, None).await; let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -2645,7 +2647,7 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new(context.child("engine").with_attribute("uid", uid.clone()), config).await; finalizer_mailboxes.insert(idx, engine.finalizer_mailbox.clone()); let (pending, recovered, resolver, orchestrator, broadcast) = @@ -2659,18 +2661,14 @@ fn test_joining_validator_withdrawal_inline_cancel_clears_status() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; - if metric.ends_with("finalizer_height") { - let height = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let height = sample.value.parse::().unwrap(); if height >= stop_height { - height_reached.insert(metric.to_string()); + height_reached.insert(sample.uid.clone()); } } diff --git a/node/src/tests/observer.rs b/node/src/tests/observer.rs index 2855af40..3aa84de2 100644 --- a/node/src/tests/observer.rs +++ b/node/src/tests/observer.rs @@ -5,13 +5,15 @@ use crate::test_harness::common::{ }; use crate::test_harness::mock_engine_client::MockEngineNetworkBuilder; use commonware_cryptography::{Signer, bls12381}; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::simulated; use commonware_p2p::simulated::{Link, Network}; +use commonware_runtime::Supervisor as _; use commonware_runtime::deterministic::Runner; use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use rand::SeedableRng; use rand::rngs::StdRng; use std::collections::HashSet; @@ -40,7 +42,7 @@ fn test_observer_reaches_end_height() { executor.start(|context| async move { let total_nodes = n_validators + 1; let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -76,7 +78,7 @@ fn test_observer_reaches_end_height() { // observers under (#335). The harness uses genesis_hash as the // config_digest (see get_default_engine_config), so derive against // chain_domain(genesis_hash) to match the validators' authorized set. - let observer_config_digest: [u8; 32] = from_hex_formatted(common::GENESIS_HASH) + let observer_config_digest: [u8; 32] = from_hex(common::GENESIS_HASH) .expect("genesis hash hex") .try_into() .expect("genesis hash len"); @@ -104,7 +106,7 @@ fn test_observer_reaches_end_height() { common::link_validators(&mut oracle, &all_pubkeys, link.clone(), None).await; // Shared genesis + engine client network. - let genesis_hash = from_hex_formatted(common::GENESIS_HASH).expect("genesis hash hex"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("genesis hash hex"); let genesis_hash: [u8; 32] = genesis_hash.try_into().expect("genesis hash len"); let engine_client_network = MockEngineNetworkBuilder::new(genesis_hash) .with_stop_at(stop_height) @@ -130,7 +132,11 @@ fn test_observer_reaches_end_height() { initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; let (pending, recovered, resolver, orchestrator, broadcast) = registrations.remove(&public_key).unwrap(); @@ -156,7 +162,13 @@ fn test_observer_reaches_end_height() { initial_state.clone(), ); observer_config.force_verifier_only = true; - let observer_engine = Engine::new(context.with_label(&observer_uid), observer_config).await; + let observer_engine = Engine::new( + context + .child("observer") + .with_attribute("uid", observer_uid.clone()), + observer_config, + ) + .await; let (pending, recovered, resolver, orchestrator, broadcast) = registrations.remove(&observer_pubkey).unwrap(); observer_engine.start(pending, recovered, resolver, orchestrator, broadcast); @@ -167,23 +179,23 @@ fn test_observer_reaches_end_height() { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !(line.starts_with("validator_") || line.starts_with("observer_")) { + let Some(sample) = common::parse_metric(line) else { + continue; + }; + if !(sample.uid.starts_with("validator_") || sample.uid.starts_with("observer_")) { continue; } - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - if metric.ends_with("_peers_blocked") { + if sample.name.ends_with("_peers_blocked") { assert_eq!( - value.parse::().unwrap(), + sample.value.parse::().unwrap(), 0, "no node should have blocked peers" ); } - if metric.ends_with("finalizer_height") - && value.parse::().unwrap() >= stop_height + if sample.name.ends_with("finalizer_height") + && sample.value.parse::().unwrap() >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() as u32 >= total_nodes { success = true; break; @@ -233,7 +245,7 @@ fn test_observer_backfills_from_parent_validator() { executor.start(|context| async move { let total_nodes = n_validators + 1; let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -273,7 +285,7 @@ fn test_observer_backfills_from_parent_validator() { // observers under (#335). The harness uses genesis_hash as the // config_digest (see get_default_engine_config), so derive against // chain_domain(genesis_hash) to match the validators' authorized set. - let observer_config_digest: [u8; 32] = from_hex_formatted(common::GENESIS_HASH) + let observer_config_digest: [u8; 32] = from_hex(common::GENESIS_HASH) .expect("genesis hash hex") .try_into() .expect("genesis hash len"); @@ -298,7 +310,7 @@ fn test_observer_backfills_from_parent_validator() { .await; // Shared genesis + engine client network. - let genesis_hash = from_hex_formatted(common::GENESIS_HASH).expect("genesis hash hex"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("genesis hash hex"); let genesis_hash: [u8; 32] = genesis_hash.try_into().expect("genesis hash len"); let engine_client_network = MockEngineNetworkBuilder::new(genesis_hash) .with_stop_at(stop_height) @@ -323,7 +335,11 @@ fn test_observer_backfills_from_parent_validator() { initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; let (pending, recovered, resolver, orchestrator, broadcast) = registrations.remove(&public_key).unwrap(); @@ -338,13 +354,11 @@ fn test_observer_backfills_from_parent_validator() { let metrics = context.encode(); let advanced = metrics .lines() - .filter(|l| l.starts_with("validator_")) - .filter(|l| { - let mut parts = l.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - metric.ends_with("finalizer_height") - && value.parse::().unwrap() >= join_height + .filter_map(common::parse_metric) + .filter(|sample| { + sample.uid.starts_with("validator_") + && sample.name.ends_with("finalizer_height") + && sample.value.parse::().unwrap() >= join_height }) .count(); if advanced as u32 >= n_validators { @@ -371,23 +385,26 @@ fn test_observer_backfills_from_parent_validator() { ); observer_config.force_verifier_only = true; observer_config.observer_network_key = Some(observer_pubkey.clone()); - let observer_engine = Engine::new(context.with_label(&observer_uid), observer_config).await; + let observer_engine = Engine::new( + context + .child("observer") + .with_attribute("uid", observer_uid.clone()), + observer_config, + ) + .await; let (pending, recovered, resolver, orchestrator, broadcast) = registrations.remove(&observer_pubkey).unwrap(); observer_engine.start(pending, recovered, resolver, orchestrator, broadcast); // The observer must backfill the missed blocks from its parent — the // only peer it is linked to — and reach stop_height. - let observer_height_metric = format!("{observer_uid}_finalizer_height"); let mut polls = 0; loop { let metrics = context.encode(); - let observer_done = metrics.lines().any(|l| { - let mut parts = l.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); - metric.ends_with(&observer_height_metric) - && value.parse::().unwrap() >= stop_height + let observer_done = metrics.lines().filter_map(common::parse_metric).any(|s| { + s.uid == observer_uid + && s.name.ends_with("finalizer_height") + && s.value.parse::().unwrap() >= stop_height }); if observer_done { break; diff --git a/node/src/tests/syncer.rs b/node/src/tests/syncer.rs index d878db1f..dd9fca47 100644 --- a/node/src/tests/syncer.rs +++ b/node/src/tests/syncer.rs @@ -4,13 +4,15 @@ use crate::test_harness::common::DEFAULT_BLOCKS_PER_EPOCH; use crate::test_harness::common::{SimulatedOracle, get_default_engine_config, get_initial_state}; use crate::test_harness::mock_engine_client::MockEngineNetworkBuilder; use commonware_cryptography::{Signer, bls12381}; +use commonware_formatting::from_hex; use commonware_macros::test_traced; use commonware_math::algebra::Random; use commonware_p2p::simulated; use commonware_p2p::simulated::{Link, Network}; +use commonware_runtime::Supervisor as _; use commonware_runtime::deterministic::Runner; use commonware_runtime::{Clock, Metrics, Runner as _, deterministic}; -use commonware_utils::{NZUsize, from_hex_formatted}; +use commonware_utils::NZUsize; use rand::SeedableRng; use rand::rngs::StdRng; use std::collections::{HashMap, HashSet}; @@ -34,7 +36,7 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -76,8 +78,7 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { common::register_validators(&oracle, &initial_node_public_keys).await; common::link_validators(&mut oracle, &initial_node_public_keys, link.clone(), None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -116,7 +117,11 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { validators.clone(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -172,7 +177,11 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { validators.clone(), initial_state, // pass initial state (start from genesis) ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; // Get networking from late registrations let (pending, recovered, resolver, orchestrator, broadcast) = @@ -189,26 +198,20 @@ fn test_node_joins_later_no_checkpoint_in_genesis() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let value = sample.value.parse::().unwrap(); if value >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() as u32 == n { success = true; break; @@ -260,7 +263,7 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { executor.start(|context| async move { // Create simulated network let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: false, @@ -301,8 +304,7 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { common::register_validators(&oracle, &initial_node_public_keys).await; common::link_validators(&mut oracle, &initial_node_public_keys, link.clone(), None).await; // Create the engine clients - let genesis_hash = - from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash = from_hex(common::GENESIS_HASH).expect("failed to decode genesis hash"); let genesis_hash: [u8; 32] = genesis_hash .try_into() .expect("failed to convert genesis hash"); @@ -341,7 +343,11 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { initial_validators.to_vec(), initial_state.clone(), ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); // Get networking @@ -399,7 +405,11 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { initial_validators.to_vec(), initial_state, // pass initial state (start from genesis) ); - let engine = Engine::new(context.with_label(&uid), config).await; + let engine = Engine::new( + context.child("engine").with_attribute("uid", uid.clone()), + config, + ) + .await; // Get networking from late registrations let (pending, recovered, resolver, orchestrator, broadcast) = @@ -416,27 +426,21 @@ fn test_node_joins_later_no_checkpoint_not_in_genesis() { // Iterate over all lines let mut success = false; for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { + let Some(sample) = common::parse_metric(line) else { continue; - } - - // Split metric and value - let mut parts = line.split_whitespace(); - let metric = parts.next().unwrap(); - let value = parts.next().unwrap(); + }; // If ends with peers_blocked, ensure it is zero - if metric.ends_with("_peers_blocked") { - let value = value.parse::().unwrap(); - println!("{} -> {}", metric, value); + if sample.name.ends_with("_peers_blocked") { + let value = sample.value.parse::().unwrap(); + println!("{} {} -> {}", sample.uid, sample.name, value); assert_eq!(value, 0); } - if metric.ends_with("finalizer_height") { - let value = value.parse::().unwrap(); + if sample.name.ends_with("finalizer_height") { + let value = sample.value.parse::().unwrap(); if value >= stop_height { - nodes_finished.insert(metric.to_string()); + nodes_finished.insert(sample.uid.clone()); if nodes_finished.len() as u32 == n { success = true; break; diff --git a/orchestrator/Cargo.toml b/orchestrator/Cargo.toml index 7eb14c9e..51b73bce 100644 --- a/orchestrator/Cargo.toml +++ b/orchestrator/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true summit-types.workspace = true summit-syncer.workspace = true +commonware-actor.workspace = true commonware-broadcast.workspace = true commonware-codec.workspace = true commonware-consensus.workspace = true diff --git a/orchestrator/src/actor.rs b/orchestrator/src/actor.rs index b249376e..d799b533 100644 --- a/orchestrator/src/actor.rs +++ b/orchestrator/src/actor.rs @@ -21,13 +21,13 @@ use commonware_runtime::{ use commonware_utils::{NZU16, NZUsize, vec::NonEmptyVec}; use futures::{StreamExt, channel::mpsc}; use governor::clock::Clock as GClock; -use rand_core::CryptoRngCore; +use rand_core::CryptoRng; use std::{ collections::BTreeMap, sync::{Arc, RwLock}, time::Duration, }; -use summit_types::scheme::{EpochSchemeProvider, MultisigScheme}; +use summit_types::scheme::{EpochGenesisProvider, EpochSchemeProvider, MultisigScheme}; use crate::committee_filter::{ActiveCommittees, CommitteeFilteredReceiver}; use tracing::info; @@ -37,7 +37,8 @@ pub struct Config where B: Blocker, A: CertifiableAutomaton, Digest = Digest> - + Relay>, + + Relay> + + EpochGenesisProvider, St: Strategy + Default, ES: Epocher, { @@ -68,15 +69,16 @@ where pub struct Actor where - E: BufferPooler + Spawner + Metrics + CryptoRngCore + Clock + GClock + Storage + Network, + E: BufferPooler + Spawner + Metrics + CryptoRng + Clock + GClock + Storage + Network, B: Blocker, A: CertifiableAutomaton, Digest = Digest> - + Relay>, + + Relay> + + EpochGenesisProvider, St: Strategy + Default, ES: Epocher, { context: ContextCell, - mailbox: mpsc::Receiver, + mailbox: mpsc::UnboundedReceiver, application: A, oracle: B, @@ -101,15 +103,16 @@ where impl Actor where - E: BufferPooler + Spawner + Metrics + CryptoRngCore + Clock + GClock + Storage + Network, + E: BufferPooler + Spawner + Metrics + CryptoRng + Clock + GClock + Storage + Network, B: Blocker, A: CertifiableAutomaton, Digest = Digest> - + Relay>, + + Relay> + + EpochGenesisProvider, St: Strategy + Default, ES: Epocher, { pub fn new(context: E, config: Config) -> (Self, Mailbox) { - let (sender, mailbox) = mpsc::channel(config.mailbox_size); + let (sender, mailbox) = mpsc::unbounded(); let page_cache = CacheRef::from_pooler(&context, NZU16!(16_384), NZUsize!(10_000)); ( @@ -151,7 +154,7 @@ where impl Receiver, ), ) -> Handle<()> { - spawn_cell!(self.context, self.run(pending, recovered, resolver).await) + spawn_cell!(self.context, self.run(pending, recovered, resolver)) } async fn run( @@ -187,7 +190,7 @@ where // Start muxers for each physical channel used by consensus let (mux, mut pending_mux, mut pending_backup) = Muxer::builder( - self.context.with_label("pending_mux"), + self.context.child("pending_mux"), pending_sender, pending_receiver, self.muxer_size, @@ -196,14 +199,14 @@ where .build(); mux.start(); let (mux, mut recovered_mux) = Muxer::new( - self.context.with_label("recovered_mux"), + self.context.child("recovered_mux"), recovered_sender, recovered_receiver, self.muxer_size, ); mux.start(); let (mux, mut resolver_mux) = Muxer::new( - self.context.with_label("resolver_mux"), + self.context.child("resolver_mux"), resolver_sender, resolver_receiver, self.muxer_size, @@ -240,11 +243,10 @@ where let boundary_height = self.epocher.last(our_epoch).expect("epoch should exist"); // Non-blocking: this advisory catch-up hint must not park the // orchestrator loop on a full syncer mailbox, or epoch Enter/Exit - // (processed by the arm below) would wait behind it. Dropping the - // hint under syncer backpressure is fine: the ahead peer keeps - // re-advertising the later epoch and the finalization also arrives - // through the normal flow. - self.syncer_mailbox.try_hint_finalized(boundary_height, NonEmptyVec::new(from)); + // (processed by the arm below) would wait behind it. Enqueueing is + // synchronous: when the syncer mailbox is full, hints are coalesced + // per height in the mailbox overflow state instead of blocking. + self.syncer_mailbox.hint_finalized(boundary_height, NonEmptyVec::new(from)); }, transition = self.mailbox.next() => { let Some(transition) = transition else { @@ -338,11 +340,16 @@ where impl Receiver, >, ) -> Handle<()> { + // Fetch the epoch's genesis payload: consensus no longer queries the + // automaton for it and instead takes the certified root via `floor`. + let genesis = self.application.genesis(epoch).await; + // Start the new engine let elector = simplex::elector::RoundRobin::::default(); let engine = simplex::Engine::new( self.context - .with_label(&format!("consensus_engine_{}", epoch)), + .child("consensus_engine") + .with_attribute("epoch", epoch), simplex::Config { scheme, elector, @@ -352,8 +359,9 @@ where reporter: self.syncer_mailbox.clone(), strategy: St::default(), partition: format!("{}_consensus_{}", self.partition_prefix, epoch), - mailbox_size: 1024, + mailbox_size: NZUsize!(1024), epoch, + floor: simplex::Floor::Genesis(genesis), replay_buffer: NZUsize!(1024 * 1024), write_buffer: NZUsize!(1024 * 1024), leader_timeout: self.leader_timeout, @@ -362,7 +370,7 @@ where fetch_timeout: self.fetch_timeout, activity_timeout: self.activity_timeout, skip_timeout: self.skip_timeout, - fetch_concurrent: 2, + fetch_concurrent: NZUsize!(2), page_cache: self.page_cache.clone(), forwarding: simplex::ForwardingPolicy::SilentVoters, }, diff --git a/orchestrator/src/ingress.rs b/orchestrator/src/ingress.rs index 4073a85e..b761edb7 100644 --- a/orchestrator/src/ingress.rs +++ b/orchestrator/src/ingress.rs @@ -1,7 +1,8 @@ //! Inbound communication channel for epoch transitions. +use commonware_actor::Feedback; use commonware_consensus::{Reporter, types::Epoch}; -use futures::{SinkExt, channel::mpsc}; +use futures::channel::mpsc; use summit_types::scheme::EpochTransition; /// Messages that can be sent to the orchestrator. @@ -13,12 +14,12 @@ pub enum Message { /// Inbound communication channel for epoch transitions. #[derive(Debug, Clone)] pub struct Mailbox { - sender: mpsc::Sender, + sender: mpsc::UnboundedSender, } impl Mailbox { /// Create a new [Mailbox]. - pub fn new(sender: mpsc::Sender) -> Self { + pub fn new(sender: mpsc::UnboundedSender) -> Self { Self { sender } } } @@ -26,10 +27,10 @@ impl Mailbox { impl Reporter for Mailbox { type Activity = Message; - async fn report(&mut self, activity: Self::Activity) { + fn report(&mut self, activity: Self::Activity) -> Feedback { self.sender - .send(activity) - .await - .expect("failed to send epoch transition") + .unbounded_send(activity) + .expect("failed to send epoch transition"); + Feedback::Ok } } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index bcfd0c13..e2fe0d68 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -19,6 +19,7 @@ async-trait = "0.1" jsonrpsee = { version = "0.26.0", features = ["server", "client", "macros"] } jsonrpsee-core = "0.26.0" jsonrpsee-types = "0.26.0" +commonware-formatting.workspace = true commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } commonware-utils = { workspace = true } diff --git a/rpc/src/auth.rs b/rpc/src/auth.rs index 3186fb06..25f75af8 100644 --- a/rpc/src/auth.rs +++ b/rpc/src/auth.rs @@ -1,6 +1,6 @@ use crate::error::RpcError; use alloy_primitives::{Address, Signature}; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; @@ -60,7 +60,7 @@ pub(crate) fn verify_action_with( return Err(RpcError::TimestampOutOfWindow); } - let sig_bytes = from_hex_formatted(signature_hex).ok_or(RpcError::InvalidSignature)?; + let sig_bytes = from_hex(signature_hex).ok_or(RpcError::InvalidSignature)?; let signature = Signature::from_raw(&sig_bytes).map_err(|_| RpcError::InvalidSignature)?; let message = format!("{DOMAIN}:{scope}:{action}:{timestamp_secs}"); diff --git a/rpc/src/builder.rs b/rpc/src/builder.rs index da67c815..69ded1ab 100644 --- a/rpc/src/builder.rs +++ b/rpc/src/builder.rs @@ -1,4 +1,4 @@ -use http::{HeaderValue, Method}; +use http::{HeaderValue, Method, StatusCode}; use jsonrpsee::server::{BatchRequestConfig, ServerBuilder, ServerConfigBuilder, ServerHandle}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; @@ -122,9 +122,13 @@ impl RpcServerBuilder { .map(create_cors_layer) .transpose()?; - let http_middleware = ServiceBuilder::new() - .option_layer(cors_layer) - .layer(TimeoutLayer::new(self.request_timeout)); + let http_middleware = + ServiceBuilder::new() + .option_layer(cors_layer) + .layer(TimeoutLayer::with_status_code( + StatusCode::REQUEST_TIMEOUT, + self.request_timeout, + )); let server = ServerBuilder::new() .set_config(self.config.build()) diff --git a/rpc/src/server.rs b/rpc/src/server.rs index 8d2798ad..d76dd4f2 100644 --- a/rpc/src/server.rs +++ b/rpc/src/server.rs @@ -14,7 +14,7 @@ use alloy_primitives::{Address, U256, hex::FromHex as _}; use async_trait::async_trait; use commonware_codec::{DecodeExt as _, Encode as _}; use commonware_cryptography::Signer; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; use jsonrpsee::core::RpcResult; use ssz::Encode as _; use std::sync::Arc; @@ -244,7 +244,7 @@ impl SummitApiServer for SummitRpcServer { } async fn get_validator_balance(&self, public_key: String) -> RpcResult { - let key_bytes = from_hex_formatted(&public_key) + let key_bytes = from_hex(&public_key) .ok_or_else(|| RpcError::InvalidPublicKey("Invalid hex format".to_string()))?; let public_key = PublicKey::decode(&*key_bytes) @@ -262,7 +262,7 @@ impl SummitApiServer for SummitRpcServer { &self, public_key: String, ) -> RpcResult { - let key_bytes = from_hex_formatted(&public_key) + let key_bytes = from_hex(&public_key) .ok_or_else(|| RpcError::InvalidPublicKey("Invalid hex format".to_string()))?; let public_key = PublicKey::decode(&*key_bytes) @@ -343,7 +343,7 @@ impl SummitApiServer for SummitRpcServer { &self, public_key: String, ) -> RpcResult { - let key_bytes = from_hex_formatted(&public_key) + let key_bytes = from_hex(&public_key) .ok_or_else(|| RpcError::InvalidPublicKey("Invalid hex format".to_string()))?; let pubkey: [u8; 32] = key_bytes diff --git a/rpc/tests/utils.rs b/rpc/tests/utils.rs index 387d1bd1..54d1a305 100644 --- a/rpc/tests/utils.rs +++ b/rpc/tests/utils.rs @@ -217,13 +217,13 @@ pub fn create_test_keystore() -> anyhow::Result { // Generate ed25519 node key (deterministic for testing) let mut rng = StdRng::seed_from_u64(0); let node_private_key = ed25519::PrivateKey::random(&mut rng); - let encoded_node_key = commonware_utils::hex(&node_private_key.encode()); + let encoded_node_key = commonware_formatting::hex(&node_private_key.encode()); let node_key_path = temp_dir.path().join("node_key.pem"); fs::write(node_key_path, encoded_node_key)?; // Generate BLS consensus key (deterministic for testing) let consensus_private_key = bls12381::PrivateKey::random(&mut rng); - let encoded_consensus_key = commonware_utils::hex(&consensus_private_key.encode()); + let encoded_consensus_key = commonware_formatting::hex(&consensus_private_key.encode()); let consensus_key_path = temp_dir.path().join("consensus_key.pem"); fs::write(consensus_key_path, encoded_consensus_key)?; diff --git a/syncer/Cargo.toml b/syncer/Cargo.toml index 25540bbc..a28928a3 100644 --- a/syncer/Cargo.toml +++ b/syncer/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] summit-types.workspace = true +commonware-actor.workspace = true commonware-broadcast.workspace = true commonware-codec.workspace = true commonware-consensus.workspace = true diff --git a/syncer/src/acks.rs b/syncer/src/acks.rs new file mode 100644 index 00000000..aa38c0fd --- /dev/null +++ b/syncer/src/acks.rs @@ -0,0 +1,190 @@ +use commonware_consensus::{Block, types::Height}; +use commonware_utils::{Acknowledgement, futures::OptionFuture}; +use futures::FutureExt; +use pin_project::pin_project; +use std::{ + collections::VecDeque, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +/// A pending acknowledgement from the application for a block at the contained height/commitment. +#[pin_project] +pub(crate) struct PendingAck { + pub(crate) height: Height, + pub(crate) commitment: B::Digest, + #[pin] + pub(crate) receiver: A::Waiter, +} + +impl Future for PendingAck { + type Output = ::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().receiver.poll(cx) + } +} + +/// Tracks in-flight application acknowledgements with FIFO semantics. +pub(crate) struct PendingAcks { + current: OptionFuture>, + queue: VecDeque>, + max: usize, +} + +impl PendingAcks { + /// Creates a new pending-ack tracker with a maximum in-flight capacity. + pub(crate) fn new(max: usize) -> Self { + Self { + current: None.into(), + queue: VecDeque::with_capacity(max), + max, + } + } + + /// Drops the current ack and all queued acks. + pub(crate) fn clear(&mut self) { + self.current = None.into(); + self.queue.clear(); + } + + /// Returns the currently armed ack future (if any) for `select_loop!`. + pub(crate) const fn current(&mut self) -> &mut OptionFuture> { + &mut self.current + } + + /// Returns whether we can dispatch another block without exceeding capacity. + pub(crate) fn has_capacity(&self) -> bool { + let reserved = usize::from(self.current.is_some()); + self.queue.len() < self.max - reserved + } + + /// Returns the next height to dispatch while preserving sequential order. + pub(crate) fn next_dispatch_height(&self, start_height: Height) -> Height { + self.queue + .back() + .map(|ack| ack.height.next()) + .or_else(|| self.current.as_ref().map(|ack| ack.height.next())) + .unwrap_or(start_height) + } + + /// Enqueues a newly dispatched ack, arming it immediately when idle. + pub(crate) fn enqueue(&mut self, ack: PendingAck) { + if self.current.is_none() { + self.current.replace(ack); + return; + } + self.queue.push_back(ack); + } + + /// Returns metadata for a completed current ack and arms the next queued ack. + pub(crate) fn complete_current( + &mut self, + result: ::Output, + ) -> (Height, B::Digest, ::Output) { + let PendingAck { + height, commitment, .. + } = self.current.take().expect("ack state must be present"); + if let Some(next) = self.queue.pop_front() { + self.current.replace(next); + } + (height, commitment, result) + } + + /// If the current ack is already resolved, takes it and arms the next ack. + pub(crate) fn pop_ready( + &mut self, + ) -> Option<(Height, B::Digest, ::Output)> { + let pending = self.current.as_mut()?; + let result = Pin::new(&mut pending.receiver).now_or_never()?; + Some(self.complete_current(result)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mocks::block::Block as MockBlock; + use commonware_cryptography::Hasher; + use commonware_cryptography::sha256::{Digest, Sha256}; + use commonware_utils::acknowledgement::Exact; + + type TestBlock = MockBlock; + + fn digest(byte: u8) -> Digest { + let mut hasher = Sha256::new(); + hasher.update(&[byte]); + hasher.finalize() + } + + fn pending_ack(height: u64, byte: u8) -> (PendingAck, Exact) { + let (ack, receiver) = Exact::handle(); + ( + PendingAck { + height: Height::new(height), + commitment: digest(byte), + receiver, + }, + ack, + ) + } + + #[test] + fn enqueue_tracks_capacity_and_fifo_ready_order() { + let mut pending = PendingAcks::::new(2); + assert!(pending.has_capacity()); + assert_eq!(pending.next_dispatch_height(Height::new(8)), Height::new(8)); + + let (first, first_ack) = pending_ack(8, 1); + pending.enqueue(first); + assert!(pending.has_capacity()); + assert_eq!(pending.next_dispatch_height(Height::new(8)), Height::new(9)); + + let (second, second_ack) = pending_ack(9, 2); + pending.enqueue(second); + assert!(!pending.has_capacity()); + assert_eq!( + pending.next_dispatch_height(Height::new(8)), + Height::new(10) + ); + + second_ack.acknowledge(); + assert!(pending.pop_ready().is_none()); + + first_ack.acknowledge(); + let (height, commitment, result) = pending.pop_ready().expect("first ack should be ready"); + assert_eq!(height, Height::new(8)); + assert_eq!(commitment, digest(1)); + assert!(result.is_ok()); + + let (height, commitment, result) = pending + .pop_ready() + .expect("queued ready ack should be armed next"); + assert_eq!(height, Height::new(9)); + assert_eq!(commitment, digest(2)); + assert!(result.is_ok()); + assert!(pending.has_capacity()); + } + + #[test] + fn clear_drops_all_pending_acks() { + let mut pending = PendingAcks::::new(2); + let (first, first_ack) = pending_ack(3, 1); + let (second, second_ack) = pending_ack(4, 2); + pending.enqueue(first); + pending.enqueue(second); + assert!(!pending.has_capacity()); + + pending.clear(); + first_ack.acknowledge(); + second_ack.acknowledge(); + + assert!(pending.pop_ready().is_none()); + assert!(pending.has_capacity()); + assert_eq!( + pending.next_dispatch_height(Height::new(10)), + Height::new(10) + ); + } +} diff --git a/syncer/src/actor.rs b/syncer/src/actor.rs index fa287dcd..d053235d 100644 --- a/syncer/src/actor.rs +++ b/syncer/src/actor.rs @@ -1,15 +1,22 @@ use super::{ + acks::{PendingAck, PendingAcks}, cache, config::{Config, SyncCheckpoint, SyncStart}, + delivery::PendingVerification, + durability::{DispatchGate, Durable as _}, + floor::Floor, ingress::{ - handler::{self, Request}, - mailbox::{Mailbox, Message}, + handler::{self, Annotation, Key, Request}, + mailbox::{Identifier as BlockID, Mailbox, Message}, }, + stream::Stream, }; -use crate::{Update, ingress::mailbox::Identifier as BlockID, variant::Buffer as _}; +use crate::{Update, variant::Buffer as _}; use bytes::Bytes; +use commonware_actor::mailbox; use commonware_broadcast::buffered; use commonware_codec::{Decode, Encode}; +use commonware_consensus::marshal::store::{Blocks, Certificates}; use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::simplex::types::{ Finalization, Notarization, Subject, verify_certificates, @@ -17,162 +24,62 @@ use commonware_consensus::simplex::types::{ use commonware_consensus::types::{Epoch, Epocher, Height, Round, View, ViewDelta}; use commonware_consensus::{Block, Epochable, Reporter, Viewable}; use commonware_cryptography::PublicKey; -use commonware_cryptography::certificate::Scheme as CertificateScheme; +use commonware_cryptography::certificate::{Provider, Verifier as CertificateVerifier}; use commonware_macros::select_loop; use commonware_p2p::Recipients; use commonware_parallel::Strategy; -use commonware_resolver::Resolver; +use commonware_resolver::{Delivery, Resolver, TargetedResolver}; use commonware_runtime::{ BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage, spawn_cell, + telemetry::metrics::{Gauge, GaugeExt, MetricsExt as _}, }; use commonware_storage::archive::Identifier as ArchiveID; use commonware_utils::{ Acknowledgement, BoxedError, - channel::{mpsc, oneshot}, - futures::{AbortablePool, Aborter}, - sequence::U64, + acknowledgement::Exact, + channel::{fallible::OneshotExt, oneshot}, + futures::{AbortablePool, Aborter, Pool}, +}; +use futures::{ + future::{join, join_all}, + try_join, }; -use pin_project::pin_project; -use summit_types::utils::is_last_block_of_epoch; - -use commonware_consensus::marshal::store::{Blocks, Certificates}; -use commonware_cryptography::certificate::Provider; -use commonware_storage::metadata; -use commonware_storage::metadata::Metadata; -use commonware_utils::acknowledgement::Exact; -use commonware_utils::channel::fallible::OneshotExt; -use commonware_utils::futures::OptionFuture; -use futures::{FutureExt, future::join_all, try_join}; use governor::clock::Clock as GClock; #[cfg(feature = "prom")] use metrics::{counter, histogram}; -#[cfg(feature = "prom")] -use prometheus_client::metrics::gauge::Gauge; -use rand_core::CryptoRngCore; -use std::collections::VecDeque; +use rand_core::CryptoRng; +use std::collections::{BTreeMap, BTreeSet, btree_map::Entry}; use std::num::NonZeroUsize; -use std::pin::Pin; use std::sync::Arc; #[cfg(feature = "prom")] use std::time::Instant; -use std::{ - collections::{BTreeMap, btree_map::Entry}, - future::Future, -}; -use tracing::{debug, error, info, warn}; - use summit_types::Digest; +use summit_types::utils::is_last_block_of_epoch; +use tracing::{debug, error, info, warn}; -/// The key used to store the last processed height in the metadata store. -const LATEST_KEY: U64 = U64::new(0xFF); - -/// A parsed-but-unverified resolver delivery awaiting batch certificate verification. -enum PendingVerification { - Notarized { - notarization: Notarization, - block: B, - response: oneshot::Sender, - }, - Finalized { - finalization: Finalization, - block: B, - response: oneshot::Sender, - }, -} - -/// A pending acknowledgement from the application for processing a block at the contained height/commitment. -#[pin_project] -struct PendingAck { - height: Height, - commitment: B::Digest, - #[pin] - receiver: A::Waiter, -} - -impl Future for PendingAck { - type Output = ::Output; - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - self.project().receiver.poll(cx) - } +/// A resolver delivery plus the peer-validity response channel. Local +/// annotations on the delivery decide how accepted data is used. +struct ResolverDelivery { + delivery: Delivery, Annotation>, + value: Bytes, + response: oneshot::Sender, } -/// Tracks in-flight application acknowledgements with FIFO semantics. -struct PendingAcks { - current: OptionFuture>, - queue: VecDeque>, - max: usize, +/// Completion produced by the actor's independent durability pool. +enum PooledSync { + Observed, + Finalized(u64), + Notarized(B), } -impl PendingAcks { - /// Creates a new pending-ack tracker with a maximum in-flight capacity. - fn new(max: usize) -> Self { - Self { - current: None.into(), - queue: VecDeque::with_capacity(max), - max, - } - } - - /// Drops the current ack and all queued acks. - fn clear(&mut self) { - self.current = None.into(); - self.queue.clear(); - } - - /// Returns the currently armed ack future (if any) for `select_loop!`. - const fn current(&mut self) -> &mut OptionFuture> { - &mut self.current - } - - /// Returns whether we can dispatch another block without exceeding capacity. - fn has_capacity(&self) -> bool { - let reserved = usize::from(self.current.is_some()); - self.queue.len() < self.max - reserved - } - - /// Returns the next height to dispatch while preserving sequential order. - fn next_dispatch_height(&self, last_processed_height: Height) -> Height { - self.queue - .back() - .map(|ack| ack.height.next()) - .or_else(|| self.current.as_ref().map(|ack| ack.height.next())) - .unwrap_or_else(|| last_processed_height.next()) - } - - /// Enqueues a newly dispatched ack, arming it immediately when idle. - fn enqueue(&mut self, ack: PendingAck) { - if self.current.is_none() { - self.current.replace(ack); - return; - } - self.queue.push_back(ack); - } - - /// Returns metadata for a completed current ack and arms the next queued ack. - fn complete_current( - &mut self, - result: ::Output, - ) -> (Height, B::Digest, ::Output) { - let PendingAck { - height, commitment, .. - } = self.current.take().expect("ack state must be present"); - if let Some(next) = self.queue.pop_front() { - self.current.replace(next); - } - (height, commitment, result) - } - - /// If the current ack is already resolved, takes it and arms the next ack. - fn pop_ready(&mut self) -> Option<(Height, B::Digest, ::Output)> { - let pending = self.current.as_mut()?; - let result = Pin::new(&mut pending.receiver).now_or_never()?; - Some(self.complete_current(result)) - } -} +/// 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< + Result< + (::Digest, B), + ::Digest, + >, +>; /// A struct that holds multiple subscriptions for a block. struct BlockSubscription { @@ -219,7 +126,7 @@ fn header_view_binds_to_round( /// behind. pub struct Actor where - E: BufferPooler + CryptoRngCore + Spawner + Metrics + Clock + GClock + Storage, + E: BufferPooler + CryptoRng + Spawner + Metrics + Clock + GClock + Storage, B: Block + Epochable + Viewable, P: Provider>, FC: Certificates, @@ -233,7 +140,7 @@ where // ---------- Message Passing ---------- // Mailbox - mailbox: mpsc::Receiver>, + mailbox: mailbox::Receiver>, // ---------- Configuration ---------- // Provider for epoch-specific signing schemes @@ -250,22 +157,26 @@ where strategy: T, // ---------- State ---------- - // Last view processed - last_processed_round: Round, - // Last height processed by the application - last_processed_height: Height, + // Last proposed block + last_proposed_block: Option<(Round, B::Digest, B)>, + // Current processed floor and any pending floor update + floor: Floor, + // Application delivery cursor + stream: Stream, // Pending application acknowledgements pending_acks: PendingAcks, // Highest known finalized height tip: Height, // Outstanding subscriptions for blocks block_subscriptions: BTreeMap>, + // Finalized archive writes awaiting a covering sync + dispatch_gate: DispatchGate, + // Blocks whose durable notarized update must precede finalized delivery + pending_notarized_reports: BTreeSet, // ---------- Storage ---------- // Prunable cache cache: cache::Manager, - // Metadata tracking application progress - application_metadata: Metadata, // Finalizations stored by height finalizations_by_height: FC, // Finalized blocks stored by height @@ -273,16 +184,14 @@ where // ---------- Metrics ---------- // Latest height metric - #[cfg(feature = "prom")] finalized_height: Gauge, // Latest processed height - #[cfg(feature = "prom")] processed_height: Gauge, } impl Actor where - E: BufferPooler + CryptoRngCore + Spawner + Metrics + Clock + GClock + Storage, + E: BufferPooler + CryptoRng + Spawner + Metrics + Clock + GClock + Storage, B: Block + Epochable + Viewable, P: Provider>, FC: Certificates, @@ -308,93 +217,53 @@ where key_page_cache: config.page_cache.clone(), }; let cache = cache::Manager::init( - context.with_label("cache"), + context.child("cache"), prunable_config, config.block_codec_config.clone(), ) .await; // Initialize metadata tracking application progress - let application_metadata = Metadata::init( - context.with_label("application_metadata"), - metadata::Config { - partition: format!("{}-application-metadata", config.partition_prefix), - codec_config: (), - }, + let application_metadata_partition = + format!("{}-application-metadata", config.partition_prefix); + let stream = Stream::new( + context.child("application_metadata"), + &application_metadata_partition, ) - .await - .expect("failed to initialize application metadata"); + .await; // Create metrics - #[cfg(feature = "prom")] - { - let finalized_height = Gauge::default(); - context.register( - "finalized_height", - "Finalized height of application", - finalized_height.clone(), - ); - let processed_height = Gauge::default(); - context.register( - "processed_height", - "Processed height of application", - processed_height.clone(), - ); - - // Initialize mailbox - let (sender, mailbox) = mpsc::channel(config.mailbox_size); - ( - Self { - context: ContextCell::new(context), - mailbox, - provider: config.scheme_provider, - epocher: config.epocher, - view_retention_timeout: config.view_retention_timeout, - max_repair: config.max_repair, - block_codec_config: config.block_codec_config, - strategy: config.strategy, - last_processed_round: Round::zero(), - last_processed_height: Height::zero(), - pending_acks: PendingAcks::new(config.max_pending_acks.get()), - tip: Height::zero(), - block_subscriptions: BTreeMap::new(), - cache, - application_metadata, - finalizations_by_height, - finalized_blocks, - finalized_height, - processed_height, - }, - Mailbox::new(sender), - ) - } - #[cfg(not(feature = "prom"))] - { - // Initialize mailbox - let (sender, mailbox) = mpsc::channel(config.mailbox_size); - ( - Self { - context: ContextCell::new(context), - mailbox, - provider: config.scheme_provider, - epocher: config.epocher, - view_retention_timeout: config.view_retention_timeout, - max_repair: config.max_repair, - block_codec_config: config.block_codec_config, - strategy: config.strategy, - last_processed_round: Round::zero(), - last_processed_height: Height::zero(), - pending_acks: PendingAcks::new(config.max_pending_acks.get()), - tip: Height::zero(), - block_subscriptions: BTreeMap::new(), - cache, - application_metadata, - finalizations_by_height, - finalized_blocks, - }, - Mailbox::new(sender), - ) - } + let finalized_height = context.gauge("finalized_height", "Finalized height of application"); + let processed_height = context.gauge("processed_height", "Processed height of application"); + + // Initialize mailbox + let (sender, mailbox) = mailbox::new(context.child("mailbox"), config.mailbox_size); + ( + Self { + context: ContextCell::new(context), + mailbox, + provider: config.scheme_provider, + epocher: config.epocher, + view_retention_timeout: config.view_retention_timeout, + max_repair: config.max_repair, + block_codec_config: config.block_codec_config, + strategy: config.strategy, + last_proposed_block: None, + floor: Floor::resolved(None, Round::zero()), + stream, + 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, + finalized_height, + processed_height, + }, + Mailbox::new(sender), + ) } /// Start the actor. @@ -402,21 +271,21 @@ where mut self, application: impl Reporter>, buffer: buffered::Mailbox, - resolver: (mpsc::Receiver>, R), + resolver: (handler::Receiver, R), sync_start: SyncStart, checkpoint: Option>, ) -> Handle<()> where - R: Resolver< - Key = handler::Request, - PublicKey = ::PublicKey, + R: TargetedResolver< + Key = Key, + Subscriber = Annotation, + PublicKey = ::PublicKey, >, - K: PublicKey + From<::PublicKey>, + K: PublicKey + From<::PublicKey>, { spawn_cell!( self.context, - self.run(application, buffer, resolver, sync_start, checkpoint,) - .await + self.run(application, buffer, resolver, sync_start, checkpoint) ) } @@ -425,23 +294,26 @@ where mut self, mut application: impl Reporter>, mut buffer: buffered::Mailbox, - (mut resolver_rx, mut resolver): (mpsc::Receiver>, R), + (mut resolver_rx, mut resolver): (handler::Receiver, R), sync_start: SyncStart, checkpoint: Option>, ) where - R: Resolver< - Key = handler::Request, - PublicKey = ::PublicKey, + R: TargetedResolver< + Key = Key, + Subscriber = Annotation, + PublicKey = ::PublicKey, >, - K: PublicKey + From<::PublicKey>, + K: PublicKey + From<::PublicKey>, { let SyncStart { height: sync_height, epoch: sync_epoch, view: sync_view, } = sync_start; - self.last_processed_height = Height::new(sync_height); - self.last_processed_round = Round::new(Epoch::new(sync_epoch), View::new(sync_view)); + 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"); @@ -468,30 +340,27 @@ where checkpoint.last_block, finalization, &mut application, - &mut buffer, ) .await; self.sync_finalized().await; } - #[cfg(feature = "prom")] - { - self.processed_height - .set(self.last_processed_height.get() as i64); - } + let _ = self + .processed_height + .try_set(self.floor.processed_height().get()); // Create a local pool for waiter futures. - let mut waiters = AbortablePool::<(B::Digest, B)>::default(); + let mut waiters = BlockWaiters::::default(); + + // Observe non-blocking storage syncs without stalling mailbox processing. + let mut syncs = Pool::>::default(); // Get tip and send to application let tip = self.get_latest().await; if let Some((height, commitment)) = tip { - application - .report(Update::Tip(height.get(), commitment)) - .await; + application.report(Update::Tip(height.get(), commitment)); self.tip = height; - #[cfg(feature = "prom")] - self.finalized_height.set(height.get() as i64); + let _ = self.finalized_height.try_set(height.get()); } // Load persisted cache epochs so find_block can discover blocks @@ -522,407 +391,793 @@ where on_stopped => { debug!("context shutdown, stopping syncer"); }, + sync = syncs.next_completed() => { + match sync { + PooledSync::Observed => {} + PooledSync::Finalized(seq) => { + self.dispatch_gate.release(seq); + self.try_dispatch_blocks(&mut application, &mut resolver).await; + } + PooledSync::Notarized(block) => { + self.pending_notarized_reports.remove(&block.digest()); + application.report(Update::NotarizedBlock(block)); + self.try_dispatch_blocks(&mut application, &mut resolver).await; + } + } + }, // Handle waiter completions first result = waiters.next_completed() => { - let Ok((commitment, block)) = result else { + let Ok(completion) = result else { continue; // Aborted future }; - self.notify_subscribers(commitment, &block).await; + match completion { + Ok((commitment, block)) => { + self.notify_subscribers(commitment, &block); + self.apply_floor_anchor(&block, &mut buffer, &mut application, &mut resolver) + .await; + } + Err(commitment) => { + debug!( + ?commitment, + "buffer subscription closed, canceling local subscribers" + ); + self.block_subscriptions.remove(&commitment); + } + } }, // Handle application acknowledgements (drain all ready acks, sync once) result = self.pending_acks.current() => { - // Start with the ack that woke this arm. - let mut pending = Some(self.pending_acks.complete_current(result)); - loop { - let (height, commitment, result) = - pending.take().expect("pending ack must exist"); - match result { - Ok(()) => { - // Apply in-memory progress updates for this acknowledged block. - self.handle_block_processed(height, commitment, &mut resolver) - .await; - } - Err(e) => { - error!(?e, %height, "application did not acknowledge block"); - return; - } - } - - // Opportunistically drain any additional already-ready acks so we - // can persist one metadata sync for the whole batch. - 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.application_metadata.sync().await { - error!(?e, "failed to sync application progress"); + if !self.handle_ack(result, &mut application, &mut resolver).await { return; } - - // Fill the pipeline - self.try_dispatch_blocks(&mut application, &mut resolver).await; }, // Handle consensus inputs before backfill or resolver traffic Some(message) = self.mailbox.recv() else { info!("mailbox closed, shutting down"); break; } => { - match message { - Message::GetInfo { identifier, response } => { - let info = match identifier { - BlockID::Digest(commitment) => self - .finalized_blocks - .get(ArchiveID::Key(&commitment)) - .await - .ok() - .flatten() - .map(|b| (b.height(), commitment)), - BlockID::Height(height) => self - .finalizations_by_height - .get(ArchiveID::Index(height.get())) - .await - .ok() - .flatten() - .map(|f| (height, f.proposal.payload)), - BlockID::Latest => self.get_latest().await, - }; - response.send_lossy(info); - } - Message::Proposed { round, block } => { - self.cache_verified(round, block.digest(), block.clone()).await; - buffer.send(round, block, Recipients::All).await; - } - Message::Forward { - round, - commitment, - peers, - } => { - if peers.is_empty() { - continue; - } - let Some(block) = self.find_block(&mut buffer, commitment).await else { + self.handle_mailbox_message( + message, + &mut resolver, + &mut waiters, + &mut syncs, + &mut buffer, + &mut application, + ) + .await; + }, + // Handle resolver messages last (batched up to max_repair, sync once) + Some(message) = resolver_rx.recv() else { + info!("handler closed, shutting down"); + return; + } => { + self.handle_resolver_message( + message, + &mut resolver_rx, + &mut resolver, + &mut syncs, + &mut buffer, + &mut application, + ) + .await; + }, + } + } + + /// Handles one ready application acknowledgement and drains any queued acks + /// that are already complete. + /// + /// Returns `false` if the actor should shut down. + async fn handle_ack( + &mut self, + result: ::Output, + application: &mut impl Reporter>, + resolver: &mut R, + ) -> bool + where + R: Resolver, Subscriber = Annotation>, + { + // Start with the ack that woke this `select_loop!` arm. + let mut pending = Some(self.pending_acks.complete_current(result)); + loop { + let (height, commitment, result) = pending.take().expect("pending ack must exist"); + let _ = commitment; + match result { + Ok(()) => { + // Apply in-memory progress updates for this acknowledged block. + self.update_processed_height(height, resolver); + self.update_processed_round(height, resolver).await; + } + Err(e) => { + error!(?e, %height, "application did not acknowledge block"); + return false; + } + } + + // Opportunistically drain any additional already-ready acks so we + // can persist one metadata sync for the whole batch. + 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 + } + + /// Handles a single mailbox message from local consensus/application callers. + async fn handle_mailbox_message( + &mut self, + message: Message, + resolver: &mut R, + waiters: &mut BlockWaiters, + syncs: &mut Pool>, + buffer: &mut buffered::Mailbox, + application: &mut impl Reporter>, + ) where + R: TargetedResolver< + Key = Key, + Subscriber = Annotation, + PublicKey = ::PublicKey, + >, + K: PublicKey + From<::PublicKey>, + { + if message.response_closed() { + return; + } + + match message { + Message::GetInfo { + identifier, + response, + } => { + let info = match identifier { + BlockID::Digest(commitment) => self + .finalized_blocks + .get(ArchiveID::Key(&commitment)) + .await + .ok() + .flatten() + .map(|b| (b.height(), commitment)), + BlockID::Height(height) => self.get_info_by_height(height).await, + BlockID::Latest => self.get_latest().await, + }; + response.send_lossy(info); + } + Message::GetVerified { round, response } => { + let block = self.cache.get_verified(round).await; + response.send_lossy(block); + } + Message::Proposed { round, block, ack } => { + // Match marshal's latency-sensitive ordering: hand the proposal + // to the network before any storage work can delay propagation. + buffer.send(round, Arc::new(block.clone()), Recipients::All); + + // If the round has already been pruned by tip advancement, + // `cache_verified` is a no-op because the round is below + // the retention floor (and no longer is required by consensus + // to make progress). + let handle = self + .cache_verified(round, block.digest(), block.clone()) + .await; + self.apply_floor_anchor(&block, buffer, application, resolver) + .await; + + // Retain the block in memory so a subsequent `Forward` can + // re-send it without reloading from storage. An older retained + // proposal (if any) is overwritten. + let commitment = block.digest(); + self.last_proposed_block = Some((round, commitment, block)); + ack.send_lossy(handle); + } + Message::Forward { + round, + commitment, + recipients, + } => { + if matches!(&recipients, Recipients::Some(peers) if peers.is_empty()) { + return; + } + let block = match self.take_proposed(round, commitment) { + Some(block) => block, + None => { + let Some(block) = self.find_block(buffer, commitment).await else { debug!(?commitment, "block not found for forwarding"); - continue; + return; }; - let peers: Vec = peers.into_iter().map(K::from).collect(); - buffer.send(round, block, Recipients::Some(peers)).await; + block } - Message::Verified { round, block } => { - self.cache_verified(round, block.digest(), block).await; + }; + let recipients = match recipients { + Recipients::All => Recipients::All, + Recipients::Some(peers) => { + Recipients::Some(peers.into_iter().map(K::from).collect()) } - Message::Notarization { notarization } => { - let round = notarization.round(); - let commitment = notarization.proposal.payload; - - // Store notarization by view - self.cache.put_notarization(round, commitment, notarization.clone()).await; - - // Search for block locally, otherwise fetch it remotely - if let Some(block) = self.find_block(&mut buffer, commitment).await { - // If found, persist the block and send to application - self.cache_block(round, commitment, block.clone()).await; - application.report(Update::NotarizedBlock(block.clone())).await; - self.notify_subscribers(commitment, &block).await; - } else { - debug!(?round, "notarized block missing"); - resolver.fetch(Request::::Notarized { round }).await; - } + Recipients::One(peer) => Recipients::One(K::from(peer)), + }; + buffer.send(round, Arc::new(block), recipients); + } + Message::Verified { round, block, ack } => { + // If the round has already been pruned by tip advancement, + // `cache_verified` is a no-op because the round is below + // the retention floor (and no longer is required by consensus + // to make progress). + let handle = self + .cache_verified(round, block.digest(), block.clone()) + .await; + self.apply_floor_anchor(&block, buffer, application, resolver) + .await; + ack.send_lossy(handle); + } + Message::Certified { round, block, ack } => { + // If the round has already been pruned by tip advancement, + // `cache_block` is a no-op because the round is below + // the retention floor (and no longer is required by consensus + // to make progress). + let commitment = block.digest(); + let block_sync = if self.cache.has_verified(round, &commitment).await { + self.notify_subscribers(commitment, &block); + self.cache.start_sync_verified(round).await + } else { + self.cache_block(round, commitment, block.clone()).await + }; + self.apply_floor_anchor(&block, buffer, application, resolver) + .await; + let certificate_sync = self.cache.start_sync_notarizations(round).await; + let handle = Handle::from_future(async move { + let (certificate, block) = join(certificate_sync, block_sync).await; + certificate.and(block) + }); + ack.send_lossy(handle); + } + Message::Notarization { notarization } => { + let round = notarization.round(); + let commitment = notarization.proposal.payload; + + // Store notarization by view + let notarization_sync = self + .cache + .put_notarization(round, commitment, notarization.clone()) + .await; + + // A notarization alone is not enough to fetch missing proposal + // data. If the block is not locally available, remember the + // certificate and wait for a later finalization/repair path + // (or a round-bound subscription) to fetch it. + if let Some(block) = self.find_block(buffer, commitment).await { + let block_sync = if self.cache.has_verified(round, &commitment).await { + self.notify_subscribers(commitment, &block); + debug!(?round, "notarized block covered by verified write"); + self.cache.start_sync_verified(round).await + } else { + self.cache_block(round, commitment, block.clone()).await + }; + let installed_floor = self + .apply_floor_anchor(&block, buffer, application, resolver) + .await; + if !installed_floor { + self.pending_notarized_reports.insert(commitment); } - Message::Finalization { finalization } => { - // Cache finalization by round - let round = finalization.round(); - let commitment = finalization.proposal.payload; - self.cache.put_finalization(round, commitment, finalization.clone()).await; - - // Search for block locally, otherwise fetch it remotely - if let Some(block) = self.find_block(&mut buffer, commitment).await { - // If found, persist the block - let height = block.height(); - let mut needs_sync = self.store_finalization( - height, - commitment, - block, - Some(finalization), - &mut application, - &mut buffer, - ) - .await; - if needs_sync { - needs_sync |= self.try_repair_gaps(&mut buffer, &mut resolver, &mut application) - .await; - } - if needs_sync { - self.sync_finalized().await; - debug!(?round, %height, "finalized block stored"); - } - self.try_dispatch_blocks(&mut application, &mut resolver).await; + syncs.push(async move { + let (certificate, block_durable) = join( + notarization_sync.durable(round, "notarization"), + block_sync.durable(round, "notarized"), + ) + .await; + if certificate && block_durable && !installed_floor { + PooledSync::Notarized(block) } else { - // Otherwise, fetch the block from the network. - debug!(?round, ?commitment, "finalized block missing"); - resolver.fetch(Request::::Block(commitment)).await; - } - } - Message::GetBlock { identifier, response } => { - match identifier { - BlockID::Digest(commitment) => { - let result = self.find_block(&mut buffer, commitment).await; - response.send_lossy(result); - } - BlockID::Height(height) => { - let result = self.get_finalized_block(height).await; - response.send_lossy(result); - } - BlockID::Latest => { - let block = match self.get_latest().await { - Some((_, commitment)) => self.find_block(&mut buffer, commitment).await, - None => None, - }; - response.send_lossy(block); - } + PooledSync::Observed } + }); + } else { + debug!(?round, "notarized block unavailable locally"); + syncs.push(async move { + notarization_sync.durable(round, "notarization").await; + PooledSync::Observed + }); + } + } + Message::Finalization { finalization } => { + let round = finalization.round(); + let commitment = finalization.proposal.payload; + + // Cache finalization by round + self.cache + .put_finalization(round, commitment, finalization.clone()) + .await; + + // Search for the finalized block locally, otherwise fetch it remotely. + if let Some(block) = self.find_block(buffer, commitment).await { + // The anchor path stores the floor block and finalization, + // advances floors, prunes below them, and resumes dispatch. + if self + .apply_floor_anchor(&block, buffer, application, resolver) + .await + { + return; } - Message::GetFinalization { height, response } => { - let finalization = self.get_finalization_by_height(height).await; - response.send_lossy(finalization); + + let height = block.height(); + self.update_processed_round_floor(height, round, resolver) + .await; + if self + .store_finalization( + height, + commitment, + block, + Some(finalization), + application, + ) + .await + { + // If a floor anchor is pending, repair and dispatch are + // no-ops until the anchor block is stored. + self.try_repair_gaps(buffer, resolver, application).await; + self.start_finalized_sync(round, syncs).await; + debug!(?round, %height, "finalized block stored"); } - Message::HintFinalized { height, targets } => { - // Skip if height is at or below the floor - if height <= self.last_processed_height { - continue; - } + } else { + // The finalization carries a round and commitment, but not a + // height. Keep the request round-bound until the block is decoded. + debug!(?round, ?commitment, "finalized block missing"); + self.floor + .fetch_if_permitted( + resolver, + Request::finalized_block_by_round(commitment, round), + ) + .ignore(); + } + } + Message::GetBlock { + identifier, + response, + } => match identifier { + BlockID::Digest(commitment) => { + let result = self.find_block(buffer, commitment).await; + response.send_lossy(result); + } + BlockID::Height(height) => { + let result = self.get_finalized_block(height).await; + response.send_lossy(result); + } + BlockID::Latest => { + let block = match self.get_latest().await { + Some((_, commitment)) => self.find_block(buffer, commitment).await, + None => None, + }; + response.send_lossy(block); + } + }, + Message::GetFinalization { height, response } => { + let finalization = self.get_finalization_by_height(height).await; + response.send_lossy(finalization); + } + Message::GetProcessedHeight { response } => { + response.send_lossy(self.stream.processed_height()); + } + Message::HintFinalized { height, targets } => { + // Skip if finalization is already available locally. + if self.has_finalization_by_height(height).await { + return; + } - // Skip if finalization is already available locally - if self.get_finalization_by_height(height).await.is_some() { - continue; - } + // Trigger a targeted fetch via the resolver (denied below the floor). + self.floor + .fetch_targeted_if_permitted(resolver, Request::finalized(height), targets) + .ignore(); + } + Message::HintNotarized { round, commitment } => { + if self.find_block(buffer, commitment).await.is_none() { + self.floor + .fetch_if_permitted(resolver, Request::notarized(round)) + .ignore(); + } + } + Message::Subscribe { + round, + commitment, + response, + } => { + // Check for block locally + if let Some(block) = self.find_block(buffer, commitment).await { + response.send_lossy(block); + return; + } - // Trigger a targeted fetch via the resolver - let request = Request::::Finalized { height: height.get() }; - resolver.fetch_targeted(request, targets).await; + // We don't have the block locally, so fetch the block from the network + // if we have an associated round. If we only have the digest, don't make + // 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 + .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; } - Message::Subscribe { round, commitment, response } => { - // Check for block locally - if let Some(block) = self.find_block(&mut buffer, commitment).await { - response.send_lossy(block); - continue; - } - - // We don't have the block locally, so fetch the block from the network - // if we have an associated view. If we only have the digest, don't make - // 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 round < self.last_processed_round { - warn!( - ?round, - ?commitment, - last_processed_round = ?self.last_processed_round, - last_processed_height = %self.last_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); - - continue; - } - // Attempt to fetch the block (with notarization) from the resolver. - // If this is a valid view, this request should be fine to keep open - // until resolution or pruning (even if the oneshot is canceled). - debug!(?round, ?commitment, "requested block missing"); - resolver.fetch(Request::::Notarized { round }).await; - } + // 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). + debug!(?round, ?commitment, "requested block missing"); + } - // Register subscriber - debug!(?round, ?commitment, "registering subscriber"); - match self.block_subscriptions.entry(commitment) { - Entry::Occupied(mut entry) => { - entry.get_mut().subscribers.push(response); - } - Entry::Vacant(entry) => { - let (tx, rx) = oneshot::channel(); - buffer.subscribe_prepared(commitment, tx).await; - let aborter = waiters.push(async move { - (commitment, rx.await.expect("buffer subscriber closed")) - }); - entry.insert(BlockSubscription { - subscribers: vec![response], - _aborter: aborter, - }); - } - } + // Register subscriber + debug!(?round, ?commitment, "registering subscriber"); + match self.block_subscriptions.entry(commitment) { + Entry::Occupied(mut entry) => { + entry.get_mut().subscribers.push(response); } - Message::SetFloor { height } => { - if self.last_processed_height >= height { - warn!( - %height, - existing = %self.last_processed_height, - "floor not updated, lower than existing" - ); - continue; - } + Entry::Vacant(entry) => { + let rx = buffer.subscribe(commitment); + let aborter = waiters.push(async move { + rx.await + .map(|block| (commitment, (*block).clone())) + .map_err(|_| commitment) + }); + entry.insert(BlockSubscription { + subscribers: vec![response], + _aborter: aborter, + }); + } + } + } + Message::SetFloor { finalization } => { + self.install_floor(finalization, true, resolver, buffer, application) + .await; + } + Message::Prune { height } => { + // Only allow pruning at or below the current floor + if height > self.floor.processed_height() { + warn!(%height, floor = %self.floor.processed_height(), "prune height above floor, ignoring"); + return; + } - // Update the processed height - self.update_processed_height(height, &mut resolver).await; - if let Err(err) = self.application_metadata.sync().await { - error!(?err, %height, "failed to update floor"); - return; - } + // Prune the finalized block and finalization certificate archives in parallel. + self.prune_finalized_archives(height) + .await + .expect("failed to prune finalized archives"); - // Drop all pending acknowledgements. We must do this to prevent - // an in-process block from being processed that is below the new floor - // updating `last_processed_height`. - self.pending_acks.clear(); + // Intentionally keep existing block subscriptions alive. Canceling + // waiters can have catastrophic consequences because actors do not + // retry subscriptions on failed channels. + } + } + } - // Prune data in the finalized archives below the new floor. - if let Err(err) = self.prune_finalized_archives(height).await { - error!(?err, %height, "failed to prune finalized archives"); - return; - } - } - Message::Prune { height } => { - // Only allow pruning at or below the current floor - if height > self.last_processed_height { - warn!(%height, floor = %self.last_processed_height, "prune height above floor, ignoring"); - continue; - } + /// Handles a batch of resolver messages and starts one finalized-archive + /// sync covering writes accepted by the batch. + async fn handle_resolver_message( + &mut self, + message: handler::Message, + resolver_rx: &mut handler::Receiver, + resolver: &mut R, + syncs: &mut Pool>, + buffer: &mut buffered::Mailbox, + application: &mut impl Reporter>, + ) where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + let mut handled = false; + let mut produces = Vec::new(); + let mut delivers = Vec::new(); + + // Drain up to max_repair resolver messages. Block deliveries are handled + // immediately, certificate-bearing deliveries are batched for verification, + // and produce responses wait until repair has had a chance to fill gaps. + for msg in std::iter::once(message) + .chain(std::iter::from_fn(|| resolver_rx.try_recv().ok())) + .take(self.max_repair.get()) + { + if msg.response_closed() { + continue; + } + handled = true; - // Prune the finalized block and finalization certificate archives in parallel. - if let Err(err) = self.prune_finalized_archives(height).await { - error!(?err, %height, "failed to prune finalized archives"); - return; - } - } + match msg { + handler::Message::Produce { key, response } => { + produces.push((key, response)); } - }, - // Handle resolver messages last (batched up to max_repair, sync once) - Some(message) = resolver_rx.recv() else { - info!("handler closed, shutting down"); - return; - } => { - // Drain up to max_repair messages: blocks handled immediately, - // certificates batched for verification, produces deferred. - let mut needs_sync = false; - let mut produces = Vec::new(); - let mut delivers = Vec::new(); - for msg in std::iter::once(message) - .chain(std::iter::from_fn(|| resolver_rx.try_recv().ok())) - .take(self.max_repair.get()) - { - match msg { - handler::Message::Produce { key, response } => { - produces.push((key, response)); - } - handler::Message::Deliver { - key, + handler::Message::Deliver { + delivery, + value, + response, + } => { + self.handle_deliver( + ResolverDelivery { + delivery, value, response, - } => { - needs_sync |= self - .handle_deliver( - key, - value, - response, - &mut delivers, - &mut application, - &mut buffer, - ) - .await; - } - } + }, + &mut delivers, + buffer, + application, + resolver, + ) + .await; } + } + } + if !handled { + return; + } - // Batch verify and process all delivers. - needs_sync |= self - .verify_delivered(delivers, &mut application, &mut buffer) - .await; + // Batch verify and process all certificate-bearing deliveries. + self.verify_delivered(delivers, buffer, application, resolver) + .await; - // Attempt to fill gaps before handling produce requests (so we - // can serve data we just received). - needs_sync |= self - .try_repair_gaps(&mut buffer, &mut resolver, &mut application) - .await; + // Attempt to fill gaps before handling produce requests so we can serve + // data received earlier in the same batch. + self.try_repair_gaps(buffer, resolver, application).await; + self.start_finalized_sync(self.floor.processed_round(), syncs) + .await; + + // Handle produce requests in parallel. + join_all( + produces + .into_iter() + .filter(|(_, response)| !response.is_closed()) + .map(|(key, response)| self.handle_produce(key, response, buffer)), + ) + .await; + } + + /// Handle a produce request from a remote peer. + async fn handle_produce( + &self, + key: Key, + response: oneshot::Sender, + buffer: &buffered::Mailbox, + ) { + match key { + Key::Block(commitment) => { + let Some(block) = self.find_block(buffer, commitment).await else { + debug!(?commitment, "block missing on request"); + return; + }; + response.send_lossy(block.encode()); + } + Key::Finalized { height } => { + let height = Height::new(height); + let Some(finalization) = self.get_finalization_by_height(height).await else { + debug!(%height, "finalization missing on request"); + return; + }; + let Some(block) = self.get_finalized_block(height).await else { + debug!(%height, "finalized block missing on request"); + return; + }; + response.send_lossy((finalization, block).encode()); + } + Key::Notarized { round } => { + let Some(notarization) = self.cache.get_notarization(round).await else { + debug!(?round, "notarization missing on request"); + return; + }; + let commitment = notarization.proposal.payload; + let Some(block) = self.find_block(buffer, commitment).await else { + debug!(?commitment, "block missing on request"); + return; + }; + response.send_lossy((notarization, block).encode()); + } + } + } + + /// Verifies and installs a floor, fetching the anchor block if needed. + async fn install_floor( + &mut self, + finalization: Finalization, + skip_if_superseded: bool, + resolver: &mut R, + buffer: &mut buffered::Mailbox, + application: &mut impl Reporter>, + ) where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + let round = finalization.round(); + if round <= self.floor.processed_round() { + warn!( + ?round, + floor = ?self.floor.processed_round(), + "floor not updated, below existing round floor" + ); + return; + } + + let Some(scheme) = self.get_scheme_certificate_verifier(finalization.epoch()) else { + panic!("floor finalization epoch unavailable"); + }; + assert!( + finalization.verify(self.context.as_mut(), &scheme, &self.strategy), + "floor finalization must verify" + ); + + let commitment = finalization.proposal.payload; + self.cache + .put_finalization(round, commitment, finalization.clone()) + .await; + + // A pending anchor at the same or a newer floor already blocks + // progress. Keep waiting for it instead of replacing it. + if skip_if_superseded && self.floor.has_pending_anchor_at_or_after(round) { + return; + } + + if let Some(block) = self.find_block(buffer, commitment).await { + self.floor.await_anchor(finalization); + assert!( + self.apply_floor_anchor(&block, buffer, application, resolver) + .await + ); + return; + } + + // The pending floor owns the next application sync point. Drop any + // in-flight acks before they can advance the processed height past it. + self.pending_acks.clear(); + + debug!(?round, ?commitment, "starting fetch for floor block"); + self.floor.await_anchor(finalization); + self.floor + .fetch_if_permitted( + resolver, + Request::finalized_block_by_round(commitment, round), + ) + .ignore(); + } + + /// Applies a block if it satisfies the current floor transition. + async fn apply_floor_anchor( + &mut self, + block: &B, + buffer: &mut buffered::Mailbox, + application: &mut impl Reporter>, + resolver: &mut R, + ) -> bool + where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + let commitment = block.digest(); + if !self.floor.matches_pending_anchor(commitment) { + return false; + } + let block = block.clone(); + + // This anchor cannot move the application sync point, but its + // finalization round can still prune round-bound resolver work. + // Keep pending acks intact because processed_height is unchanged. + let height = block.height(); + if height <= self.floor.processed_height() { + warn!( + %height, + existing = %self.floor.processed_height(), + "floor not updated, at or below existing" + ); + let finalization = self + .floor + .take_pending_anchor() + .expect("pending floor anchor missing"); + self.update_processed_round_floor(height, finalization.round(), resolver) + .await; + if self.try_repair_gaps(buffer, resolver, application).await { + self.sync_finalized().await; + } + self.try_dispatch_blocks(application, resolver).await; + return true; + } + + let finalization = self + .floor + .take_pending_anchor() + .expect("pending floor anchor missing"); + let round = finalization.round(); + try_join!( + async { + self.finalized_blocks + .put(block.clone()) + .await + .map_err(Box::new)?; + Ok::<_, BoxedError>(()) + }, + async { + self.finalizations_by_height + .put(height, commitment, finalization) + .await + .map_err(Box::new)?; + Ok::<_, BoxedError>(()) + } + ) + .expect("failed to store floor anchor"); + self.sync_finalized().await; + self.notify_subscribers(commitment, &block); + + if height > self.tip { + application.report(Update::Tip(height.get(), commitment)); + self.tip = height; + let _ = self.finalized_height.try_set(height.get()); + } - // Sync archives before responding to peers (prioritize our own durability). - if needs_sync { - self.sync_finalized().await; - } + // The anchor is durable, but the application still needs to process it. + // Record the previous height so dispatch resumes at the anchor itself. + let dispatch_floor = height + .previous() + .expect("floor anchor above processed height must have predecessor"); + 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"); - // Dispatch blocks to the application. - self.try_dispatch_blocks(&mut application, &mut resolver).await; + // 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(); - // Handle produce requests in parallel. - join_all( - produces - .into_iter() - .map(|(key, response)| self.handle_produce(key, response, &buffer)), - ) - .await; - }, - } - } + // The floor is durable, so cache/finalized data below it can be pruned. + self.prune_after_floor(height) + .await + .expect("failed to prune data below floor"); - /// Handle a produce request from a remote peer. - async fn handle_produce( - &self, - key: Request, - response: oneshot::Sender, - buffer: &buffered::Mailbox, - ) { - match key { - Request::Block(commitment) => { - let Some(block) = self.find_block_const(buffer, commitment).await else { - debug!(?commitment, "block missing on request"); - return; - }; - response.send_lossy(block.encode()); - } - Request::Finalized { height } => { - let height = Height::new(height); - let Some(finalization) = self.get_finalization_by_height(height).await else { - debug!(%height, "finalization missing on request"); - return; - }; - let Some(block) = self.get_finalized_block(height).await else { - debug!(%height, "finalized block missing on request"); - return; - }; - response.send_lossy((finalization, block).encode()); - } - Request::Notarized { round } => { - let Some(notarization) = self.cache.get_notarization(round).await else { - debug!(?round, "notarization missing on request"); - return; - }; - let commitment = notarization.proposal.payload; - let Some(block) = self.find_block_const(buffer, commitment).await else { - debug!(?commitment, "block missing on request"); - return; - }; - response.send_lossy((notarization, block).encode()); - } + // Intentionally keep existing block subscriptions alive. Canceling + // waiters can have catastrophic consequences (nodes can get stuck in + // different views) as actors do not retry subscriptions on failed channels. + if self.try_repair_gaps(buffer, resolver, application).await { + self.sync_finalized().await; } + self.try_dispatch_blocks(application, resolver).await; + true } /// Handle a deliver message from the resolver. Block delivers are handled /// immediately. Finalized/Notarized delivers are parsed and structurally /// validated, then collected into `delivers` for batch certificate verification. /// Returns true if finalization archives were written and need syncing. - async fn handle_deliver( + async fn handle_deliver( &mut self, - key: Request, - value: Bytes, - response: oneshot::Sender, + message: ResolverDelivery, delivers: &mut Vec>, - application: &mut impl Reporter>, buffer: &mut buffered::Mailbox, - ) -> bool { + application: &mut impl Reporter>, + resolver: &mut R, + ) -> bool + where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + let ResolverDelivery { + delivery, + value, + response, + } = message; + let Delivery { key, subscribers } = delivery; match key { - Request::Block(commitment) => { + Key::Block(commitment) => { let Ok(block) = B::decode_cfg(value.as_ref(), &self.block_codec_config) else { response.send_lossy(false); return false; @@ -932,29 +1187,70 @@ where return false; } - // Persist the block, also storing the finalization if we have it. + // This block may match the pending floor request. Whether it + // installs or is rejected as the floor anchor, do not also + // process it as an ordinary block delivery. + if self + .apply_floor_anchor(&block, buffer, application, resolver) + .await + { + response.send_lossy(true); + return false; + } + + // The commitment validates the peer response. Annotations are + // local context attached to the request and do not affect peer + // validity. + self.notify_subscribers(commitment, &block); + + // The peer-visible request only says "give me this block". + // Local annotations explain why the block was requested and + // therefore where, if anywhere, it should be stored. let height = block.height(); + let annotations: Vec<_> = subscribers + .into_vec() + .into_iter() + .map(|(annotation, _span)| annotation) + .collect(); + + // Round-bound proposal-parent fetches are `Key::Notarized` + // deliveries and are handled below. In this block-keyed path, + // `Finalized` means the block belongs in the finalized chain. let finalization = self.cache.get_finalization_for(commitment).await; - let wrote = self - .store_finalization( - height, - commitment, - block, - finalization, - application, - buffer, - ) - .await; + if let Some(finalization) = &finalization { + self.update_processed_round_floor(height, finalization.round(), resolver) + .await; + } + let wrote = if finalization.is_some() + || annotations + .iter() + .any(|annotation| matches!(annotation, Annotation::Finalized(_))) + { + self.store_finalization(height, commitment, block, finalization, application) + .await + } else { + if annotations + .iter() + .any(|annotation| matches!(annotation, Annotation::Certified { .. })) + && height > self.floor.processed_height() + && let Some(bounds) = self.epocher.containing(height) + { + self.cache + .put_certified(bounds.epoch(), height, commitment, block) + .await; + } + false + }; debug!(?commitment, %height, "received block"); response.send_lossy(true); wrote } - Request::Finalized { height } => { + Key::Finalized { height } => { let height = Height::new(height); let Some(bounds) = self.epocher.containing(height) else { debug!( %height, - floor = %self.last_processed_height, + floor = %self.floor.processed_height(), "ignoring stale delivery" ); response.send_lossy(true); @@ -964,7 +1260,7 @@ where let Some(scheme) = self.get_scheme_certificate_verifier(epoch) else { debug!( %height, - floor = %self.last_processed_height, + floor = %self.floor.processed_height(), "ignoring stale delivery" ); response.send_lossy(true); @@ -1015,11 +1311,11 @@ where }); false } - Request::Notarized { round } => { + Key::Notarized { round } => { let Some(scheme) = self.get_scheme_certificate_verifier(round.epoch()) else { debug!( ?round, - floor = %self.last_processed_height, + floor = %self.floor.processed_height(), "ignoring stale delivery" ); response.send_lossy(true); @@ -1069,16 +1365,20 @@ where } } - /// Batch verify pending certificates and process valid items. Returns true - /// if finalization archives were written and need syncing. - async fn verify_delivered( + /// Batch verify pending certificates and process valid items. + async fn verify_delivered( &mut self, mut delivers: Vec>, - application: &mut impl Reporter>, buffer: &mut buffered::Mailbox, - ) -> bool { + application: &mut impl Reporter>, + resolver: &mut R, + ) where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + delivers.retain(|item| !item.response_closed()); if delivers.is_empty() { - return false; + return; } // Extract (subject, certificate) pairs for batch verification. @@ -1100,11 +1400,8 @@ where }) .collect(); - // Batch verify using the all-epoch verifier if available, otherwise - // batch verify per epoch using scoped verifiers. - let verified = if let Some(scheme) = self.provider.all() { - verify_certificates(&mut self.context, scheme.as_ref(), &certs, &self.strategy) - } else { + // Batch verify per epoch using scoped verifiers. + let verified = { let mut verified = vec![false; delivers.len()]; // Group indices by epoch. @@ -1124,7 +1421,7 @@ where }; let group: Vec<_> = indices.iter().map(|&i| certs[i]).collect(); let results = - verify_certificates(&mut self.context, scheme.as_ref(), &group, &self.strategy); + verify_certificates(self.context.as_mut(), &scheme, &group, &self.strategy); for (j, &idx) in indices.iter().enumerate() { verified[idx] = results[j]; } @@ -1133,7 +1430,6 @@ where }; // Process each verified item, rejecting unverified ones. - let mut wrote = false; for (index, item) in delivers.drain(..).enumerate() { if !verified[index] { match item { @@ -1150,77 +1446,103 @@ where block, response, } => { + // Valid finalization received. response.send_lossy(true); let round = finalization.round(); let height = block.height(); let commitment = block.digest(); debug!(?round, %height, "received finalization"); - wrote |= self - .store_finalization( - height, - commitment, - block, - Some(finalization), - application, - buffer, - ) + // The floor-anchor path fully handles this finalization + // and moves the lower bound past it. + if self + .apply_floor_anchor(&block, buffer, application, resolver) + .await + { + continue; + } + + self.update_processed_round_floor(height, round, resolver) .await; + + self.store_finalization( + height, + commitment, + block, + Some(finalization), + application, + ) + .await; } PendingVerification::Notarized { notarization, block, response, } => { + // Valid notarization received. response.send_lossy(true); let round = notarization.round(); let commitment = block.digest(); debug!(?round, ?commitment, "received notarization"); - // If there exists a finalization certificate for this block, we - // should finalize it. + // Match marshal's imported-data ordering: make the block and + // certificate durable before repair bookkeeping can advance. let height = block.height(); + let block_sync = self.cache_block(round, commitment, block.clone()).await; + let notarization_sync = self + .cache + .put_notarization(round, commitment, notarization) + .await; + join( + block_sync.durable(round, "notarized"), + notarization_sync.durable(round, "notarization"), + ) + .await; + + // A notarized delivery can carry the pending floor block + // after the finalization is cached. + let installed_floor = self + .apply_floor_anchor(&block, buffer, application, resolver) + .await; + if installed_floor { + continue; + } + + // If there exists a finalization certificate for this block, we + // should finalize it. This could finalize the block faster when + // a notarization then a finalization are received via consensus + // and we resolve the notarization request before the block request. if let Some(finalization) = self.cache.get_finalization_for(commitment).await { - wrote |= self - .store_finalization( - height, - commitment, - block.clone(), - Some(finalization), - application, - buffer, - ) + self.update_processed_round_floor(height, finalization.round(), resolver) .await; - } - // Cache the notarization and block. - self.cache_block(round, commitment, block.clone()).await; - self.cache - .put_notarization(round, commitment, notarization) - .await; - application - .report(Update::NotarizedBlock(block.clone())) + self.store_finalization( + height, + commitment, + block.clone(), + Some(finalization), + application, + ) .await; - self.notify_subscribers(commitment, &block).await; + } + application.report(Update::NotarizedBlock(block)); } } } - - wrote } - /// Returns a scheme suitable for verifying certificates at the given epoch. - /// - /// Prefers a certificate verifier if available, otherwise falls back - /// to the scheme for the given epoch. - fn get_scheme_certificate_verifier(&self, epoch: Epoch) -> Option> { - self.provider.all().or_else(|| self.provider.scoped(epoch)) + /// Returns a scoped certificate verifier for the given epoch. + fn get_scheme_certificate_verifier( + &self, + epoch: Epoch, + ) -> Option> { + self.provider.scoped(epoch) } // -------------------- Waiters -------------------- /// Notify any subscribers for the given commitment with the provided block. - async fn notify_subscribers(&mut self, commitment: B::Digest, block: &B) { + fn notify_subscribers(&mut self, commitment: B::Digest, block: &B) { if let Some(mut bs) = self.block_subscriptions.remove(&commitment) { for subscriber in bs.subscribers.drain(..) { subscriber.send_lossy(block.clone()); @@ -1233,21 +1555,32 @@ where /// Attempt to dispatch finalized blocks to the application until the pipeline is full /// or no more blocks are available. /// - /// This does NOT advance `last_processed_height` or sync metadata. It only + /// This does NOT advance the processed floor height or sync metadata. It only /// sends blocks to the application and enqueues pending acks. Metadata is - /// updated later when acks arrive and [`Self::handle_block_processed`] runs. + /// updated later when acks arrive and [`Self::handle_ack`] runs. /// - /// Acks are processed in FIFO order so `last_processed_height` always + /// Acks are processed in FIFO order so the processed floor height always /// advances sequentially. - async fn try_dispatch_blocks( + async fn try_dispatch_blocks( &mut self, application: &mut impl Reporter>, - resolver: &mut impl Resolver>, - ) { + resolver: &mut R, + ) where + R: Resolver, Subscriber = Annotation>, + { + // Dispatch resumes after the floor anchor is durably stored. + if self.floor.blocks_progress() { + return; + } + + let barrier = self.dispatch_gate.barrier(); while self.pending_acks.has_capacity() { let next_height = self .pending_acks - .next_dispatch_height(self.last_processed_height); + .next_dispatch_height(self.stream.next_height()); + if barrier.is_some_and(|lowest| next_height >= lowest) { + return; + } let Some(block) = self.get_finalized_block(next_height).await else { return; }; @@ -1258,16 +1591,18 @@ where ); let (height, commitment) = (block.height(), block.digest()); + if self.pending_notarized_reports.contains(&commitment) { + return; + } let (ack, ack_waiter) = A::handle(); if is_last_block_of_epoch(&self.epocher, next_height.get()) { let Some(finalization) = self.get_finalization_by_height(next_height).await else { // The last block of an epoch will always have an explicit finalization certificate. // The finalizer requires it for storing the finalized header. - let request = Request::::Finalized { - height: height.get(), - }; - resolver.fetch(request).await; + self.floor + .fetch_if_permitted(resolver, Request::finalized(next_height)) + .ignore(); return; }; @@ -1288,13 +1623,9 @@ where return; } - application - .report(Update::FinalizedBlock((block, Some(finalization)), ack)) - .await; + application.report(Update::FinalizedBlock((block, Some(finalization)), ack)); } else { - application - .report(Update::FinalizedBlock((block, None), ack)) - .await; + application.report(Update::FinalizedBlock((block, None), ack)); } self.pending_acks.enqueue(PendingAck { @@ -1305,63 +1636,38 @@ where } } - /// Handle acknowledgement from the application that a block has been processed. - /// - /// Buffers the processed height update but does NOT sync to durable storage. - /// The caller must sync metadata after processing all ready acks. - async fn handle_block_processed( + // -------------------- Prunable Storage -------------------- + + /// Add a verified block to the prunable archive. + async fn cache_verified( &mut self, - height: Height, + round: Round, commitment: B::Digest, - resolver: &mut impl Resolver>, - ) { - // Update the processed height (buffered, not synced) - self.update_processed_height(height, resolver).await; - - // Cancel any useless requests - resolver - .cancel(Request::::Block(commitment)) - .await; - - if let Some(finalization) = self.get_finalization_by_height(height).await { - // Trail the previous processed finalized block by the timeout - let lpr = self.last_processed_round; - let prune_round = Round::new( - lpr.epoch(), - lpr.view().saturating_sub(self.view_retention_timeout), - ); - - // Prune archives - self.cache.prune(prune_round).await; - - // Update the last processed round - let round = finalization.round(); - self.last_processed_round = round; - - // Cancel useless requests - resolver - .retain(Request::::Notarized { round }.predicate()) - .await; - } + block: B, + ) -> Handle<()> { + self.notify_subscribers(commitment, &block); + self.cache.put_verified(round, commitment, block).await } - // -------------------- Prunable Storage -------------------- - - /// Add a verified block to the prunable archive. - async fn cache_verified(&mut self, round: Round, commitment: B::Digest, block: B) { - self.notify_subscribers(commitment, &block).await; - self.cache.put_verified(round, commitment, block).await; + /// If a block previously accepted via [`Message::Proposed`] matches the + /// supplied `(round, commitment)`, remove and return it. + fn take_proposed(&mut self, round: Round, commitment: B::Digest) -> Option { + let (cached_round, cached_commitment, _) = self.last_proposed_block.as_ref()?; + if *cached_round != round || *cached_commitment != commitment { + return None; + } + self.last_proposed_block.take().map(|(_, _, block)| block) } /// Add a notarized block to the prunable archive. - async fn cache_block(&mut self, round: Round, commitment: B::Digest, block: B) { - self.notify_subscribers(commitment, &block).await; - self.cache.put_block(round, commitment, block).await; + async fn cache_block(&mut self, round: Round, commitment: B::Digest, block: B) -> Handle<()> { + self.notify_subscribers(commitment, &block); + self.cache.put_block(round, commitment, block).await } // -------------------- Immutable Storage -------------------- - /// Sync both finalization archives to durable storage. + /// Sync both finalization archives to durable storage, blocking the actor. /// /// Must be called within the same `select_loop!` arm as any preceding /// [`Self::store_finalization`] / [`Self::try_repair_gaps`] writes, before yielding back @@ -1382,6 +1688,45 @@ where ) { panic!("failed to sync finalization archives: {e}"); } + self.dispatch_gate.clear(); + } + + /// Start a pooled sync covering all finalized writes buffered so far. + /// + /// 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>) { + let Some(seq) = self.dispatch_gate.adopt() else { + return; + }; + let (blocks, finalizations) = try_join!( + async { + let handle = self.finalized_blocks.start_sync().await.map_err(Box::new)?; + Ok::<_, BoxedError>(handle) + }, + async { + let handle = self + .finalizations_by_height + .start_sync() + .await + .map_err(Box::new)?; + Ok::<_, BoxedError>(handle) + }, + ) + .unwrap_or_else(|e| panic!("failed to start finalization archive sync: {e}")); + syncs.push(async move { + let (blocks, finalizations) = join( + blocks.durable(round, "finalized blocks"), + finalizations.durable(round, "finalizations"), + ) + .await; + if blocks && finalizations { + PooledSync::Finalized(seq) + } else { + PooledSync::Observed + } + }); } /// Get a finalized block from the immutable archive. @@ -1411,28 +1756,47 @@ 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 { + Ok(has) => has, + Err(e) => panic!("failed to check finalization: {e}"), + } + } + + /// Get finalized block information from either the finalization archive or + /// the finalized-block archive. + async fn get_info_by_height(&self, height: Height) -> Option<(Height, B::Digest)> { + if let Some(finalization) = self.get_finalization_by_height(height).await { + return Some((height, finalization.proposal.payload)); + } + + self.get_finalized_block(height) + .await + .map(|block| (block.height(), block.digest())) + } + /// Add a finalized block, and optionally a finalization, to the archive. /// - /// After persisting the block, attempt to dispatch the next contiguous block to the application. - /// - /// Writes are buffered and not synced. The caller must call - /// [`sync_finalized`](Self::sync_finalized) before yielding to the `select_loop!`. + /// Writes are buffered and not synced. Before yielding to the + /// `select_loop!`, the caller must invoke either + /// [`sync_finalized`](Self::sync_finalized) or + /// [`start_finalized_sync`](Self::start_finalized_sync). /// /// Returns `true` if data was written and the archives need syncing. - async fn store_finalization( + async fn store_finalization( &mut self, height: Height, commitment: B::Digest, block: B, finalization: Option>, application: &mut impl Reporter>, - _buffer: &mut buffered::Mailbox, ) -> bool { // Blocks below the last processed height are stale - if height <= self.last_processed_height { + if height <= self.floor.processed_height() { debug!( %height, - floor = %self.last_processed_height, + floor = %self.floor.processed_height(), ?commitment, "dropping finalization at or below processed height floor" ); @@ -1445,7 +1809,7 @@ where // mismatches early, but a `(block, finalization)` pair becomes trusted // storage here regardless of which path produced it — including paths // that bypass those checks: a finalization cached first then the block - // fetched later by digest (`Request::Block`), a consensus finalization + // fetched later by digest (`Key::Block`), a consensus finalization // matched against a locally-found block, a notarized block paired with // a cached finalization, checkpoint restart, and gap repair. Enforcing // the binding at this join point ensures every order gets the same @@ -1496,7 +1860,7 @@ where return false; } - self.notify_subscribers(commitment, &block).await; + self.notify_subscribers(commitment, &block); #[cfg(feature = "prom")] let store_start = Instant::now(); @@ -1522,6 +1886,8 @@ where panic!("failed to finalize: {e}"); } + self.dispatch_gate.defer(height); + #[cfg(feature = "prom")] { let store_duration = store_start.elapsed().as_micros() as f64; @@ -1540,14 +1906,9 @@ where "tip advanced by multiple blocks (catch-up)" ); } - application - .report(Update::Tip(height.get(), commitment)) - .await; + application.report(Update::Tip(height.get(), commitment)); self.tip = height; - #[cfg(feature = "prom")] - { - self.finalized_height.set(height.get() as i64); - } + let _ = self.finalized_height.try_set(height.get()); } true @@ -1567,34 +1928,13 @@ where /// Looks for a block anywhere in local storage. async fn find_block( - &mut self, - buffer: &mut buffered::Mailbox, - commitment: B::Digest, - ) -> Option { - // Check buffer. - if let Some(block) = buffer.get(commitment).await { - return Some(block); - } - // Check verified / notarized blocks via cache manager. - if let Some(block) = self.cache.find_block(commitment).await { - return Some(block); - } - // Check finalized blocks. - match self.finalized_blocks.get(ArchiveID::Key(&commitment)).await { - Ok(block) => block, // may be None - Err(e) => panic!("failed to get block: {e}"), - } - } - - /// Looks for a block anywhere in local storage (immutable borrow of buffer). - async fn find_block_const( &self, buffer: &buffered::Mailbox, commitment: B::Digest, ) -> Option { // Check buffer. if let Some(block) = buffer.get(commitment).await { - return Some(block); + return Some((*block).clone()); } // Check verified / notarized blocks via cache manager. if let Some(block) = self.cache.find_block(commitment).await { @@ -1602,7 +1942,7 @@ where } // Check finalized blocks. match self.finalized_blocks.get(ArchiveID::Key(&commitment)).await { - Ok(block) => block, + Ok(block) => block, // may be None Err(e) => panic!("failed to get block: {e}"), } } @@ -1610,15 +1950,26 @@ where /// Attempt to repair any identified gaps in the finalized blocks archive. /// /// Writes are buffered. Returns `true` if this call wrote repaired blocks and - /// needs a subsequent [`sync_finalized`](Self::sync_finalized). - async fn try_repair_gaps( + /// needs a subsequent [`sync_finalized`](Self::sync_finalized) or + /// [`start_finalized_sync`](Self::start_finalized_sync). + async fn try_repair_gaps( &mut self, buffer: &mut buffered::Mailbox, - resolver: &mut impl Resolver>, + resolver: &mut R, application: &mut impl Reporter>, - ) -> bool { + ) -> bool + where + R: Resolver, Subscriber = Annotation>, + K: PublicKey, + { + // Gap repair needs a known processed floor. A floor transition may + // jump the lower bound once its anchor block arrives. + if self.floor.blocks_progress() { + return false; + } + let mut wrote = false; - let start = self.last_processed_height.next(); + let start = self.floor.processed_height().next(); 'cache_repair: loop { let (gap_start, Some(gap_end)) = self.finalized_blocks.next_gap(start) else { // No gaps detected @@ -1646,7 +1997,6 @@ where block.clone(), finalization, application, - buffer, ) .await; debug!( @@ -1657,15 +2007,23 @@ where ); cursor = block; } else { - // Request the next missing block digest + // Request the next missing block by commitment, bounding the + // request by the parent height derived from the child block. + let parent_height = cursor + .height() + .previous() + .expect("cursor above gap start has a parent"); debug!( ?commitment, - target_height = cursor.height().get() - 1, + target_height = %parent_height, "requesting missing block from network for gap repair" ); - resolver - .fetch(Request::::Block(commitment)) - .await; + self.floor + .fetch_if_permitted( + resolver, + Request::finalized_block_by_height(commitment, parent_height), + ) + .ignore(); break 'cache_repair; } } @@ -1676,40 +2034,71 @@ where let missing_items = self .finalized_blocks .missing_items(start, self.max_repair.get()); - let requests = missing_items - .into_iter() - .map(|height| Request::::Finalized { - height: height.get(), - }) - .collect::>(); + let requests: Vec<_> = missing_items.into_iter().map(Request::finalized).collect(); if !requests.is_empty() { - resolver.fetch_all(requests).await + self.floor + .fetch_all_if_permitted(resolver, requests) + .ignore(); } 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. - async fn update_processed_height( + 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 + .try_set(self.floor.processed_height().get()); + + // Prune any existing requests below the new floor. + resolver.retain(handler::above_height_floor::(height)); + } + + /// Buffers a processed round update in memory and prunes round-bound requests. + async fn update_processed_round(&mut self, height: Height, resolver: &mut R) + where + R: Resolver, Subscriber = Annotation>, + { + let Some(finalization) = self.get_finalization_by_height(height).await else { + return; + }; + self.update_processed_round_floor(height, finalization.round(), resolver) + .await; + } + + /// Buffers a processed round floor update in memory and prunes round-bound requests. + async fn update_processed_round_floor( &mut self, height: Height, - resolver: &mut impl Resolver>, - ) { - self.application_metadata.put(LATEST_KEY.clone(), height); - self.last_processed_height = height; - #[cfg(feature = "prom")] - self.processed_height - .set(self.last_processed_height.get() as i64); - - // Cancel any existing requests below the new floor. - resolver - .retain( - Request::::Finalized { - height: height.get(), - } - .predicate(), - ) - .await; + round: Round, + resolver: &mut R, + ) where + R: Resolver, Subscriber = Annotation>, + { + if height > self.floor.processed_height() || round <= self.floor.processed_round() { + return; + } + + let previous = self.floor.processed_round(); + self.floor.set_processed_round(round); + + // Retain view-indexed cache data for a window behind the previously + // processed finalized block. + let prune_round = Round::new( + previous.epoch(), + previous.view().saturating_sub(self.view_retention_timeout), + ); + self.cache.prune_by_view(prune_round).await; + + // Prune round-bound requests at or below the processed round. + resolver.retain(handler::above_round_floor::( + self.floor.processed_round(), + )); } /// Prunes finalized blocks and certificates below the given height. @@ -1732,6 +2121,31 @@ where )?; Ok(()) } + + /// Prunes finalized archives and height-indexed certified cache data below the durable floor. + async fn prune_after_floor(&mut self, height: Height) -> Result<(), BoxedError> { + let cache = &mut self.cache; + let finalized_blocks = &mut self.finalized_blocks; + let finalizations_by_height = &mut self.finalizations_by_height; + try_join!( + async { + cache.prune_by_height(height).await; + Ok::<_, BoxedError>(()) + }, + async { + finalized_blocks.prune(height).await.map_err(Box::new)?; + Ok::<_, BoxedError>(()) + }, + async { + finalizations_by_height + .prune(height) + .await + .map_err(Box::new)?; + Ok::<_, BoxedError>(()) + } + )?; + Ok(()) + } } #[cfg(test)] diff --git a/syncer/src/cache.rs b/syncer/src/cache.rs index 327d7361..de8b7d90 100644 --- a/syncer/src/cache.rs +++ b/syncer/src/cache.rs @@ -3,22 +3,23 @@ use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::{ Block, simplex::types::{Finalization, Notarization}, - types::{Epoch, Round, View}, + types::{Epoch, Height, Round, View}, +}; +use commonware_runtime::{ + BufferPooler, Clock, Handle, Metrics, Spawner, Storage, buffer::paged::CacheRef, }; -use commonware_runtime::{BufferPooler, Clock, Metrics, Spawner, Storage, buffer::paged::CacheRef}; use commonware_storage::{ - archive::{self, Archive as _, Identifier, prunable}, + archive::{self, Archive as _, Identifier, MultiArchive as _, prunable}, metadata::{self, Metadata}, translator::TwoCap, }; -// Unused imports removed use governor::clock::Clock as GClock; use rand::Rng; use std::{ cmp::max, collections::BTreeMap, num::{NonZero, NonZeroUsize}, - time::Instant, + time::Duration, }; use tracing::{debug, info}; @@ -45,6 +46,8 @@ struct Cache< 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 @@ -57,8 +60,8 @@ impl< S: Scheme, > Cache { - /// Prune the archives to the given view. - async fn prune(&mut self, min_view: View) { + /// 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()), @@ -69,6 +72,14 @@ impl< Err(e) => panic!("failed to prune archives: {e}"), } } + + /// 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"); + } } /// Manages prunable caches and their metadata. @@ -104,7 +115,7 @@ impl< pub(crate) async fn init(context: R, cfg: Config, block_codec_config: B::Cfg) -> Self { // Initialize metadata let metadata = Metadata::init( - context.with_label("metadata"), + context.child("metadata"), metadata::Config { partition: format!("{}-metadata", cfg.partition_prefix), codec_config: ((), ()), @@ -181,31 +192,50 @@ impl< /// Helper to initialize the cache for a given epoch. async fn init_epoch(&mut self, epoch: Epoch) { - let verified_blocks = self - .init_archive(epoch, "verified", self.block_codec_config.clone()) - .await; - let notarized_blocks = self - .init_archive(epoch, "notarized", self.block_codec_config.clone()) - .await; - let notarizations = self - .init_archive( + let context = self.context.child("epoch").with_attribute("epoch", epoch); + let (verified_blocks, notarized_blocks, certified_blocks, notarizations, finalizations) = futures::join!( + Self::init_archive( + &context, + &self.cfg, + epoch, + "verified", + self.block_codec_config.clone() + ), + Self::init_archive( + &context, + &self.cfg, + epoch, + "notarized", + self.block_codec_config.clone() + ), + Self::init_archive( + &context, + &self.cfg, + epoch, + "certified", + self.block_codec_config.clone() + ), + Self::init_archive( + &context, + &self.cfg, epoch, "notarizations", S::certificate_codec_config_unbounded(), - ) - .await; - let finalizations = self - .init_archive( + ), + Self::init_archive( + &context, + &self.cfg, epoch, "finalizations", S::certificate_codec_config_unbounded(), - ) - .await; + ), + ); let existing = self.caches.insert( epoch, Cache { verified_blocks, notarized_blocks, + certified_blocks, notarizations, finalizations, }, @@ -215,75 +245,130 @@ impl< /// Helper to initialize an archive. async fn init_archive( - &self, + ctx: &R, + cfg: &Config, epoch: Epoch, - name: &str, + name: &'static str, codec_config: T::Cfg, ) -> prunable::Archive { - let start = Instant::now(); - let cfg = prunable::Config { + let start = ctx.current(); + let archive_cfg = prunable::Config { translator: TwoCap, - key_partition: format!("{}-cache-{epoch}-{name}-key", self.cfg.partition_prefix), - key_page_cache: self.cfg.key_page_cache.clone(), - value_partition: format!("{}-cache-{epoch}-{name}-value", self.cfg.partition_prefix), - items_per_section: self.cfg.prunable_items_per_section, + 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: self.cfg.replay_buffer, - key_write_buffer: self.cfg.key_write_buffer, - value_write_buffer: self.cfg.value_write_buffer, + replay_buffer: cfg.replay_buffer, + key_write_buffer: cfg.key_write_buffer, + value_write_buffer: cfg.value_write_buffer, }; - let archive = prunable::Archive::init( - self.context - .with_label(&format!("{name}_{epoch}")) - .with_attribute("epoch", epoch), - cfg, - ) - .await - .unwrap_or_else(|_| panic!("failed to initialize {name} archive")); - info!(elapsed = ?start.elapsed(), "restored {name} archive"); + let archive = prunable::Archive::init(ctx.child(name), archive_cfg) + .await + .unwrap_or_else(|_| panic!("failed to initialize {name} archive")); + info!(elapsed = ?ctx.current().duration_since(start).unwrap_or(Duration::ZERO), "restored {name} archive"); archive } - /// Add a verified block to the prunable archive. - pub(crate) async fn put_verified(&mut self, round: Round, commitment: B::Digest, block: B) { + /// Add a verified block to the prunable archive and start syncing it. + pub(crate) async fn put_verified( + &mut self, + round: Round, + commitment: B::Digest, + block: B, + ) -> Handle<()> { let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { - return; + 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 .verified_blocks - .put_sync(round.view().get(), commitment, block) + .put_multi_start_sync(view, commitment, block) .await; - Self::handle_result(result, round, "verified"); + Self::handle_start_result(result, round, "verified") } - /// Add a notarized block to the prunable archive. - pub(crate) async fn put_block(&mut self, round: Round, commitment: B::Digest, block: B) { - let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { + /// Add a certified block to the height-indexed archive. + pub(crate) async fn put_certified( + &mut self, + epoch: Epoch, + height: Height, + commitment: B::Digest, + block: B, + ) { + 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 + .certified_blocks + .has_at(height.get(), &commitment) + .await + { + Ok(true) => return, + Ok(false) => {} + Err(e) => panic!("failed to check certified block: {e}"), + } + + match 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}"), + } + } + + /// Add a notarized block to the prunable archive and start syncing it. + pub(crate) async fn put_block( + &mut self, + round: Round, + commitment: B::Digest, + block: B, + ) -> Handle<()> { + let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { + return Handle::ready(Ok(())); + }; let result = cache .notarized_blocks - .put_sync(round.view().get(), commitment, block) + .put_start_sync(round.view().get(), commitment, block) .await; - Self::handle_result(result, round, "notarized"); + Self::handle_start_result(result, round, "notarized") } - /// Add a notarization to the prunable archive. + /// Add a notarization to the prunable archive and start syncing it. pub(crate) async fn put_notarization( &mut self, round: Round, commitment: B::Digest, notarization: Notarization, - ) { + ) -> Handle<()> { let Some(cache) = self.get_or_init_epoch(round.epoch()).await else { - return; + return Handle::ready(Ok(())); }; let result = cache .notarizations - .put_sync(round.view().get(), commitment, notarization) + .put_start_sync(round.view().get(), commitment, notarization) .await; - Self::handle_result(result, round, "notarization"); + Self::handle_start_result(result, round, "notarization") } /// Add a finalization to the prunable archive. @@ -318,6 +403,56 @@ impl< } } + 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}"), + } + } + + /// 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 + .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") + } + + /// 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", + ) + } + /// Get a notarization from the prunable archive by round. pub(crate) async fn get_notarization( &self, @@ -331,6 +466,20 @@ impl< .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. + pub(crate) async fn get_verified(&self, round: Round) -> Option { + let cache = self.caches.get(&round.epoch())?; + cache + .verified_blocks + .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, @@ -346,8 +495,17 @@ impl< None } - /// Looks for a block (verified or notarized). + /// 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 @@ -356,6 +514,7 @@ impl< .get(Identifier::Key(&commitment)) .await .expect("failed to get verified block") + && predicate(&block) { return Some(block); } @@ -366,6 +525,18 @@ impl< .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); } @@ -373,8 +544,8 @@ impl< None } - /// Prune the caches below the given round. - pub(crate) async fn prune(&mut self, round: Round) { + /// 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 @@ -387,12 +558,13 @@ impl< 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"); } @@ -407,7 +579,14 @@ impl< // Prune archives for the given epoch let min_view = round.view(); if let Some(prunable) = self.caches.get_mut(&round.epoch()) { - prunable.prune(min_view).await; + prunable.prune_by_view(min_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 1f33ed59..efd4b839 100644 --- a/syncer/src/config.rs +++ b/syncer/src/config.rs @@ -38,7 +38,7 @@ where pub partition_prefix: String, /// Size of backfill request/response mailbox. - pub mailbox_size: usize, + pub mailbox_size: NonZeroUsize, /// Minimum number of views to retain temporary data after the application processes a block. /// diff --git a/syncer/src/delivery.rs b/syncer/src/delivery.rs new file mode 100644 index 00000000..216bca13 --- /dev/null +++ b/syncer/src/delivery.rs @@ -0,0 +1,30 @@ +use commonware_consensus::{ + Block, + simplex::scheme::Scheme, + simplex::types::{Finalization, Notarization}, +}; +use commonware_utils::channel::oneshot; + +/// A parsed-but-unverified resolver delivery awaiting batch certificate verification. +pub(crate) enum PendingVerification, B: Block> { + Notarized { + notarization: Notarization, + block: B, + response: oneshot::Sender, + }, + Finalized { + finalization: Finalization, + block: B, + response: oneshot::Sender, + }, +} + +impl, B: Block> PendingVerification { + pub(crate) fn response_closed(&self) -> bool { + match self { + Self::Notarized { response, .. } | Self::Finalized { response, .. } => { + response.is_closed() + } + } + } +} diff --git a/syncer/src/durability.rs b/syncer/src/durability.rs new file mode 100644 index 00000000..72c7291e --- /dev/null +++ b/syncer/src/durability.rs @@ -0,0 +1,153 @@ +//! Helpers for observing deferred storage syncs and gating finalized dispatch. + +use commonware_consensus::types::{Height, Round}; +use commonware_runtime::{Error, Handle}; +use std::{collections::BTreeMap, future::Future}; +use tracing::debug; + +/// Applies the syncer's fatal policy when awaiting a durable-sync handle. +pub(crate) trait Durable { + /// Resolves once the sync is durable. Storage failures are fatal; `false` + /// only indicates runtime shutdown before completion. + fn durable(self, round: Round, name: &'static str) -> impl Future + Send; +} + +impl Durable for Handle<()> { + async fn durable(self, round: Round, name: &'static str) -> bool { + match self.await { + Ok(()) => true, + Err(Error::Closed | Error::Aborted) => { + debug!(name, "runtime shutdown before sync completed"); + false + } + Err(e) => panic!("failed to sync {name} at {round}: {e}"), + } + } +} + +/// Defers finalized-block dispatch until a sync covering each buffered write completes. +#[derive(Default)] +pub(crate) struct DispatchGate { + unsynced: Option, + inflight: BTreeMap, + next_seq: u64, +} + +impl DispatchGate { + pub(crate) fn defer(&mut self, height: Height) { + self.unsynced = Some(self.unsynced.map_or(height, |lowest| lowest.min(height))); + } + + pub(crate) fn adopt(&mut self) -> Option { + let lowest = self.unsynced.take()?; + let seq = self.next_seq; + self.next_seq += 1; + self.inflight.insert(seq, lowest); + Some(seq) + } + + pub(crate) fn release(&mut self, seq: u64) { + self.inflight = self.inflight.split_off(&(seq + 1)); + } + + pub(crate) fn clear(&mut self) { + self.unsynced = None; + self.inflight.clear(); + } + + pub(crate) fn barrier(&self) -> Option { + self.inflight.values().copied().chain(self.unsynced).min() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use commonware_runtime::{Runner as _, deterministic}; + + #[test] + fn durable_resolves_true_on_success() { + deterministic::Runner::default().start(|_| async move { + assert!(Handle::ready(Ok(())).durable(Round::zero(), "test").await); + }); + } + + #[test] + fn durable_reports_shutdown_as_not_durable() { + deterministic::Runner::default().start(|_| async move { + assert!( + !Handle::ready(Err(Error::Closed)) + .durable(Round::zero(), "test") + .await + ); + assert!( + !Handle::ready(Err(Error::Aborted)) + .durable(Round::zero(), "test") + .await + ); + }); + } + + #[test] + #[should_panic(expected = "failed to sync test")] + fn durable_panics_on_sync_failure() { + deterministic::Runner::default().start(|_| async move { + let _ = Handle::<()>::ready(Err(Error::WriteFailed)) + .durable(Round::zero(), "test") + .await; + }); + } + + #[test] + fn gate_defer_keeps_lowest_write() { + let mut gate = DispatchGate::default(); + gate.defer(Height::new(5)); + gate.defer(Height::new(3)); + gate.defer(Height::new(7)); + assert_eq!(gate.barrier(), Some(Height::new(3))); + } + + #[test] + fn gate_adopt_moves_writes_to_one_batch() { + let mut gate = DispatchGate::default(); + assert_eq!(gate.adopt(), None); + gate.defer(Height::new(5)); + let seq = gate.adopt().expect("deferred write must adopt"); + assert_eq!(gate.adopt(), None); + gate.release(seq); + assert_eq!(gate.barrier(), None); + } + + #[test] + fn gate_release_covers_earlier_batches_only() { + let mut gate = DispatchGate::default(); + gate.defer(Height::new(5)); + let first = gate.adopt().expect("first batch"); + gate.defer(Height::new(8)); + let second = gate.adopt().expect("second batch"); + gate.release(first); + assert_eq!(gate.barrier(), Some(Height::new(8))); + gate.release(second); + assert_eq!(gate.barrier(), None); + + let mut out_of_order = DispatchGate::default(); + out_of_order.defer(Height::new(5)); + out_of_order.adopt().expect("first batch"); + out_of_order.defer(Height::new(8)); + let newest = out_of_order.adopt().expect("second batch"); + out_of_order.release(newest); + assert_eq!(out_of_order.barrier(), None); + } + + #[test] + fn gate_clear_does_not_release_later_batches() { + let mut gate = DispatchGate::default(); + gate.defer(Height::new(5)); + let stale = gate.adopt().expect("first batch"); + gate.clear(); + gate.defer(Height::new(9)); + gate.adopt().expect("post-clear batch"); + gate.release(stale); + assert_eq!(gate.barrier(), Some(Height::new(9))); + } +} diff --git a/syncer/src/floor.rs b/syncer/src/floor.rs new file mode 100644 index 00000000..05559362 --- /dev/null +++ b/syncer/src/floor.rs @@ -0,0 +1,417 @@ +use crate::ingress::handler::{Annotation, Key, Request}; +use commonware_consensus::{ + simplex::types::Finalization, + types::{Height, Round}, +}; +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. +#[derive(Clone, Copy)] +struct ProcessedFloor { + height: Option, + round: Round, +} + +impl ProcessedFloor { + /// Returns true when the resolver request is above all processed floors. + fn permits(&self, fetch: &Request) -> bool { + if let Some(height) = self.height + && !fetch.above_height_floor(height) + { + return false; + } + + fetch.above_round_floor(self.round) + } +} + +#[must_use = "fetch admission must be handled explicitly"] +pub(crate) enum FetchAdmission { + Issued, + Denied, +} + +impl FetchAdmission { + pub(crate) const fn denied(self) -> bool { + matches!(self, Self::Denied) + } + + pub(crate) const fn ignore(self) {} +} + +/// The processed floor plus any pending floor update awaiting its anchor block. +pub(crate) struct Floor { + processed: ProcessedFloor, + pending: Option>, +} + +impl Floor { + pub(crate) const fn resolved(height: Option, round: Round) -> Self { + Self { + processed: ProcessedFloor { height, round }, + pending: None, + } + } + + pub(crate) const fn processed_height(&self) -> Height { + match self.processed.height { + Some(height) => height, + None => Height::zero(), + } + } + + pub(crate) const fn processed_round(&self) -> Round { + self.processed.round + } + + pub(crate) const fn set_processed_height(&mut self, height: Height) { + self.processed.height = Some(height); + } + + pub(crate) const fn set_processed_round(&mut self, round: Round) { + self.processed.round = round; + } + + /// Returns true while repair and application dispatch must wait for the floor anchor. + pub(crate) const fn blocks_progress(&self) -> bool { + self.pending.is_some() + } + + /// Returns true if a pending floor already supersedes the candidate floor round. + pub(crate) fn has_pending_anchor_at_or_after(&self, round: Round) -> bool { + matches!(&self.pending, Some(pending) if pending.round() >= round) + } + + /// Returns true when `commitment` is the awaited anchor. + pub(crate) fn matches_pending_anchor(&self, commitment: C) -> bool { + matches!(&self.pending, Some(pending) if pending.proposal.payload == commitment) + } + + /// Records a verified floor finalization whose block anchor still needs to arrive. + pub(crate) fn await_anchor(&mut self, finalization: Finalization) { + self.pending = Some(finalization); + } + + /// Takes the pending anchor finalization, if any. + #[must_use] + pub(crate) const fn take_pending_anchor(&mut self) -> Option> { + self.pending.take() + } + + pub(crate) fn fetch_if_permitted( + &self, + resolver: &mut R, + fetch: Request, + ) -> FetchAdmission + where + R: Resolver, Subscriber = Annotation>, + { + if !self.processed.permits(&fetch) { + return FetchAdmission::Denied; + } + resolver.fetch(fetch); + FetchAdmission::Issued + } + + pub(crate) fn fetch_targeted_if_permitted( + &self, + resolver: &mut R, + fetch: Request, + targets: NonEmptyVec, + ) -> FetchAdmission + where + R: TargetedResolver, Subscriber = Annotation>, + { + if !self.processed.permits(&fetch) { + return FetchAdmission::Denied; + } + resolver.fetch_targeted(fetch, targets); + FetchAdmission::Issued + } + + pub(crate) fn fetch_all_if_permitted( + &self, + resolver: &mut R, + fetches: Vec>, + ) -> FetchAdmission + where + R: Resolver, Subscriber = Annotation>, + { + let fetches = fetches + .into_iter() + .filter(|fetch| self.processed.permits(fetch)) + .collect::>(); + if fetches.is_empty() { + return FetchAdmission::Denied; + } + resolver.fetch_all(fetches); + FetchAdmission::Issued + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ingress::handler::Finalized; + use commonware_actor::Feedback; + use commonware_consensus::simplex::scheme::ed25519 as simplex_ed25519; + use commonware_consensus::types::{Epoch, View}; + use commonware_cryptography::{Signer as _, ed25519 as crypto_ed25519, sha256::Sha256}; + use commonware_math::algebra::Random as _; + use commonware_resolver::Fetch; + use commonware_utils::sync::Mutex; + use std::sync::Arc; + + type TestDigest = ::Digest; + type TestScheme = simplex_ed25519::Scheme; + type FetchRecord = Fetch, Annotation>; + type RecordedFetches = Arc>>; + type RecordedTargets = Arc>>>; + + #[derive(Clone, Default)] + struct TestResolver { + fetches: RecordedFetches, + targeted: RecordedTargets, + } + + impl TestResolver { + fn fetches(&self) -> Vec { + self.fetches.lock().clone() + } + + fn targeted(&self) -> Vec> { + self.targeted.lock().clone() + } + } + + impl Resolver for TestResolver { + type Key = Key; + type Subscriber = Annotation; + + fn fetch(&mut self, fetch: F) -> Feedback + where + F: Into> + Send, + { + self.fetches.lock().push(fetch.into()); + Feedback::Ok + } + + fn fetch_all(&mut self, fetches: Vec) -> Feedback + where + F: Into> + Send, + { + self.fetches + .lock() + .extend(fetches.into_iter().map(Into::into)); + Feedback::Ok + } + + fn retain( + &mut self, + _predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static, + ) -> Feedback { + Feedback::Ok + } + } + + impl TargetedResolver for TestResolver { + type PublicKey = crypto_ed25519::PublicKey; + + fn fetch_targeted( + &mut self, + fetch: impl Into> + Send, + _targets: NonEmptyVec, + ) -> Feedback { + self.targeted.lock().push(fetch.into().key); + Feedback::Ok + } + + fn fetch_all_targeted( + &mut self, + fetches: Vec<(F, NonEmptyVec)>, + ) -> Feedback + where + F: Into> + Send, + { + self.targeted + .lock() + .extend(fetches.into_iter().map(|(fetch, _)| fetch.into().key)); + Feedback::Ok + } + } + + fn round(view: u64) -> Round { + Round::new(Epoch::zero(), View::new(view)) + } + + fn digest(byte: u8) -> TestDigest { + use commonware_cryptography::Hasher as _; + let mut hasher = Sha256::new(); + hasher.update(&[byte]); + hasher.finalize() + } + + fn floor() -> Floor { + Floor::resolved(Some(Height::new(5)), round(5)) + } + + #[test] + fn fetch_if_permitted_applies_height_and_round_floors() { + let floor = floor(); + let mut resolver = TestResolver::default(); + + assert!( + floor + .fetch_if_permitted(&mut resolver, Request::finalized(Height::new(5))) + .denied() + ); + assert!( + floor + .fetch_if_permitted( + &mut resolver, + Request::finalized_block_by_height(digest(1), Height::new(4)), + ) + .denied() + ); + assert!( + floor + .fetch_if_permitted(&mut resolver, Request::notarized(round(5))) + .denied() + ); + assert!(resolver.fetches().is_empty()); + + assert!( + !floor + .fetch_if_permitted(&mut resolver, Request::finalized(Height::new(6))) + .denied() + ); + assert!( + !floor + .fetch_if_permitted(&mut resolver, Request::notarized(round(6))) + .denied() + ); + + let fetches = resolver.fetches(); + assert_eq!(fetches.len(), 2); + assert!(matches!( + fetches[0], + Fetch { + key: Key::Finalized { height: 6 }, + subscriber: Annotation::Finalized(Finalized::ByHeight { + height: subscriber_height + }), + .. + } if subscriber_height == Height::new(6) + )); + assert!(matches!( + fetches[1], + Fetch { + key: Key::Notarized { + round: request_round + }, + subscriber: Annotation::Notarization { + round: subscriber_round + }, + .. + } if request_round == round(6) && subscriber_round == round(6) + )); + } + + #[test] + fn fetch_targeted_if_permitted_returns_denied_without_fetching() { + let floor = floor(); + let mut resolver = TestResolver::default(); + let mut rng = commonware_utils::test_rng(); + let target = crypto_ed25519::PrivateKey::random(&mut rng).public_key(); + + assert!( + floor + .fetch_targeted_if_permitted( + &mut resolver, + Request::finalized(Height::new(5)), + NonEmptyVec::new(target.clone()), + ) + .denied() + ); + assert!(resolver.targeted().is_empty()); + + assert!( + !floor + .fetch_targeted_if_permitted( + &mut resolver, + Request::finalized(Height::new(6)), + NonEmptyVec::new(target), + ) + .denied() + ); + assert_eq!(resolver.targeted(), vec![Key::Finalized { height: 6 }]); + } + + #[test] + fn fetch_all_if_permitted_filters_denied_requests() { + let floor = floor(); + let mut resolver = TestResolver::default(); + + assert!( + !floor + .fetch_all_if_permitted( + &mut resolver, + vec![ + Request::finalized(Height::new(5)), + Request::finalized(Height::new(6)), + Request::notarized(round(5)), + Request::notarized(round(6)), + ], + ) + .denied() + ); + + let fetches = resolver.fetches(); + assert_eq!(fetches.len(), 2); + assert!(matches!(fetches[0].key, Key::Finalized { height: 6 })); + assert!( + matches!(fetches[1].key, Key::Notarized { round: request_round } if request_round == round(6)) + ); + + let mut resolver = TestResolver::default(); + assert!( + floor + .fetch_all_if_permitted( + &mut resolver, + vec![ + Request::finalized(Height::new(5)), + Request::notarized(round(5)), + ], + ) + .denied() + ); + assert!(resolver.fetches().is_empty()); + } + + #[test] + fn fetch_if_permitted_without_height_floor_allows_genesis_height() { + let floor = Floor::::resolved(None, round(5)); + let mut resolver = TestResolver::default(); + + assert!( + !floor + .fetch_if_permitted(&mut resolver, Request::finalized(Height::zero())) + .denied() + ); + + let fetches = resolver.fetches(); + assert_eq!(fetches.len(), 1); + assert!(matches!( + fetches[0], + Fetch { + key: Key::Finalized { height: 0 }, + subscriber: Annotation::Finalized(Finalized::ByHeight { + height: subscriber_height + }), + .. + } if subscriber_height == Height::zero() + )); + } +} diff --git a/syncer/src/ingress/handler.rs b/syncer/src/ingress/handler.rs index 062f8e7d..e00906b9 100644 --- a/syncer/src/ingress/handler.rs +++ b/syncer/src/ingress/handler.rs @@ -1,17 +1,20 @@ use bytes::{Buf, BufMut, Bytes}; +use commonware_actor::mailbox::{ + self, Overflow, UnreliablePolicy, UnreliableReceiver, UnreliableSender, +}; use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt, Write}; -use commonware_consensus::types::Round; +use commonware_consensus::types::{Height, Round}; use commonware_cryptography::Digest; -use commonware_resolver::{Consumer, p2p::Producer}; -use commonware_utils::{ - Span, - channel::{mpsc, oneshot}, -}; +use commonware_resolver::{Consumer, Delivery, Fetch as ResolverFetch, p2p::Producer}; +use commonware_runtime::Metrics; +use commonware_utils::{Span, channel::oneshot}; use std::{ + collections::VecDeque, fmt::{Debug, Display}, hash::{Hash, Hasher}, + num::NonZeroUsize, + sync::mpsc::TryRecvError, }; -use tracing::error; /// The subject of a backfill request. const BLOCK_REQUEST: u8 = 0; @@ -19,12 +22,12 @@ const FINALIZED_REQUEST: u8 = 1; const NOTARIZED_REQUEST: u8 = 2; /// Messages sent from the resolver's [Consumer]/[Producer] implementation -/// to the marshal [Actor](super::super::actor::Actor). +/// to the syncer [Actor](crate::actor::Actor). pub enum Message { /// A request to deliver a value for a given key. Deliver { - /// The key of the value being delivered. - key: Request, + /// The delivery metadata attached to the resolved value. + delivery: Delivery, Annotation>, /// The value being delivered. value: Bytes, /// A channel to send the result of the delivery (true for success). @@ -33,82 +36,200 @@ pub enum Message { /// A request to produce a value for a given key. Produce { /// The key of the value to produce. - key: Request, + key: Key, /// A channel to send the produced value. response: oneshot::Sender, }, } -/// A handler that forwards requests from the resolver to the marshal actor. +impl Message { + /// Returns true if the requester has stopped waiting for this response. + pub(crate) fn response_closed(&self) -> bool { + match self { + Self::Deliver { response, .. } => response.is_closed(), + Self::Produce { response, .. } => response.is_closed(), + } + } +} + +/// Pending resolver handler messages retained after the mailbox fills. +pub struct Pending(VecDeque>); + +impl Default for Pending { + fn default() -> Self { + Self(VecDeque::new()) + } +} + +impl Overflow> for Pending { + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn drain(&mut self, mut push: F) + where + F: FnMut(Message) -> Option>, + { + while let Some(message) = self.0.pop_front() { + if message.response_closed() { + continue; + } + + if let Some(message) = push(message) { + self.0.push_front(message); + break; + } + } + } +} + +impl UnreliablePolicy for Message { + type Overflow = Pending; + + fn handle(overflow: &mut Self::Overflow, message: Self) -> bool { + if message.response_closed() { + return true; + } + + match message { + // Responses to our own fetches must reach the syncer for validation. + message @ Self::Deliver { .. } => { + overflow.0.push_back(message); + true + } + // Reject peer-initiated work rather than retaining an unbounded serve backlog. + Self::Produce { .. } => false, + } + } +} + +/// A handler that forwards requests from the resolver to the syncer actor. /// /// This struct implements the [Consumer] and [Producer] traits from the /// resolver, and acts as a bridge to the main actor loop. #[derive(Clone)] pub struct Handler { - sender: mpsc::Sender>, + sender: UnreliableSender>, } impl Handler { /// Creates a new handler. - pub const fn new(sender: mpsc::Sender>) -> Self { + pub const fn new(sender: UnreliableSender>) -> Self { Self { sender } } } +/// Creates a resolver receiver and handler pair. +pub fn init(metrics: impl Metrics, capacity: NonZeroUsize) -> (Receiver, Handler) { + let (sender, receiver) = mailbox::new_unreliable(metrics, capacity); + (Receiver::new(receiver), Handler::new(sender)) +} + +/// Receiver for resolver handler messages. +pub struct Receiver { + inner: UnreliableReceiver>, +} + +impl Receiver { + pub(crate) const fn new(inner: UnreliableReceiver>) -> Self { + Self { inner } + } + + pub(crate) async fn recv(&mut self) -> Option> { + self.inner.recv().await + } + + pub(crate) fn try_recv(&mut self) -> Result, TryRecvError> { + self.inner.try_recv() + } +} + impl Consumer for Handler { - type Key = Request; + type Key = Key; type Value = Bytes; - type Failure = (); + type Subscriber = Annotation; - async fn deliver(&mut self, key: Self::Key, value: Self::Value) -> bool { + fn deliver( + &mut self, + delivery: Delivery, + value: Self::Value, + ) -> oneshot::Receiver { let (response, receiver) = oneshot::channel(); - if self - .sender - .send(Message::Deliver { - key, - value, - response, - }) - .await - .is_err() - { - error!("failed to send deliver message to actor: receiver dropped"); - return false; - } - receiver.await.unwrap_or(false) - } - - async fn failed(&mut self, _: Self::Key, _: Self::Failure) { - // We don't need to do anything on failure, the resolver will retry. + let _ = self.sender.enqueue(Message::Deliver { + delivery, + value, + response, + }); + receiver } } impl Producer for Handler { - type Key = Request; + type Key = Key; - async fn produce(&mut self, key: Self::Key) -> oneshot::Receiver { + fn produce(&mut self, key: Self::Key) -> oneshot::Receiver { let (response, receiver) = oneshot::channel(); - if self - .sender - .send(Message::Produce { key, response }) - .await - .is_err() - { - error!("failed to send produce message to actor: receiver dropped"); - } + let _ = self.sender.enqueue(Message::Produce { key, response }); receiver } } -/// A request for backfilling data. -#[derive(Clone)] -pub enum Request { +/// Local processing annotation for a resolved key. +/// +/// The resolver key is the peer-visible lookup. An annotation is local +/// metadata attached to that lookup so the syncer can decide how to process +/// the response after validating it against the key. It is not part of peer +/// response validity. Multiple local annotations may share one peer key when +/// they depend on the same block. +/// +/// [`Notarization`](Annotation::Notarization) carries round-bound local +/// context. [`Certified`](Annotation::Certified) and +/// [`Finalized`](Annotation::Finalized) describe how block-bearing responses +/// should be processed locally. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Annotation { + /// A notarization requested by round. + Notarization { round: Round }, + /// A block requested by commitment for a certified chain. + /// + /// The expected height is local pruning metadata and should only be + /// supplied when the caller has a validated height bound. It must not make + /// a commitment-matching response invalid, and certified storage uses the + /// fetched block's decoded height. + Certified { height: Height }, + /// A block requested by commitment for the finalized chain. + Finalized(Finalized), +} + +/// Metadata for a finalized block requested by commitment. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Finalized { + /// The finalized height is known before the request. + ByHeight { height: Height }, + /// Only the finalization round is known before the request. + /// + /// This happens when a finalization names the block commitment but not the + /// block height. + ByRound { round: Round }, +} + +/// A raw resolver key for backfilling data. +/// +/// The `Finalized` height is a raw `u64` on the wire (fixed-width encoding) +/// rather than a varint-encoded [`Height`]. +#[derive(Clone, Copy)] +pub enum Key { + /// Fetch a block by consensus commitment. Block(D), - Finalized { height: u64 }, - Notarized { round: Round }, + Finalized { + height: u64, + }, + Notarized { + round: Round, + }, } -impl Request { +impl Key { /// The subject of the request. const fn subject(&self) -> u8 { match self { @@ -117,26 +238,159 @@ impl Request { Self::Notarized { .. } => NOTARIZED_REQUEST, } } +} - /// The predicate to use when pruning subjects related to this subject. - /// - /// Specifically, any subjects unrelated will be left unmodified. Any related - /// subjects will be pruned if they are "less than or equal to" this subject. - pub fn predicate(&self) -> impl Fn(&Self) -> bool + Send + 'static { - let cloned = self.clone(); - move |s| match (&cloned, &s) { - (Self::Block(_), _) => unreachable!("we should never retain by block"), - (Self::Finalized { height: mine }, Self::Finalized { height: theirs }) => { - *theirs > *mine +/// A valid syncer backfill fetch request. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RequestKind { + /// Fetch a notarized proposal for a round. + Notarized { round: Round }, + /// Fetch a finalization for a height. + Finalized { height: Height }, + /// Fetch a certified-chain block by commitment. + CertifiedBlock { commitment: D, height: Height }, + /// Fetch a finalized-chain block by commitment when its height is known. + FinalizedBlockByHeight { commitment: D, height: Height }, + /// Fetch a finalized-chain block by commitment when only its finalization round is known. + FinalizedBlockByRound { commitment: D, round: Round }, +} + +/// A syncer backfill fetch with a request and local processing annotation that match. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Request { + kind: RequestKind, +} + +impl Request { + /// Fetch a notarized proposal for `round`. + pub const fn notarized(round: Round) -> Self { + Self { + kind: RequestKind::Notarized { round }, + } + } + + /// Fetch a finalization for `height`. + pub const fn finalized(height: Height) -> Self { + Self { + kind: RequestKind::Finalized { height }, + } + } + + /// Fetch a certified-chain block by commitment. + pub const fn certified_block(commitment: D, height: Height) -> Self { + Self { + kind: RequestKind::CertifiedBlock { commitment, height }, + } + } + + /// Fetch a finalized-chain block by commitment when its height is known. + pub const fn finalized_block_by_height(commitment: D, height: Height) -> Self { + Self { + kind: RequestKind::FinalizedBlockByHeight { commitment, height }, + } + } + + /// Fetch a finalized-chain block by commitment when only its finalization round is known. + pub const fn finalized_block_by_round(commitment: D, round: Round) -> Self { + Self { + kind: RequestKind::FinalizedBlockByRound { commitment, round }, + } + } + + pub(crate) fn above_height_floor(&self, floor: Height) -> bool { + match self.kind { + RequestKind::Finalized { height } + | RequestKind::CertifiedBlock { height, .. } + | RequestKind::FinalizedBlockByHeight { height, .. } => height > floor, + RequestKind::Notarized { .. } | RequestKind::FinalizedBlockByRound { .. } => true, + } + } + + pub(crate) fn above_round_floor(&self, floor: Round) -> bool { + match self.kind { + RequestKind::Notarized { round } | RequestKind::FinalizedBlockByRound { round, .. } => { + round > floor } - (Self::Finalized { .. }, _) => true, - (Self::Notarized { round: mine }, Self::Notarized { round: theirs }) => *theirs > *mine, - (Self::Notarized { .. }, _) => true, + RequestKind::Finalized { .. } + | RequestKind::CertifiedBlock { .. } + | RequestKind::FinalizedBlockByHeight { .. } => true, + } + } + + pub(crate) fn into_inner(self) -> ResolverFetch, Annotation> { + match self.kind { + RequestKind::Notarized { round } => ResolverFetch { + key: Key::Notarized { round }, + subscriber: Annotation::Notarization { round }, + span: tracing::Span::current(), + }, + RequestKind::Finalized { height } => ResolverFetch { + key: Key::Finalized { + height: height.get(), + }, + subscriber: Annotation::Finalized(Finalized::ByHeight { height }), + span: tracing::Span::current(), + }, + RequestKind::CertifiedBlock { commitment, height } => ResolverFetch { + key: Key::Block(commitment), + subscriber: Annotation::Certified { height }, + span: tracing::Span::current(), + }, + RequestKind::FinalizedBlockByHeight { commitment, height } => ResolverFetch { + key: Key::Block(commitment), + subscriber: Annotation::Finalized(Finalized::ByHeight { height }), + span: tracing::Span::current(), + }, + RequestKind::FinalizedBlockByRound { commitment, round } => ResolverFetch { + key: Key::Block(commitment), + subscriber: Annotation::Finalized(Finalized::ByRound { round }), + span: tracing::Span::current(), + }, } } } -impl Write for Request { +impl From> for ResolverFetch, Annotation> { + fn from(fetch: Request) -> Self { + fetch.into_inner() + } +} + +/// Returns a predicate that keeps resolver requests above the processed height floor. +/// +/// Unrelated requests are retained. Height-bound requests are pruned once the +/// processed height reaches them. +pub(crate) fn above_height_floor( + height: Height, +) -> impl Fn(&Key, &Annotation) -> bool + Send + 'static { + move |request, annotation| match (request, annotation) { + (Key::Finalized { height: requested }, _) => *requested > height.get(), + ( + Key::Block(_), + Annotation::Certified { height: requested } + | Annotation::Finalized(Finalized::ByHeight { height: requested }), + ) => *requested > height, + _ => true, + } +} + +/// Returns a predicate that keeps resolver requests above the processed round floor. +/// +/// Unrelated requests are retained. Round-bound requests are pruned once the +/// processed round reaches them. +pub(crate) fn above_round_floor( + round: Round, +) -> impl Fn(&Key, &Annotation) -> bool + Send + 'static { + move |request, annotation| match (request, annotation) { + (Key::Notarized { round: requested }, _) => *requested > round, + (Key::Block(_), Annotation::Finalized(Finalized::ByRound { round: requested })) => { + *requested > round + } + _ => true, + } +} + +impl Write for Key { fn write(&self, buf: &mut impl BufMut) { self.subject().write(buf); match self { @@ -147,7 +401,7 @@ impl Write for Request { } } -impl Read for Request { +impl Read for Key { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { @@ -165,19 +419,19 @@ impl Read for Request { } } -impl EncodeSize for Request { +impl EncodeSize for Key { fn encode_size(&self) -> usize { 1 + match self { - Self::Block(block) => block.encode_size(), + Self::Block(commitment) => commitment.encode_size(), Self::Finalized { height } => height.encode_size(), Self::Notarized { round } => round.encode_size(), } } } -impl Span for Request {} +impl Span for Key {} -impl PartialEq for Request { +impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { match (&self, &other) { (Self::Block(a), Self::Block(b)) => a == b, @@ -188,9 +442,9 @@ impl PartialEq for Request { } } -impl Eq for Request {} +impl Eq for Key {} -impl Ord for Request { +impl Ord for Key { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match (&self, &other) { (Self::Block(a), Self::Block(b)) => a.cmp(b), @@ -201,24 +455,24 @@ impl Ord for Request { } } -impl PartialOrd for Request { +impl PartialOrd for Key { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Hash for Request { +impl Hash for Key { fn hash(&self, state: &mut H) { self.subject().hash(state); match self { - Self::Block(digest) => digest.hash(state), + Self::Block(commitment) => commitment.hash(state), Self::Finalized { height } => height.hash(state), Self::Notarized { round } => round.hash(state), } } } -impl Display for Request { +impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Block(commitment) => write!(f, "Block({commitment:?})"), @@ -228,7 +482,7 @@ impl Display for Request { } } -impl Debug for Request { +impl Debug for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Block(commitment) => write!(f, "Block({commitment:?})"), @@ -247,14 +501,107 @@ mod tests { Hasher as _, sha256::{Digest as Sha256Digest, Sha256}, }; + use commonware_runtime::{Runner as _, deterministic}; + use commonware_utils::{NZUsize, vec::NonEmptyVec}; use std::collections::BTreeSet; type D = Sha256Digest; + #[test] + fn full_mailbox_rejects_produce_and_retains_deliver() { + let executor = deterministic::Runner::default(); + executor.start(|context| async move { + let (mut receiver, mut handler) = init::(context, NZUsize!(1)); + + let first = handler.produce(Key::Finalized { height: 1 }); + let rejected = handler.produce(Key::Finalized { height: 2 }); + + let round = Round::new(Epoch::new(1), View::new(1)); + let delivered = handler.deliver( + Delivery { + key: Key::Notarized { round }, + subscribers: NonEmptyVec::new(( + Annotation::Notarization { round }, + tracing::Span::none(), + )), + }, + Bytes::new(), + ); + + assert!(rejected.await.is_err()); + + let Message::Produce { key, response } = receiver.recv().await.unwrap() else { + panic!("first message should be produce"); + }; + assert_eq!(key, Key::Finalized { height: 1 }); + response.send(Bytes::from_static(b"first")).unwrap(); + assert_eq!(first.await.unwrap(), Bytes::from_static(b"first")); + + let Message::Deliver { + delivery, response, .. + } = receiver.recv().await.unwrap() + else { + panic!("deliver message should be retained"); + }; + assert_eq!(delivery.key, Key::Notarized { round }); + response.send(true).unwrap(); + assert!(delivered.await.unwrap()); + }); + } + + #[test] + fn handler_drain_skips_closed_responses() { + let mut overflow = Pending::::default(); + let make_message = |height, response| Message::Deliver { + delivery: Delivery { + key: Key::Finalized { height }, + subscribers: NonEmptyVec::new(( + Annotation::Finalized(Finalized::ByHeight { + height: Height::new(height), + }), + tracing::Span::none(), + )), + }, + value: Bytes::new(), + response, + }; + + let (closed_response, closed_receiver) = oneshot::channel(); + assert!(Message::handle( + &mut overflow, + make_message(1, closed_response), + )); + drop(closed_receiver); + + let (open_response, _open_receiver) = oneshot::channel(); + assert!(Message::handle( + &mut overflow, + make_message(2, open_response), + )); + + let mut messages = Vec::new(); + Overflow::drain(&mut overflow, |message| { + messages.push(message); + None + }); + + assert_eq!(messages.len(), 1); + assert!(matches!( + messages.pop(), + Some(Message::Deliver { + delivery: Delivery { + key: Key::Finalized { height: 2 }, + .. + }, + .. + }) + )); + } + #[test] fn test_subject_block_encoding() { let commitment = Sha256::hash(b"test"); - let request = Request::::Block(commitment); + let request = Key::::Block(commitment); // Test encoding let encoded = request.encode(); @@ -263,31 +610,31 @@ mod tests { // Test decoding let mut buf = encoded.as_ref(); - let decoded = Request::::read(&mut buf).unwrap(); + let decoded = Key::::read(&mut buf).unwrap(); assert_eq!(request, decoded); - assert_eq!(decoded, Request::Block(commitment)); + assert_eq!(decoded, Key::Block(commitment)); } #[test] fn test_subject_finalized_encoding() { - let height = 12345u64; - let request = Request::::Finalized { height }; + let request = Key::::Finalized { height: 12345u64 }; - // Test encoding + // Test encoding: fixed-width u64 height on the wire. let encoded = request.encode(); + assert_eq!(encoded.len(), 9); // 1 byte for enum variant + 8 bytes for height assert_eq!(encoded[0], 1); // Finalized variant // Test decoding let mut buf = encoded.as_ref(); - let decoded = Request::::read(&mut buf).unwrap(); + let decoded = Key::::read(&mut buf).unwrap(); assert_eq!(request, decoded); - assert_eq!(decoded, Request::Finalized { height }); + assert_eq!(decoded, Key::Finalized { height: 12345 }); } #[test] fn test_subject_notarized_encoding() { let round = Round::new(Epoch::new(67890), View::new(12345)); - let request = Request::::Notarized { round }; + let request = Key::::Notarized { round }; // Test encoding let encoded = request.encode(); @@ -295,18 +642,28 @@ mod tests { // Test decoding let mut buf = encoded.as_ref(); - let decoded = Request::::read(&mut buf).unwrap(); + let decoded = Key::::read(&mut buf).unwrap(); assert_eq!(request, decoded); - assert_eq!(decoded, Request::Notarized { round }); + assert_eq!(decoded, Key::Notarized { round }); + } + + #[test] + fn test_subject_decode_rejects_invalid_enum_tag() { + let bad = [3u8]; + let mut buf = bad.as_ref(); + assert!(matches!( + Key::::read(&mut buf), + Err(CodecError::InvalidEnum(3)) + )); } #[test] fn test_subject_hash() { use std::collections::HashSet; - let r1 = Request::::Finalized { height: 100 }; - let r2 = Request::::Finalized { height: 100 }; - let r3 = Request::::Finalized { height: 200 }; + let r1 = Key::::Finalized { height: 100 }; + let r2 = Key::::Finalized { height: 100 }; + let r3 = Key::::Finalized { height: 200 }; let mut set = HashSet::new(); set.insert(r1); @@ -315,27 +672,100 @@ mod tests { } #[test] - fn test_subject_predicate() { - let r1 = Request::::Finalized { height: 100 }; - let r2 = Request::::Finalized { height: 200 }; - let r3 = Request::::Notarized { + fn test_height_floor_predicate() { + let floor = Height::new(100); + let higher_finalized = Key::::Finalized { height: 200 }; + let notarized = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(150)), }; + let block = Key::::Block(Sha256::hash(b"block")); + let stale_finalized = Annotation::Finalized(Finalized::ByHeight { + height: Height::new(100), + }); + let fresh_certified = Annotation::Certified { + height: Height::new(101), + }; + let stale_certified = Annotation::Certified { + height: Height::new(100), + }; - let predicate = r1.predicate(); - assert!(predicate(&r2)); // r2.height > r1.height - assert!(predicate(&r3)); // Different variant (notarized) + let predicate = above_height_floor(floor); + assert!(predicate( + &higher_finalized, + &Annotation::Finalized(Finalized::ByHeight { + height: Height::new(200), + }) + )); + assert!(predicate( + ¬arized, + &Annotation::Notarization { + round: Round::new(Epoch::new(333), View::new(150)), + } + )); + assert!(predicate(&block, &fresh_certified)); + + let same_height = Key::::Finalized { height: 100 }; + assert!(!predicate( + &same_height, + &Annotation::Finalized(Finalized::ByHeight { + height: Height::new(100), + }) + )); + assert!(!predicate(&block, &stale_finalized)); + assert!(!predicate(&block, &stale_certified)); + } - let r1_same = Request::::Finalized { height: 100 }; - assert!(!predicate(&r1_same)); // Same height, should not pass + #[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 higher_notarized = Key::::Notarized { + round: Round::new(Epoch::new(1), View::new(11)), + }; + let same_notarized = Key::::Notarized { + round: Round::new(Epoch::new(1), View::new(10)), + }; + let finalized = Key::::Finalized { height: 100 }; + + let predicate = above_round_floor(floor); + assert!(predicate( + &higher_notarized, + &Annotation::Notarization { + round: Round::new(Epoch::new(1), View::new(11)), + } + )); + assert!(predicate( + &finalized, + &Annotation::Finalized(Finalized::ByHeight { + height: Height::new(100), + }) + )); + assert!(predicate( + &block, + &Annotation::Finalized(Finalized::ByRound { + round: Round::new(Epoch::new(1), View::new(11)), + }) + )); + assert!(!predicate( + &same_notarized, + &Annotation::Notarization { + round: Round::new(Epoch::new(1), View::new(10)), + } + )); + assert!(!predicate( + &block, + &Annotation::Finalized(Finalized::ByRound { + round: Round::new(Epoch::new(1), View::new(10)), + }) + )); } #[test] fn test_encode_size() { let commitment = Sha256::hash(&[0u8; 32]); - let r1 = Request::::Block(commitment); - let r2 = Request::::Finalized { height: u64::MAX }; - let r3 = Request::::Notarized { + let r1 = Key::::Block(commitment); + let r2 = Key::::Finalized { height: u64::MAX }; + let r3 = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(0)), }; @@ -350,8 +780,8 @@ mod tests { // Test ordering within the same variant let commitment1 = Sha256::hash(b"test1"); let commitment2 = Sha256::hash(b"test2"); - let block1 = Request::::Block(commitment1); - let block2 = Request::::Block(commitment2); + let block1 = Key::::Block(commitment1); + let block2 = Key::::Block(commitment2); // Block ordering depends on commitment ordering if commitment1 < commitment2 { @@ -363,22 +793,22 @@ mod tests { } // Finalized ordering by height - let fin1 = Request::::Finalized { height: 100 }; - let fin2 = Request::::Finalized { height: 200 }; - let fin3 = Request::::Finalized { height: 200 }; + let fin1 = Key::::Finalized { height: 100 }; + let fin2 = Key::::Finalized { height: 200 }; + let fin3 = Key::::Finalized { height: 200 }; assert!(fin1 < fin2); assert!(fin2 > fin1); assert_eq!(fin2.cmp(&fin3), std::cmp::Ordering::Equal); // Notarized ordering by view - let not1 = Request::::Notarized { + let not1 = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(50)), }; - let not2 = Request::::Notarized { + let not2 = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(150)), }; - let not3 = Request::::Notarized { + let not3 = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(150)), }; @@ -390,9 +820,9 @@ mod tests { #[test] fn test_request_ord_cross_variant() { let commitment = Sha256::hash(b"test"); - let block = Request::::Block(commitment); - let finalized = Request::::Finalized { height: 100 }; - let notarized = Request::::Notarized { + let block = Key::::Block(commitment); + let finalized = Key::::Finalized { height: 100 }; + let notarized = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(200)), }; @@ -414,37 +844,6 @@ mod tests { assert_eq!(notarized.cmp(&finalized), std::cmp::Ordering::Greater); } - #[test] - fn test_request_partial_ord() { - let commitment1 = Sha256::hash(b"test1"); - let commitment2 = Sha256::hash(b"test2"); - let block1 = Request::::Block(commitment1); - let block2 = Request::::Block(commitment2); - let finalized = Request::::Finalized { height: 100 }; - let notarized = Request::::Notarized { - round: Round::new(Epoch::new(333), View::new(200)), - }; - - // PartialOrd should always return Some - assert!(block1.partial_cmp(&block2).is_some()); - assert!(block1.partial_cmp(&finalized).is_some()); - assert!(finalized.partial_cmp(¬arized).is_some()); - - // Verify consistency with Ord - assert_eq!( - block1.partial_cmp(&finalized), - Some(std::cmp::Ordering::Less) - ); - assert_eq!( - finalized.partial_cmp(¬arized), - Some(std::cmp::Ordering::Less) - ); - assert_eq!( - notarized.partial_cmp(&block1), - Some(std::cmp::Ordering::Greater) - ); - } - #[test] fn test_request_ord_sorting() { let commitment1 = Sha256::hash(b"a"); @@ -452,17 +851,17 @@ mod tests { let commitment3 = Sha256::hash(b"c"); let requests = vec![ - Request::::Notarized { + Key::::Notarized { round: Round::new(Epoch::new(333), View::new(300)), }, - Request::::Block(commitment2), - Request::::Finalized { height: 200 }, - Request::::Block(commitment1), - Request::::Notarized { + Key::::Block(commitment2), + Key::::Finalized { height: 200 }, + Key::::Block(commitment1), + Key::::Notarized { round: Round::new(Epoch::new(333), View::new(250)), }, - Request::::Finalized { height: 100 }, - Request::::Block(commitment3), + Key::::Finalized { height: 100 }, + Key::::Block(commitment3), ]; // Sort using BTreeSet (uses Ord) @@ -476,24 +875,24 @@ mod tests { assert_eq!(sorted.len(), 7); // Check that all blocks come first - assert!(matches!(sorted[0], Request::::Block(_))); - assert!(matches!(sorted[1], Request::::Block(_))); - assert!(matches!(sorted[2], Request::::Block(_))); + assert!(matches!(sorted[0], Key::::Block(_))); + assert!(matches!(sorted[1], Key::::Block(_))); + assert!(matches!(sorted[2], Key::::Block(_))); // Check that finalized come next - assert_eq!(sorted[3], Request::::Finalized { height: 100 }); - assert_eq!(sorted[4], Request::::Finalized { height: 200 }); + assert_eq!(sorted[3], Key::::Finalized { height: 100 }); + assert_eq!(sorted[4], Key::::Finalized { height: 200 }); // Check that notarized come last assert_eq!( sorted[5], - Request::::Notarized { + Key::::Notarized { round: Round::new(Epoch::new(333), View::new(250)) } ); assert_eq!( sorted[6], - Request::::Notarized { + Key::::Notarized { round: Round::new(Epoch::new(333), View::new(300)) } ); @@ -502,12 +901,12 @@ mod tests { #[test] fn test_request_ord_edge_cases() { // Test with extreme values - let min_finalized = Request::::Finalized { height: 0 }; - let max_finalized = Request::::Finalized { height: u64::MAX }; - let min_notarized = Request::::Notarized { + let min_finalized = Key::::Finalized { height: 0 }; + let max_finalized = Key::::Finalized { height: u64::MAX }; + let min_notarized = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(0)), }; - let max_notarized = Request::::Notarized { + let max_notarized = Key::::Notarized { round: Round::new(Epoch::new(333), View::new(u64::MAX)), }; @@ -517,7 +916,7 @@ mod tests { // Test self-comparison let commitment = Sha256::hash(b"self"); - let block = Request::::Block(commitment); + 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); assert_eq!(max_notarized.cmp(&max_notarized), std::cmp::Ordering::Equal); diff --git a/syncer/src/ingress/mailbox.rs b/syncer/src/ingress/mailbox.rs index 57e724f8..e9a495e2 100644 --- a/syncer/src/ingress/mailbox.rs +++ b/syncer/src/ingress/mailbox.rs @@ -1,3 +1,8 @@ +use crate::durability::Durable as _; +use commonware_actor::{ + Feedback, + mailbox::{Overflow, Policy, Sender}, +}; use commonware_consensus::{ Block, Reporter, simplex::scheme::Scheme, @@ -5,11 +10,10 @@ use commonware_consensus::{ types::{Height, Round}, }; use commonware_cryptography::Digest; +use commonware_p2p::Recipients; +use commonware_runtime::Handle; use commonware_storage::archive; -use commonware_utils::{ - channel::{mpsc, oneshot}, - vec::NonEmptyVec, -}; +use commonware_utils::{channel::oneshot, vec::NonEmptyVec}; use futures::{ FutureExt, future::BoxFuture, @@ -17,10 +21,10 @@ use futures::{ }; use pin_project::pin_project; use std::{ + collections::{BTreeMap, VecDeque, btree_map::Entry}, pin::Pin, task::{Context, Poll}, }; -use tracing::{error, warn}; /// An identifier for a block request. pub enum Identifier { @@ -64,7 +68,7 @@ impl From> for Identifier { } } -/// Messages sent to the marshal [Actor](super::super::actor::Actor). +/// Messages sent to the marshal [Actor](crate::actor::Actor). /// /// These messages are sent from the consensus engine and other parts of the /// system to drive the state of the marshal. @@ -95,6 +99,11 @@ pub(crate) enum Message, B: Block> { /// A channel to send the retrieved finalization. response: oneshot::Sender>>, }, + /// A request to retrieve the latest processed height. + GetProcessedHeight { + /// A channel to send the latest processed height. + response: oneshot::Sender>, + }, /// A hint to fetch a finalization from the network if not available locally. /// /// This is fire-and-forget: the finalization will be stored in syncer and delivered @@ -102,7 +111,7 @@ pub(crate) enum Message, B: Block> { HintFinalized { /// The height of the finalization to fetch. height: Height, - /// Target peers to fetch from. + /// Target peers to fetch from. Added to any existing targets for this height. targets: NonEmptyVec, }, /// A request to retrieve a block by its commitment. @@ -115,21 +124,40 @@ pub(crate) enum Message, B: Block> { /// A channel to send the retrieved block. response: oneshot::Sender, }, + /// A hint to fetch a notarized block by round without adding another local subscriber. + /// + /// `commitment` is used as a locality check: if the block is already + /// available locally, the fetch is skipped. + HintNotarized { + /// The notarized round to request. + round: Round, + /// The commitment used to short-circuit if the block is already local. + commitment: B::Digest, + }, + /// A request to retrieve the verified block previously persisted for `round`. + GetVerified { + /// The round to query. + round: Round, + /// A channel to send the retrieved block, if any. + response: oneshot::Sender>, + }, /// A request to broadcast a proposed block to all peers. Proposed { /// The round in which the block was proposed. round: Round, /// The block to broadcast. block: B, + /// A channel sent once the block sync has started. + ack: oneshot::Sender>, }, - /// A request to forward a block to a set of peers. + /// A request to forward a block to a set of recipients. Forward { /// The round in which the block was proposed. round: Round, /// The commitment of the block to forward. commitment: B::Digest, - /// The peers to forward the block to. - peers: Vec, + /// The recipients to forward the block to. + recipients: Recipients, }, /// A notification that a block has been verified by the application. Verified { @@ -137,6 +165,17 @@ pub(crate) enum Message, B: Block> { round: Round, /// The verified block. block: B, + /// A channel sent once the block sync has started. + ack: oneshot::Sender>, + }, + /// A notification that a block has been certified by the application. + Certified { + /// The round in which the block was certified. + round: Round, + /// The certified block. + block: B, + /// A channel sent once the block and notarization syncs have started. + ack: oneshot::Sender>, }, // -------------------- Consensus Engine Messages -------------------- @@ -150,17 +189,17 @@ pub(crate) enum Message, B: Block> { /// The finalization. finalization: Finalization, }, - /// Sets the sync starting point (advances if higher than current). - /// - /// Marshal will sync and deliver blocks starting at `floor + 1`. Data below - /// the floor is pruned. + /// Attempts to set the sync starting point from a finalized commitment. /// - /// To prune data without affecting the sync starting point, use [Message::Prune] instead. + /// 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. /// - /// The default floor is 0. + /// To prune data without changing the sync starting point, use + /// [Message::Prune] instead. SetFloor { - /// The candidate floor height. - height: Height, + /// The candidate floor finalization, verified by the actor before use. + finalization: Finalization, }, /// Prunes finalized blocks and certificates below the given height. /// @@ -173,15 +212,302 @@ pub(crate) enum Message, B: Block> { }, } -/// A mailbox for sending messages to the marshal [Actor](super::super::actor::Actor). +impl, B: Block> Message { + fn stale(&self, current: Option) -> bool { + match self { + // Height-targeted reads below the floor can never be served + Self::GetInfo { + identifier: Identifier::Height(height), + .. + } + | Self::GetBlock { + identifier: Identifier::Height(height), + .. + } + | Self::GetFinalization { height, .. } => Some(*height) < current, + // Hints only inform the actor about heights strictly above the floor + Self::HintFinalized { height, .. } => Some(*height) <= current, + // Durability acks cannot be dropped: callers depend on them + Self::Proposed { .. } | Self::Verified { .. } | Self::Certified { .. } => false, + // Digest and latest lookups are not bound to a specific height + Self::GetBlock { + identifier: Identifier::Digest(_) | Identifier::Latest, + .. + } + | Self::GetInfo { + identifier: Identifier::Digest(_) | Identifier::Latest, + .. + } + | Self::GetProcessedHeight { .. } => false, + Self::HintNotarized { .. } => false, + Self::Subscribe { .. } + | Self::GetVerified { .. } + | Self::Forward { .. } + | Self::SetFloor { .. } + | Self::Prune { .. } + | Self::Notarization { .. } + | Self::Finalization { .. } => false, + } + } + + pub(crate) fn response_closed(&self) -> bool { + match self { + Self::GetInfo { response, .. } => response.is_closed(), + Self::GetBlock { response, .. } | Self::GetVerified { response, .. } => { + response.is_closed() + } + Self::GetFinalization { response, .. } => response.is_closed(), + Self::GetProcessedHeight { response } => response.is_closed(), + Self::Subscribe { response, .. } => response.is_closed(), + Self::HintNotarized { .. } => false, + Self::HintFinalized { .. } + | Self::Forward { .. } + | Self::Proposed { .. } + | Self::Verified { .. } + | Self::Certified { .. } + | Self::SetFloor { .. } + | Self::Prune { .. } + | Self::Notarization { .. } + | Self::Finalization { .. } => false, + } + } +} + +/// Overflow state for syncer mailbox messages retained after the mailbox fills. +/// +/// Advisory inputs are coalesced instead of queued unboundedly: finalized +/// hints keep one entry per height with a unioned target set, floors collapse +/// to the highest round seen, and prunes collapse to the highest height seen. +/// This keeps callers running control loops (e.g. the orchestrator) from ever +/// parking on a full syncer mailbox. +pub(crate) struct Pending, B: Block> { + floor: Option>, + prune: Option, + hints: BTreeMap>, + messages: VecDeque>, +} + +enum PendingMessage, B: Block> { + Message(Message), + HintFinalized(Height), +} + +impl, B: Block> Default for Pending { + fn default() -> Self { + Self { + floor: None, + prune: None, + hints: BTreeMap::new(), + messages: VecDeque::new(), + } + } +} + +impl, B: Block> Pending { + // Only prune advances are usable for height staleness checks. A pending + // floor finalization does not carry the block height until the block is decoded. + const fn height(&self) -> Option { + self.prune + } + + fn retain(&mut self) { + let current = self.height(); + self.hints.retain(|height, _| Some(*height) > current); + + let hints = &self.hints; + self.messages.retain(|message| match message { + PendingMessage::Message(message) => { + !message.response_closed() && !message.stale(current) + } + PendingMessage::HintFinalized(height) => hints.contains_key(height), + }); + } + + fn set_floor(&mut self, finalization: Finalization) { + let round = finalization.round(); + if self + .floor + .as_ref() + .is_some_and(|floor| floor.round() >= round) + { + return; + } + + self.floor = Some(finalization); + } + + fn prune(&mut self, height: Height) { + let current = self.height(); + let prune = Some(height); + if self.prune >= prune { + return; + } + + self.prune = self.prune.max(prune); + if self.height() > current { + self.retain(); + } + } + + fn extend_hint_targets( + pending: &mut NonEmptyVec, + targets: NonEmptyVec, + ) { + for target in targets { + if !pending.contains(&target) { + pending.push(target); + } + } + } + + fn hint_finalized(&mut self, height: Height, targets: NonEmptyVec) { + // The finalized height is already covered by the floor or prune point. + let current = self.height(); + if current.is_some_and(|current| height <= current) { + return; + } + + match self.hints.entry(height) { + Entry::Vacant(entry) => { + entry.insert(targets); + self.messages + .push_back(PendingMessage::HintFinalized(height)); + } + Entry::Occupied(mut entry) => { + Self::extend_hint_targets(entry.get_mut(), targets); + } + } + } + + fn restore_hint(&mut self, height: Height, targets: NonEmptyVec) { + match self.hints.entry(height) { + Entry::Vacant(entry) => { + entry.insert(targets); + } + Entry::Occupied(mut entry) => { + Self::extend_hint_targets(entry.get_mut(), targets); + } + } + self.messages + .push_front(PendingMessage::HintFinalized(height)); + } + + fn drain_one(&mut self, message: Message, push: &mut F) -> bool + where + F: FnMut(Message) -> Option>, + { + // Receiver accepted; the message is consumed + let Some(message) = push(message) else { + return true; + }; + + // Receiver rejected; restore so the next drain retries from the same point + match message { + Message::SetFloor { finalization } => self.set_floor(finalization), + Message::Prune { height } => self.prune(height), + Message::HintFinalized { height, targets } => self.restore_hint(height, targets), + message => self.messages.push_front(PendingMessage::Message(message)), + } + false + } +} + +impl, B: Block> Overflow> for Pending { + fn is_empty(&self) -> bool { + self.floor.is_none() + && self.prune.is_none() + && self.hints.is_empty() + && self.messages.is_empty() + } + + fn drain(&mut self, mut push: F) + where + F: FnMut(Message) -> Option>, + { + // Drain floor and prune first so the actor advances its floor before + // it sees the height-bounded reads that follow + if let Some(finalization) = self.floor.take() + && !self.drain_one(Message::SetFloor { finalization }, &mut push) + { + return; + } + if let Some(height) = self.prune.take() + && !self.drain_one(Message::Prune { height }, &mut push) + { + return; + } + + // Drain the remaining queued messages in FIFO order + while let Some(pending) = self.messages.pop_front() { + match pending { + PendingMessage::Message(message) => { + if message.response_closed() { + continue; + } + if !self.drain_one(message, &mut push) { + break; + } + } + PendingMessage::HintFinalized(hint_height) => { + let Some(targets) = self.hints.remove(&hint_height) else { + continue; + }; + let message = Message::HintFinalized { + height: hint_height, + targets, + }; + if !self.drain_one(message, &mut push) { + break; + } + } + } + } + } +} + +impl, B: Block> Policy for Message { + type Overflow = Pending; + + fn handle(overflow: &mut Self::Overflow, message: Self) { + // A closed responder cannot be served + if message.response_closed() { + return; + } + match message { + // Coalesce hints: a single entry per height with a unioned target set + Self::HintFinalized { height, targets } => { + overflow.hint_finalized(height, targets); + } + // Floors collapse to the highest round seen; prune collapses to + // the highest height seen. + Self::SetFloor { finalization } => { + overflow.set_floor(finalization); + } + Self::Prune { height } => { + overflow.prune(height); + } + // Queue if the new message is still useful + message => { + if message.stale(overflow.height()) { + return; + } + overflow + .messages + .push_back(PendingMessage::Message(message)); + } + } + } +} + +/// A mailbox for sending messages to the marshal [Actor](crate::actor::Actor). #[derive(Clone)] pub struct Mailbox, B: Block> { - sender: mpsc::Sender>, + sender: Sender>, } impl, B: Block> Mailbox { /// Creates a new mailbox. - pub(crate) const fn new(sender: mpsc::Sender>) -> Self { + pub(crate) const fn new(sender: Sender>) -> Self { Self { sender } } @@ -190,103 +516,59 @@ impl, B: Block> Mailbox { &mut self, identifier: impl Into>, ) -> Option<(Height, B::Digest)> { - let (tx, rx) = oneshot::channel(); - if self - .sender - .send(Message::GetInfo { - identifier: identifier.into(), - response: tx, - }) - .await - .is_err() - { - error!("failed to send get info message to actor: receiver dropped"); - } - rx.await.unwrap_or_else(|_| { - error!("failed to get block info: receiver dropped"); - None - }) + let (response, receiver) = oneshot::channel(); + let _ = self.sender.enqueue(Message::GetInfo { + identifier: identifier.into(), + response, + }); + receiver.await.ok().flatten() } /// A best-effort attempt to retrieve a given block from local /// storage. It is not an indication to go fetch the block from the network. pub async fn get_block(&mut self, identifier: impl Into>) -> Option { - let (tx, rx) = oneshot::channel(); - if self - .sender - .send(Message::GetBlock { - identifier: identifier.into(), - response: tx, - }) - .await - .is_err() - { - error!("failed to send get block message to actor: receiver dropped"); - } - rx.await.unwrap_or_else(|_| { - error!("failed to get block: receiver dropped"); - None - }) + let (response, receiver) = oneshot::channel(); + let _ = self.sender.enqueue(Message::GetBlock { + identifier: identifier.into(), + response, + }); + receiver.await.ok().flatten() } /// A best-effort attempt to retrieve a given [Finalization] from local /// storage. It is not an indication to go fetch the [Finalization] from the network. pub async fn get_finalization(&mut self, height: Height) -> Option> { - let (tx, rx) = oneshot::channel(); - if self + let (response, receiver) = oneshot::channel(); + let _ = self .sender - .send(Message::GetFinalization { - height, - response: tx, - }) - .await - .is_err() - { - error!("failed to send get finalization message to actor: receiver dropped"); - } - rx.await.unwrap_or_else(|_| { - error!("failed to get finalization: receiver dropped"); - None - }) + .enqueue(Message::GetFinalization { height, response }); + receiver.await.ok().flatten() + } + + /// Retrieve the latest processed height. + pub async fn get_processed_height(&self) -> Option { + let (response, receiver) = oneshot::channel(); + let _ = self + .sender + .enqueue(Message::GetProcessedHeight { response }); + receiver.await.ok().flatten() } /// Hints that a finalization should be fetched from the network if not available locally. /// /// This is fire-and-forget: the finalization will be stored in syncer and delivered /// via the normal finalization flow when available. - pub async fn hint_finalized(&mut self, height: Height, targets: NonEmptyVec) { - if self - .sender - .send(Message::HintFinalized { height, targets }) - .await - .is_err() - { - error!("failed to send hint finalized message to actor: receiver dropped"); - } - } - - /// Non-blocking variant of [`hint_finalized`](Self::hint_finalized). /// /// The hint is advisory catch-up input, so callers running a control loop /// that must stay responsive (the orchestrator processes epoch Enter/Exit on - /// the same loop) must not park on a full syncer mailbox. When the mailbox is - /// full we drop the hint instead of awaiting capacity: the peer re-advertises - /// the later epoch and the finalization also arrives through the normal flow, - /// so dropping under backpressure only delays catch up, it does not lose - /// correctness. - pub fn try_hint_finalized(&mut self, height: Height, targets: NonEmptyVec) { - match self + /// the same loop) must not park on a full syncer mailbox. Enqueueing is + /// non-blocking: when the mailbox is full, hints are coalesced per height + /// (with unioned target sets) in the overflow state instead of blocking or + /// being lost. + pub fn hint_finalized(&mut self, height: Height, targets: NonEmptyVec) { + let _ = self .sender - .try_send(Message::HintFinalized { height, targets }) - { - Ok(()) => {} - Err(mpsc::error::TrySendError::Full(_)) => { - warn!("syncer mailbox full, dropping advisory finalized hint"); - } - Err(mpsc::error::TrySendError::Closed(_)) => { - error!("failed to send hint finalized message to actor: receiver dropped"); - } - } + .enqueue(Message::HintFinalized { height, targets }); } /// A request to retrieve a block by its commitment. @@ -298,25 +580,43 @@ impl, B: Block> Mailbox { /// it may never become available. /// /// The oneshot receiver should be dropped to cancel the subscription. - pub async fn subscribe( + pub fn subscribe( &mut self, round: Option, commitment: B::Digest, ) -> oneshot::Receiver { - let (tx, rx) = oneshot::channel(); - if self + let (response, receiver) = oneshot::channel(); + let _ = self.sender.enqueue(Message::Subscribe { + round, + commitment, + response, + }); + receiver + } + + /// Hint that peers may have the block notarized at `round`. + /// + /// This issues a round-bound resolver request without registering a new + /// block subscriber. The `commitment` is only used to skip the request when + /// the block is already available locally. + pub fn hint_notarized(&self, round: Round, commitment: B::Digest) { + let _ = self .sender - .send(Message::Subscribe { - round, - commitment, - response: tx, - }) - .await - .is_err() - { - error!("failed to send subscribe message to actor: receiver dropped"); - } - rx + .enqueue(Message::HintNotarized { round, commitment }); + } + + /// Returns the verified block previously persisted for `round`, if any. + /// + /// Multiple candidates can exist for one round when an equivocating leader + /// stores one before a crash and another after. This returns the first stored + /// candidate. Callers must not assume it is the most recently verified block: + /// check its digest and context before reuse, or look up the expected digest. + pub async fn get_verified(&self, round: Round) -> Option { + let (response, receiver) = oneshot::channel(); + let _ = self + .sender + .enqueue(Message::GetVerified { round, response }); + receiver.await.ok().flatten() } /// Returns an [AncestorStream] over the ancestry of a given block, leading up to genesis. @@ -327,69 +627,78 @@ impl, B: Block> Mailbox { (start_round, start_commitment): (Option, B::Digest), ) -> Option> { self.subscribe(start_round, start_commitment) - .await .await .ok() .map(|block| AncestorStream::new(self.clone(), [block])) } - /// Proposed requests that a proposed block is sent to all peers. - pub async fn proposed(&mut self, round: Round, block: B) { - if self - .sender - .send(Message::Proposed { round, block }) - .await - .is_err() - { - error!("failed to send proposed message to actor: receiver dropped"); - } + /// Requests that a proposed block is sent to all peers and persisted. + /// + /// The actor broadcasts before starting persistence, then returns the sync + /// handle so durability is awaited on this caller's task rather than the + /// actor task. + #[must_use = "callers must consider block durability before proceeding"] + pub async fn proposed(&mut self, round: Round, block: B) -> bool { + let (ack, receiver) = oneshot::channel(); + let _ = self.sender.enqueue(Message::Proposed { round, block, ack }); + let Ok(handle) = receiver.await else { + return false; + }; + handle.durable(round, "verified").await } - /// Forward a block to a set of peers. - pub async fn forward(&self, round: Round, commitment: B::Digest, peers: Vec) { - if self - .sender - .send(Message::Forward { - round, - commitment, - peers, - }) - .await - .is_err() - { - error!("failed to send forward message to actor: receiver dropped"); - } + /// Forward a block to a set of recipients. + pub fn forward( + &self, + round: Round, + commitment: B::Digest, + recipients: Recipients, + ) -> Feedback { + self.sender.enqueue(Message::Forward { + round, + commitment, + recipients, + }) } /// Notifies the actor that a block has been verified. - pub async fn verified(&mut self, round: Round, block: B) { - if self - .sender - .send(Message::Verified { round, block }) - .await - .is_err() - { - error!("failed to send verified message to actor: receiver dropped"); - } + /// + /// Returns after the block is durably stored. + #[must_use = "callers must consider block durability before proceeding"] + pub async fn verified(&mut self, round: Round, block: B) -> bool { + let (ack, receiver) = oneshot::channel(); + let _ = self.sender.enqueue(Message::Verified { round, block, ack }); + let Ok(handle) = receiver.await else { + return false; + }; + handle.durable(round, "verified").await } - /// Sets the sync starting point (conditionally advances if higher). + /// Notifies the actor that a block has been certified. /// - /// Marshal will sync and deliver blocks starting at `floor + 1`. Data below - /// the floor is pruned. + /// Returns after the block is durably stored. + #[must_use = "callers must consider block durability before proceeding"] + pub async fn certified(&mut self, round: Round, block: B) -> bool { + let (ack, receiver) = oneshot::channel(); + let _ = self + .sender + .enqueue(Message::Certified { round, block, ack }); + let Ok(handle) = receiver.await else { + return false; + }; + handle.durable(round, "certified").await + } + + /// Attempts to set the sync starting point from a finalized commitment. /// - /// To prune data without affecting the sync starting point, use [`Self::prune`] instead. + /// 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. /// - /// The default floor is 0. - pub async fn set_floor(&mut self, height: Height) { - if self - .sender - .send(Message::SetFloor { height }) - .await - .is_err() - { - error!("failed to send set sync floor message to actor: receiver dropped"); - } + /// To prune data without changing the sync starting point, use + /// [`Self::prune`] instead. + pub fn set_floor(&mut self, finalization: Finalization) { + let _ = self.sender.enqueue(Message::SetFloor { finalization }); } /// Prunes finalized blocks and certificates below the given height. @@ -397,43 +706,29 @@ 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. - pub async fn prune(&mut self, height: Height) { - if self.sender.send(Message::Prune { height }).await.is_err() { - error!("failed to send prune message to actor: receiver dropped"); - } + pub fn prune(&mut self, height: Height) { + let _ = self.sender.enqueue(Message::Prune { height }); } /// Notifies the actor of a verified [`Finalization`]. /// /// This is a trusted call that injects a finalization directly into marshal. The /// finalization is expected to have already been verified by the caller. - pub async fn finalization(&mut self, finalization: Finalization) { - if self - .sender - .send(Message::Finalization { finalization }) - .await - .is_err() - { - error!("failed to send finalization message to actor: receiver dropped"); - } + pub fn finalization(&mut self, finalization: Finalization) { + let _ = self.sender.enqueue(Message::Finalization { finalization }); } } impl, B: Block> Reporter for Mailbox { type Activity = Activity; - async fn report(&mut self, activity: Self::Activity) { + fn report(&mut self, activity: Self::Activity) -> Feedback { let message = match activity { Activity::Notarization(notarization) => Message::Notarization { notarization }, Activity::Finalization(finalization) => Message::Finalization { finalization }, - _ => { - // Ignore other activity types - return; - } + _ => return Feedback::Ok, }; - if self.sender.send(message).await.is_err() { - error!("failed to report activity to actor: receiver dropped"); - } + self.sender.enqueue(message) } } @@ -444,7 +739,7 @@ fn subscribe_block_future, B: Block>( commitment: B::Digest, ) -> BoxFuture<'static, Option> { async move { - let receiver = marshal.subscribe(None, commitment).await; + let receiver = marshal.subscribe(None, commitment); receiver.await.ok() } .boxed() @@ -552,34 +847,108 @@ mod tests { type TestScheme = ed_scheme::Scheme; type TestBlock = MockBlock; + type TestMessage = Message; + type TestPending = Pending; - // The orchestrator drives try_hint_finalized on the - // same loop that processes epoch Enter/Exit, so it must never block on a - // full syncer mailbox. A full mailbox must drop the advisory hint and return - // synchronously rather than awaiting capacity. try_send/try_recv are - // non-async, so a hang here would itself be the regression. + fn target(seed: u64) -> ed25519::PublicKey { + ed25519::PrivateKey::random(&mut StdRng::seed_from_u64(seed)).public_key() + } + + // The orchestrator drives hint_finalized on the same loop that processes + // epoch Enter/Exit, so it must never block on a full syncer mailbox. The + // overflow policy coalesces hints per height (with unioned target sets) + // instead of blocking or dropping them silently. #[test] - fn try_hint_finalized_drops_when_mailbox_full() { - // capacity-1 mailbox: a single queued message saturates it. - let (tx, mut rx) = mpsc::channel::>(1); - let mut mailbox = Mailbox::::new(tx); - - let target = ed25519::PrivateKey::random(&mut StdRng::seed_from_u64(0)).public_key(); - - // first hint takes the only slot. - mailbox.try_hint_finalized(Height::new(1), NonEmptyVec::new(target.clone())); - // second hint hits a full mailbox: must return (not block) and drop. - mailbox.try_hint_finalized(Height::new(2), NonEmptyVec::new(target)); - - // exactly the first hint is enqueued; the second was dropped. - match rx.try_recv() { - Ok(Message::HintFinalized { height, .. }) => assert_eq!(height, Height::new(1)), - Ok(_) => panic!("expected a HintFinalized message"), - Err(_) => panic!("first hint should have been enqueued"), + fn hint_finalized_coalesces_in_overflow() { + let mut overflow = TestPending::default(); + + let first = target(0); + let second = target(1); + + // Two hints for the same height coalesce into one entry with a + // unioned target set. + TestMessage::handle( + &mut overflow, + Message::HintFinalized { + height: Height::new(1), + targets: NonEmptyVec::new(first.clone()), + }, + ); + TestMessage::handle( + &mut overflow, + Message::HintFinalized { + height: Height::new(1), + targets: NonEmptyVec::new(second.clone()), + }, + ); + + let mut drained = Vec::new(); + Overflow::drain(&mut overflow, |message| { + drained.push(message); + None + }); + + assert_eq!(drained.len(), 1); + match drained.pop() { + Some(Message::HintFinalized { height, targets }) => { + assert_eq!(height, Height::new(1)); + let targets: Vec<_> = targets.into_iter().collect(); + assert_eq!(targets, vec![first, second]); + } + _ => panic!("expected a coalesced HintFinalized message"), } - assert!( - rx.try_recv().is_err(), - "second hint should have been dropped on the full mailbox" + assert!(overflow.is_empty()); + } + + // Prune requests collapse to the highest height and staleness-check + // queued hints so a full mailbox cannot accumulate unbounded state. + #[test] + fn prune_collapses_and_drops_stale_hints() { + let mut overflow = TestPending::default(); + + TestMessage::handle( + &mut overflow, + Message::HintFinalized { + height: Height::new(1), + targets: NonEmptyVec::new(target(0)), + }, + ); + TestMessage::handle( + &mut overflow, + Message::HintFinalized { + height: Height::new(5), + targets: NonEmptyVec::new(target(1)), + }, ); + TestMessage::handle( + &mut overflow, + Message::Prune { + height: Height::new(2), + }, + ); + TestMessage::handle( + &mut overflow, + Message::Prune { + height: Height::new(3), + }, + ); + + let mut drained = Vec::new(); + Overflow::drain(&mut overflow, |message| { + drained.push(message); + None + }); + + // One collapsed prune (highest height) and only the still-useful hint. + assert_eq!(drained.len(), 2); + assert!(matches!( + drained[0], + Message::Prune { height } if height == Height::new(3) + )); + assert!(matches!( + drained[1], + Message::HintFinalized { height, .. } if height == Height::new(5) + )); + assert!(overflow.is_empty()); } } diff --git a/syncer/src/lib.rs b/syncer/src/lib.rs index 38413b44..801f16c4 100644 --- a/syncer/src/lib.rs +++ b/syncer/src/lib.rs @@ -62,18 +62,23 @@ //! - Uses [`broadcast::buffered`](`commonware_broadcast::buffered`) for broadcasting and receiving //! uncertified blocks from the network. +mod acks; pub mod actor; pub use actor::Actor; pub mod cache; pub mod config; pub use config::{Config, SyncCheckpoint, SyncStart}; +mod delivery; +mod durability; +mod floor; pub mod ingress; pub use ingress::mailbox::Mailbox; pub mod resolver; pub mod standard; pub use standard::Standard; +mod stream; pub mod variant; -pub use variant::{Buffer, IntoBlock, Variant}; +pub use variant::{Buffer, Variant}; use commonware_consensus::Block; use commonware_consensus::simplex::scheme::Scheme; @@ -84,7 +89,8 @@ use commonware_utils::{Acknowledgement, acknowledgement::Exact}; /// /// Finalized tips are reported as soon as known, whether or not we hold all blocks up to that height. /// Finalized blocks are reported to the application in monotonically increasing order (no gaps permitted). -/// Notarized blocks are sent without ordering guarantees to enable execution before finalization. +/// Notarized blocks are sent without ordering guarantees after the block and notarization are +/// durably stored, enabling execution before finalization without exposing non-durable data. #[derive(Clone, Debug)] pub enum Update, A: Acknowledgement = Exact> { /// A new finalized tip. @@ -100,8 +106,10 @@ pub enum Update, A: Acknowledgement = Exact> { FinalizedBlock((B, Option>), A), /// A notarized (but not yet finalized) block. /// - /// These blocks do not require acknowledgement and may arrive out of order. They enable proposers - /// to build on notarized blocks without waiting for finalization. + /// These blocks do not require acknowledgement and may arrive out of order. They are reported + /// only after the block and notarization are durable, and enable proposers to build on notarized + /// blocks without waiting for finalization. For a given block, this update is reported before + /// its [`Self::FinalizedBlock`] update. NotarizedBlock(B), } @@ -111,15 +119,25 @@ pub mod mocks; #[cfg(all(test, feature = "test-mocks"))] mod tests { use super::{ - actor, + actor, cache, config::{Config, SyncStart}, - mocks::{application::Application, block::Block}, + mocks::{ + application::{Application, RecordedUpdate}, + block::Block, + }, resolver::p2p as resolver, }; - use crate::ingress::mailbox::Identifier; + use crate::durability::Durable as _; + use crate::ingress::{ + handler::{self, Annotation, Finalized, Key}, + mailbox::Identifier, + }; use crate::mocks::fixtures::{Fixture, bls12381_threshold}; - use commonware_broadcast::buffered; + use commonware_actor::{Feedback, Unreliable, mailbox}; + use commonware_broadcast::{Broadcaster as _, buffered}; + use commonware_codec::Encode; use commonware_consensus::Reporter; + use commonware_consensus::marshal::store::{Blocks, Certificates}; use commonware_consensus::simplex::scheme::bls12381_threshold; use commonware_consensus::simplex::types::{ Activity, Finalization, Finalize, Notarization, Notarize, Proposal, @@ -130,25 +148,30 @@ mod tests { use commonware_cryptography::{ Digestible, Hasher as _, bls12381::primitives::variant::MinPk, - certificate::ConstantProvider, + certificate::{ConstantProvider, Verifier as _}, ed25519::PublicKey, sha256::{Digest as Sha256Digest, Sha256}, }; use commonware_macros::test_traced; use commonware_p2p::{ - Manager, + Manager, Recipients, simulated::{self, Link, Network, Oracle}, }; use commonware_parallel::Sequential; + use commonware_resolver::{Delivery, Fetch, Resolver, TargetedResolver}; use commonware_runtime::{ - Clock, Metrics, Quota, Runner, buffer::paged::CacheRef, deterministic, + Clock, Quota, Runner, Supervisor as _, buffer::paged::CacheRef, deterministic, + }; + use commonware_storage::{ + archive::{immutable, prunable}, + translator::EightCap, }; - use commonware_storage::archive::immutable; - use commonware_utils::{NZU64, NZUsize, ordered}; - use rand::{Rng, seq::SliceRandom}; + use commonware_utils::{NZU64, NZUsize, channel::oneshot, ordered, vec::NonEmptyVec}; + use rand::{RngExt as _, seq::SliceRandom}; use std::{ collections::BTreeMap, num::{NonZeroU16, NonZeroU32, NonZeroU64, NonZeroUsize}, + sync::{Arc, Mutex}, time::{Duration, Instant}, }; use tracing::info; @@ -180,6 +203,381 @@ mod tests { const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX); + struct PacedStore { + inner: T, + context: deterministic::Context, + pace: Duration, + fail_sync: bool, + } + + impl Blocks for PacedStore { + 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 sync(&mut self) -> Result<(), Self::Error> { + self.context.sleep(self.pace).await; + self.inner.sync().await + } + + async fn start_sync(&mut self) -> Result, Self::Error> { + let inner = self.inner.start_sync().await?; + let sleep = self.context.sleep(self.pace); + let fail_sync = self.fail_sync; + Ok(commonware_runtime::Handle::from_future(async move { + sleep.await; + inner.await?; + if fail_sync { + Err(commonware_runtime::Error::WriteFailed) + } else { + Ok(()) + } + })) + } + + async fn get( + &self, + id: commonware_storage::archive::Identifier<'_, ::Digest>, + ) -> Result, Self::Error> { + self.inner.get(id).await + } + + async fn prune(&mut self, min: Height) -> Result<(), Self::Error> { + self.inner.prune(min).await + } + + fn missing_items(&self, start: Height, max: usize) -> Vec { + self.inner.missing_items(start, max) + } + + fn next_gap(&self, value: Height) -> (Option, Option) { + self.inner.next_gap(value) + } + + fn last_index(&self) -> Option { + self.inner.last_index() + } + } + + impl Certificates for PacedStore { + type BlockDigest = T::BlockDigest; + type Commitment = T::Commitment; + type Scheme = T::Scheme; + type Error = T::Error; + + async fn put( + &mut self, + height: Height, + digest: Self::BlockDigest, + finalization: Finalization, + ) -> Result<(), Self::Error> { + self.inner.put(height, digest, finalization).await + } + + async fn sync(&mut self) -> Result<(), Self::Error> { + self.context.sleep(self.pace).await; + self.inner.sync().await + } + + async fn start_sync(&mut self) -> Result, Self::Error> { + let inner = self.inner.start_sync().await?; + let sleep = self.context.sleep(self.pace); + let fail_sync = self.fail_sync; + Ok(commonware_runtime::Handle::from_future(async move { + sleep.await; + inner.await?; + if fail_sync { + Err(commonware_runtime::Error::WriteFailed) + } else { + Ok(()) + } + })) + } + + async fn get( + &self, + id: commonware_storage::archive::Identifier<'_, Self::BlockDigest>, + ) -> Result>, Self::Error> { + self.inner.get(id).await + } + + async fn has(&self, height: Height) -> Result { + self.inner.has(height).await + } + + async fn prune(&mut self, min: Height) -> Result<(), Self::Error> { + self.inner.prune(min).await + } + + fn last_index(&self) -> Option { + self.inner.last_index() + } + + fn ranges_from(&self, from: Height) -> impl Iterator { + self.inner.ranges_from(from) + } + } + + #[derive(Clone, Copy, Default)] + enum FinalizedSyncFailure { + #[default] + None, + Blocks, + Finalizations, + } + + #[derive(Clone, Default)] + struct RecordingResolver { + fetches: Arc, Annotation>>>>, + active_fetches: Arc, Annotation>>>>, + targeted: Arc, NonEmptyVec)>>>, + sender: Option>>, + } + + impl RecordingResolver { + fn holding(metrics: impl commonware_runtime::Metrics) -> (handler::Receiver, Self) { + let (sender, receiver) = mailbox::new_unreliable(metrics, NZUsize!(100)); + ( + handler::Receiver::new(receiver), + Self { + sender: Some(sender), + ..Self::default() + }, + ) + } + + fn fetches(&self) -> Vec, Annotation>> { + self.fetches.lock().unwrap().clone() + } + + fn enqueue(&self, message: handler::Message) -> Unreliable { + self.sender + .as_ref() + .expect("recording resolver sender missing") + .enqueue(message) + } + } + + impl Resolver for RecordingResolver { + type Key = Key; + type Subscriber = Annotation; + + fn fetch(&mut self, fetch: F) -> Feedback + where + F: Into> + Send, + { + let fetch = fetch.into(); + self.fetches.lock().unwrap().push(fetch.clone()); + self.active_fetches.lock().unwrap().push(fetch); + Feedback::Ok + } + + fn fetch_all(&mut self, fetches: Vec) -> Feedback + where + F: Into> + Send, + { + for fetch in fetches { + let _ = self.fetch(fetch); + } + Feedback::Ok + } + + fn retain( + &mut self, + predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static, + ) -> Feedback { + self.active_fetches + .lock() + .unwrap() + .retain(|fetch| predicate(&fetch.key, &fetch.subscriber)); + Feedback::Ok + } + } + + impl TargetedResolver for RecordingResolver { + type PublicKey = K; + + fn fetch_targeted( + &mut self, + fetch: impl Into> + Send, + targets: NonEmptyVec, + ) -> Feedback { + self.targeted + .lock() + .unwrap() + .push((fetch.into().key, targets)); + Feedback::Ok + } + + fn fetch_all_targeted( + &mut self, + fetches: Vec<(F, NonEmptyVec)>, + ) -> Feedback + where + F: Into> + Send, + { + for (fetch, targets) in fetches { + let _ = self.fetch_targeted(fetch, targets); + } + Feedback::Ok + } + } + + #[allow(clippy::type_complexity)] + async fn paced_finalized_stores( + context: &deterministic::Context, + partition_prefix: &str, + pace: Duration, + failure: FinalizedSyncFailure, + ) -> ( + PacedStore>>, + PacedStore>, + ) { + let page_cache = CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE); + let finalizations_by_height = prunable::Archive::init( + context.child("paced_finalizations"), + prunable::Config { + translator: EightCap, + key_partition: format!("{partition_prefix}-fbh-key"), + key_page_cache: page_cache.clone(), + value_partition: format!("{partition_prefix}-fbh-value"), + compression: None, + codec_config: S::certificate_codec_config_unbounded(), + items_per_section: NZU64!(10), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + replay_buffer: NZUsize!(1024), + }, + ) + .await + .expect("failed to initialize paced finalizations archive"); + let finalized_blocks = prunable::Archive::init( + context.child("paced_blocks"), + prunable::Config { + translator: EightCap, + key_partition: format!("{partition_prefix}-fb-key"), + key_page_cache: page_cache, + value_partition: format!("{partition_prefix}-fb-value"), + compression: None, + codec_config: (), + items_per_section: NZU64!(10), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + replay_buffer: NZUsize!(1024), + }, + ) + .await + .expect("failed to initialize paced blocks archive"); + ( + PacedStore { + inner: finalizations_by_height, + context: context.child("finalizations_pacer"), + pace, + fail_sync: matches!(failure, FinalizedSyncFailure::Finalizations), + }, + PacedStore { + inner: finalized_blocks, + context: context.child("blocks_pacer"), + pace, + fail_sync: matches!(failure, FinalizedSyncFailure::Blocks), + }, + ) + } + + async fn wait_until( + context: &deterministic::Context, + timeout: Duration, + description: &str, + mut predicate: impl FnMut() -> bool, + ) { + let deadline = context.current() + timeout; + while !predicate() { + assert!( + context.current() < deadline, + "timed out waiting for {description}" + ); + context.sleep(Duration::from_millis(1)).await; + } + } + + async fn setup_paced_validator( + context: deterministic::Context, + oracle: &mut Oracle, + validator: K, + provider: P, + partition_prefix: &str, + pace: Duration, + max_pending_acks: NonZeroUsize, + failure: FinalizedSyncFailure, + ) -> ( + Application, + crate::ingress::mailbox::Mailbox, + buffered::Mailbox, + RecordingResolver, + commonware_runtime::Handle<()>, + ) { + let config = Config { + scheme_provider: provider, + 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, + block_codec_config: (), + partition_prefix: partition_prefix.to_string(), + 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, + }; + + let control = oracle.control(validator.clone()); + let (broadcast_engine, buffer) = buffered::Engine::new( + context.child("broadcast"), + buffered::Config { + public_key: validator, + mailbox_size: config.mailbox_size, + deque_size: 10, + priority: false, + codec_config: (), + peer_provider: oracle.manager(), + }, + ); + let network = control.register(2, TEST_QUOTA).await.unwrap(); + broadcast_engine.start(network); + + let (finalizations_by_height, finalized_blocks) = + paced_finalized_stores(&context, partition_prefix, pace, failure).await; + let (actor, mailbox) = actor::Actor::init( + context.child("actor"), + finalizations_by_height, + finalized_blocks, + config, + ) + .await; + let (resolver_rx, resolver) = RecordingResolver::holding(context.child("resolver")); + let application = Application::::default(); + let test_buffer = buffer.clone(); + let handle = actor.start( + application.clone(), + buffer, + (resolver_rx, resolver.clone()), + SyncStart { + height: 0, + epoch: 0, + view: 0, + }, + None, + ); + (application, mailbox, test_buffer, resolver, handle) + } + async fn setup_validator( context: deterministic::Context, oracle: &mut Oracle, @@ -193,7 +591,7 @@ mod tests { let config = Config { scheme_provider: provider, epocher: FixedEpocher::new(BLOCKS_PER_EPOCH), - mailbox_size: 100, + mailbox_size: NZUsize!(100), namespace: NAMESPACE.to_vec(), view_retention_timeout: ViewDelta::new(10), max_repair: NZUsize!(10), @@ -222,7 +620,7 @@ mod tests { priority_requests: false, priority_responses: false, }; - let resolver = resolver::init(&context, resolver_cfg, backfill); + let resolver = resolver::init(context.child("resolver"), resolver_cfg, backfill); // Create a buffered broadcast engine and get its mailbox let broadcast_config = buffered::Config { @@ -233,14 +631,15 @@ mod tests { codec_config: (), peer_provider: oracle.manager(), }; - let (broadcast_engine, buffer) = buffered::Engine::new(context.clone(), broadcast_config); + let (broadcast_engine, buffer) = + buffered::Engine::new(context.child("broadcast"), broadcast_config); let network = control.register(2, TEST_QUOTA).await.unwrap(); broadcast_engine.start(network); // Initialize finalizations by height let start = Instant::now(); let finalizations_by_height = immutable::Archive::init( - context.with_label("finalizations_by_height"), + context.child("finalizations_by_height"), immutable::Config { metadata_partition: format!( "{}-finalizations-by-height-metadata", @@ -283,7 +682,7 @@ mod tests { // Initialize finalized blocks let start = Instant::now(); let finalized_blocks = immutable::Archive::init( - context.with_label("finalized_blocks"), + context.child("finalized_blocks"), immutable::Config { metadata_partition: format!( "{}-finalized_blocks-metadata", @@ -320,13 +719,8 @@ mod tests { .expect("failed to initialize finalized blocks archive"); info!(elapsed = ?start.elapsed(), "restored finalized blocks archive"); - let (actor, mailbox) = actor::Actor::init( - context.clone(), - finalizations_by_height, - finalized_blocks, - config, - ) - .await; + let (actor, mailbox) = + actor::Actor::init(context, finalizations_by_height, finalized_blocks, config).await; let application = Application::::default(); // Start the application @@ -345,6 +739,130 @@ mod tests { (application, mailbox, Height::zero()) } + async fn setup_validator_with_prefix( + context: deterministic::Context, + oracle: &mut Oracle, + validator: K, + provider: P, + partition_prefix: &str, + ) -> ( + Application, + crate::ingress::mailbox::Mailbox, + commonware_runtime::Handle<()>, + ) { + let config = Config { + scheme_provider: provider, + 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: partition_prefix.to_string(), + 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, + }; + + let control = oracle.control(validator.clone()); + let backfill = control.register(1, TEST_QUOTA).await.unwrap(); + let resolver_cfg = resolver::Config { + public_key: validator.clone(), + provider: oracle.manager(), + blocker: control.clone(), + mailbox_size: config.mailbox_size, + initial: Duration::from_secs(1), + timeout: Duration::from_secs(2), + fetch_retry_timeout: Duration::from_millis(100), + priority_requests: false, + priority_responses: false, + }; + let resolver = resolver::init(context.child("resolver"), resolver_cfg, backfill); + + let (broadcast_engine, buffer) = buffered::Engine::new( + context.child("broadcast"), + buffered::Config { + public_key: validator, + mailbox_size: config.mailbox_size, + deque_size: 10, + priority: false, + codec_config: (), + peer_provider: oracle.manager(), + }, + ); + let network = control.register(2, TEST_QUOTA).await.unwrap(); + broadcast_engine.start(network); + + let finalizations_by_height = immutable::Archive::init( + context.child("finalizations_by_height"), + immutable::Config { + metadata_partition: format!("{partition_prefix}-finalizations-metadata"), + freezer_table_partition: format!("{partition_prefix}-finalizations-table"), + freezer_table_initial_size: 64, + freezer_table_resize_frequency: 10, + freezer_table_resize_chunk_size: 10, + freezer_key_partition: format!("{partition_prefix}-finalizations-key"), + freezer_key_page_cache: config.page_cache.clone(), + freezer_value_partition: format!("{partition_prefix}-finalizations-value"), + freezer_value_target_size: 1024, + freezer_value_compression: None, + ordinal_partition: format!("{partition_prefix}-finalizations-ordinal"), + items_per_section: NZU64!(10), + codec_config: (), + replay_buffer: config.replay_buffer, + freezer_key_write_buffer: config.key_write_buffer, + freezer_value_write_buffer: config.value_write_buffer, + ordinal_write_buffer: config.key_write_buffer, + }, + ) + .await + .expect("failed to initialize finalizations by height archive"); + let finalized_blocks = immutable::Archive::init( + context.child("finalized_blocks"), + immutable::Config { + metadata_partition: format!("{partition_prefix}-blocks-metadata"), + freezer_table_partition: format!("{partition_prefix}-blocks-table"), + freezer_table_initial_size: 64, + freezer_table_resize_frequency: 10, + freezer_table_resize_chunk_size: 10, + freezer_key_partition: format!("{partition_prefix}-blocks-key"), + freezer_key_page_cache: config.page_cache.clone(), + freezer_value_partition: format!("{partition_prefix}-blocks-value"), + freezer_value_target_size: 1024, + freezer_value_compression: None, + ordinal_partition: format!("{partition_prefix}-blocks-ordinal"), + items_per_section: NZU64!(10), + codec_config: (), + replay_buffer: config.replay_buffer, + freezer_key_write_buffer: config.key_write_buffer, + freezer_value_write_buffer: config.value_write_buffer, + ordinal_write_buffer: config.key_write_buffer, + }, + ) + .await + .expect("failed to initialize finalized blocks archive"); + + 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, + ); + (application, mailbox, handle) + } + fn make_finalization(proposal: Proposal, schemes: &[S], quorum: u32) -> Finalization { // Generate proposal signature let finalizes: Vec<_> = schemes @@ -374,7 +892,7 @@ mod tests { tracked_peer_sets: NonZeroUsize, ) -> Oracle { let (network, oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, @@ -432,7 +950,7 @@ mod tests { .with_timeout(Some(Duration::from_secs(300))), ); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(3)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(3)); let Fixture { participants, schemes, @@ -445,12 +963,10 @@ mod tests { // Register the initial peer set. let mut manager = oracle.manager(); - manager - .track(0, ordered::Set::try_from(participants.clone()).unwrap()) - .await; + let _ = manager.track(0, ordered::Set::try_from(participants.clone()).unwrap()); for (i, validator) in participants.iter().enumerate() { let (application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -490,8 +1006,8 @@ mod tests { // Broadcast block by one validator let actor_index: usize = (height.get() % (NUM_VALIDATORS as u64)) as usize; let mut actor = actors[actor_index].clone(); - actor.proposed(round, block.clone()).await; - actor.verified(round, block.clone()).await; + assert!(actor.proposed(round, block.clone()).await); + assert!(actor.verified(round, block.clone()).await); // Wait for the block to be broadcast, but due to jitter, we may or may not receive // the block before continuing. @@ -504,9 +1020,7 @@ mod tests { payload: block.digest(), }; let notarization = make_notarization(proposal.clone(), &schemes, QUORUM); - actor - .report(Activity::Notarization(notarization.clone())) - .await; + let _ = actor.report(Activity::Notarization(notarization.clone())); // Finalize block by all validators let fin = make_finalization(proposal, &schemes, QUORUM); @@ -515,10 +1029,10 @@ mod tests { // Otherwise, finalize randomly. if height == Height::new(NUM_BLOCKS) || height == bounds.last() - || context.gen_bool(0.2) + || context.random_bool(0.2) // 20% chance to finalize randomly { - actor.report(Activity::Finalization(fin.clone())).await; + let _ = actor.report(Activity::Finalization(fin.clone())); } } } @@ -559,7 +1073,7 @@ mod tests { fn test_subscribe_basic_block_delivery() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -569,7 +1083,7 @@ mod tests { let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate() { let (_application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -585,13 +1099,14 @@ mod tests { let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); - let subscription_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment) - .await; + let subscription_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment); - actor - .verified(Round::new(Epoch::new(0), View::new(1)), block.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(1)), block.clone()) + .await + ); let proposal = Proposal { round: Round::new(Epoch::new(0), View::new(1)), @@ -599,10 +1114,10 @@ mod tests { payload: commitment, }; let notarization = make_notarization(proposal.clone(), &schemes, QUORUM); - actor.report(Activity::Notarization(notarization)).await; + let _ = actor.report(Activity::Notarization(notarization)); let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); let received_block = subscription_rx.await.unwrap(); assert_eq!(received_block.digest(), block.digest()); @@ -614,7 +1129,7 @@ mod tests { fn test_subscribe_multiple_subscriptions() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -624,7 +1139,7 @@ mod tests { let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate() { let (_application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -642,22 +1157,23 @@ mod tests { let commitment1 = block1.digest(); let commitment2 = block2.digest(); - let sub1_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1) - .await; - let sub2_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(2))), commitment2) - .await; - let sub3_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1) - .await; - - actor - .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) - .await; - actor - .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) - .await; + let sub1_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1); + let sub2_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(2))), commitment2); + let sub3_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1); + + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) + .await + ); + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) + .await + ); for (view, block) in [(1u64, block1.clone()), (2u64, block2.clone())] { let proposal = Proposal { @@ -666,10 +1182,10 @@ mod tests { payload: block.digest(), }; let notarization = make_notarization(proposal.clone(), &schemes, QUORUM); - actor.report(Activity::Notarization(notarization)).await; + let _ = actor.report(Activity::Notarization(notarization)); let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); } let received1_sub1 = sub1_rx.await.unwrap(); @@ -689,7 +1205,7 @@ mod tests { fn test_subscribe_canceled_subscriptions() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -699,7 +1215,7 @@ mod tests { let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate() { let (_application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -717,21 +1233,23 @@ mod tests { let commitment1 = block1.digest(); let commitment2 = block2.digest(); - let sub1_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1) - .await; - let sub2_rx = actor - .subscribe(Some(Round::new(Epoch::new(0), View::new(2))), commitment2) - .await; + let sub1_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(1))), commitment1); + let sub2_rx = + actor.subscribe(Some(Round::new(Epoch::new(0), View::new(2))), commitment2); drop(sub1_rx); - actor - .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) - .await; - actor - .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) + .await + ); + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) + .await + ); for (view, block) in [(1u64, block1.clone()), (2u64, block2.clone())] { let proposal = Proposal { @@ -740,10 +1258,10 @@ mod tests { payload: block.digest(), }; let notarization = make_notarization(proposal.clone(), &schemes, QUORUM); - actor.report(Activity::Notarization(notarization)).await; + let _ = actor.report(Activity::Notarization(notarization)); let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); } let received2 = sub2_rx.await.unwrap(); @@ -756,7 +1274,7 @@ mod tests { fn test_subscribe_blocks_from_different_sources() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -764,14 +1282,12 @@ mod tests { } = bls12381_threshold::(&mut context, NUM_VALIDATORS); let mut manager = oracle.manager(); - manager - .track(0, ordered::Set::try_from(participants.clone()).unwrap()) - .await; + let _ = manager.track(0, ordered::Set::try_from(participants.clone()).unwrap()); let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate() { let (_application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -790,16 +1306,18 @@ mod tests { let block4 = B::new::(block3.digest(), Height::new(4), 4); let block5 = B::new::(block4.digest(), Height::new(5), 5); - let sub1_rx = actor.subscribe(None, block1.digest()).await; - let sub2_rx = actor.subscribe(None, block2.digest()).await; - let sub3_rx = actor.subscribe(None, block3.digest()).await; - let sub4_rx = actor.subscribe(None, block4.digest()).await; - let sub5_rx = actor.subscribe(None, block5.digest()).await; + let sub1_rx = actor.subscribe(None, block1.digest()); + let sub2_rx = actor.subscribe(None, block2.digest()); + let sub3_rx = actor.subscribe(None, block3.digest()); + let sub4_rx = actor.subscribe(None, block4.digest()); + let sub5_rx = actor.subscribe(None, block5.digest()); // Block1: Broadcasted by the actor - actor - .proposed(Round::new(Epoch::zero(), View::new(1)), block1.clone()) - .await; + assert!( + actor + .proposed(Round::new(Epoch::zero(), View::new(1)), block1.clone()) + .await + ); context.sleep(Duration::from_millis(20)).await; // Block1: delivered @@ -808,9 +1326,11 @@ mod tests { assert_eq!(received1.height, Height::new(1)); // Block2: Verified by the actor - actor - .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) + .await + ); // Block2: delivered let received2 = sub2_rx.await.unwrap(); @@ -824,10 +1344,12 @@ mod tests { payload: block3.digest(), }; let notarization3 = make_notarization(proposal3.clone(), &schemes, QUORUM); - actor.report(Activity::Notarization(notarization3)).await; - actor - .verified(Round::new(Epoch::new(0), View::new(3)), block3.clone()) - .await; + let _ = actor.report(Activity::Notarization(notarization3)); + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(3)), block3.clone()) + .await + ); // Block3: delivered let received3 = sub3_rx.await.unwrap(); @@ -844,10 +1366,12 @@ mod tests { &schemes, QUORUM, ); - actor.report(Activity::Finalization(finalization4)).await; - actor - .verified(Round::new(Epoch::new(0), View::new(4)), block4.clone()) - .await; + let _ = actor.report(Activity::Finalization(finalization4)); + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(4)), block4.clone()) + .await + ); // Block4: delivered let received4 = sub4_rx.await.unwrap(); @@ -856,9 +1380,11 @@ mod tests { // Block5: Broadcasted by a remote node (different actor) let remote_actor = &mut actors[1].clone(); - remote_actor - .proposed(Round::new(Epoch::zero(), View::new(5)), block5.clone()) - .await; + assert!( + remote_actor + .proposed(Round::new(Epoch::zero(), View::new(5)), block5.clone()) + .await + ); context.sleep(Duration::from_millis(20)).await; // Block5: delivered @@ -872,7 +1398,7 @@ mod tests { fn test_get_info_basic_queries_present_and_missing() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -882,7 +1408,7 @@ mod tests { // Single validator actor let me = participants[0].clone(); let (_application, mut actor, _processed_height) = setup_validator( - context.with_label("validator_0"), + context.child("validator").with_attribute("index", 0), &mut oracle, me, ConstantProvider::new(schemes[0].clone()), @@ -900,7 +1426,7 @@ mod tests { let block = B::new::(parent, Height::new(1), 1); let digest = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); - actor.verified(round, block.clone()).await; + assert!(actor.verified(round, block.clone()).await); let proposal = Proposal { round, @@ -908,7 +1434,7 @@ mod tests { payload: digest, }; let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); // Latest should now be the finalized block assert_eq!( @@ -938,7 +1464,7 @@ mod tests { fn test_get_info_latest_progression_multiple_finalizations() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -948,7 +1474,7 @@ mod tests { // Single validator actor let me = participants[0].clone(); let (_application, mut actor, _processed_height) = setup_validator( - context.with_label("validator_0"), + context.child("validator").with_attribute("index", 0), &mut oracle, me, ConstantProvider::new(schemes[0].clone()), @@ -962,9 +1488,11 @@ mod tests { let parent0 = Sha256::hash(b""); let block1 = B::new::(parent0, Height::new(1), 1); let d1 = block1.digest(); - actor - .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(1)), block1.clone()) + .await + ); let f1 = make_finalization( Proposal { round: Round::new(Epoch::new(0), View::new(1)), @@ -974,15 +1502,17 @@ mod tests { &schemes, QUORUM, ); - actor.report(Activity::Finalization(f1)).await; + let _ = actor.report(Activity::Finalization(f1)); let latest = actor.get_info(Identifier::Latest).await; assert_eq!(latest, Some((Height::new(1), d1))); let block2 = B::new::(d1, Height::new(2), 2); let d2 = block2.digest(); - actor - .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(2)), block2.clone()) + .await + ); let f2 = make_finalization( Proposal { round: Round::new(Epoch::new(0), View::new(2)), @@ -992,15 +1522,17 @@ mod tests { &schemes, QUORUM, ); - actor.report(Activity::Finalization(f2)).await; + let _ = actor.report(Activity::Finalization(f2)); let latest = actor.get_info(Identifier::Latest).await; assert_eq!(latest, Some((Height::new(2), d2))); let block3 = B::new::(d2, Height::new(3), 3); let d3 = block3.digest(); - actor - .verified(Round::new(Epoch::new(0), View::new(3)), block3.clone()) - .await; + assert!( + actor + .verified(Round::new(Epoch::new(0), View::new(3)), block3.clone()) + .await + ); let f3 = make_finalization( Proposal { round: Round::new(Epoch::new(0), View::new(3)), @@ -1010,7 +1542,7 @@ mod tests { &schemes, QUORUM, ); - actor.report(Activity::Finalization(f3)).await; + let _ = actor.report(Activity::Finalization(f3)); let latest = actor.get_info(Identifier::Latest).await; assert_eq!(latest, Some((Height::new(3), d3))); }) @@ -1020,7 +1552,7 @@ mod tests { fn test_get_block_by_height_and_latest() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1029,7 +1561,7 @@ mod tests { let me = participants[0].clone(); let (application, mut actor, _height) = setup_validator( - context.with_label("validator_0"), + context.child("validator").with_attribute("index", 0), &mut oracle, me, ConstantProvider::new(schemes[0].clone()), @@ -1046,14 +1578,14 @@ mod tests { let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); - actor.verified(round, block.clone()).await; + assert!(actor.verified(round, block.clone()).await); let proposal = Proposal { round, parent: View::new(0), payload: commitment, }; let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); // Get by height let by_height = actor.get_block(1).await.expect("missing block by height"); @@ -1079,7 +1611,7 @@ mod tests { fn test_get_block_by_commitment_from_sources_and_missing() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1088,7 +1620,7 @@ mod tests { let me = participants[0].clone(); let (_application, mut actor, _processed_height) = setup_validator( - context.with_label("validator_0"), + context.child("validator").with_attribute("index", 0), &mut oracle, me, ConstantProvider::new(schemes[0].clone()), @@ -1100,7 +1632,7 @@ mod tests { 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)); - actor.verified(round1, ver_block.clone()).await; + assert!(actor.verified(round1, ver_block.clone()).await); let got = actor .get_block(&ver_commitment) .await @@ -1111,14 +1643,14 @@ mod tests { let fin_block = B::new::(ver_commitment, Height::new(2), 2); let fin_commitment = fin_block.digest(); let round2 = Round::new(Epoch::new(0), View::new(2)); - actor.verified(round2, fin_block.clone()).await; + assert!(actor.verified(round2, fin_block.clone()).await); let proposal = Proposal { round: round2, parent: View::new(1), payload: fin_commitment, }; let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); let got = actor .get_block(&fin_commitment) .await @@ -1137,7 +1669,7 @@ mod tests { fn test_get_finalization_by_height() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1146,7 +1678,7 @@ mod tests { let me = participants[0].clone(); let (_application, mut actor, _processed_height) = setup_validator( - context.with_label("validator_0"), + context.child("validator").with_attribute("index", 0), &mut oracle, me, ConstantProvider::new(schemes[0].clone()), @@ -1162,14 +1694,14 @@ mod tests { let block = B::new::(parent, Height::new(1), 1); let commitment = block.digest(); let round = Round::new(Epoch::new(0), View::new(1)); - actor.verified(round, block.clone()).await; + assert!(actor.verified(round, block.clone()).await); let proposal = Proposal { round, parent: View::new(0), payload: commitment, }; let finalization = make_finalization(proposal, &schemes, QUORUM); - actor.report(Activity::Finalization(finalization)).await; + let _ = actor.report(Activity::Finalization(finalization)); // Get finalization by height let finalization = actor @@ -1191,7 +1723,7 @@ mod tests { fn test_finalize_same_height_different_views() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1202,7 +1734,7 @@ mod tests { let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate().take(2) { let (_app, actor, _height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -1222,12 +1754,16 @@ mod tests { let commitment = block.digest(); // Both validators verify the block at its original view (19). - actors[0] - .verified(Round::new(Epoch::new(0), View::new(19)), block.clone()) - .await; - actors[1] - .verified(Round::new(Epoch::new(0), View::new(19)), block.clone()) - .await; + assert!( + actors[0] + .verified(Round::new(Epoch::new(0), View::new(19)), block.clone()) + .await + ); + assert!( + actors[1] + .verified(Round::new(Epoch::new(0), View::new(19)), block.clone()) + .await + ); // Validator 0: finalize at the block's own view (19) — exact match. let proposal_v1 = Proposal { @@ -1237,12 +1773,8 @@ mod tests { }; let notarization_v1 = make_notarization(proposal_v1.clone(), &schemes, QUORUM); let finalization_v1 = make_finalization(proposal_v1.clone(), &schemes, QUORUM); - actors[0] - .report(Activity::Notarization(notarization_v1.clone())) - .await; - actors[0] - .report(Activity::Finalization(finalization_v1.clone())) - .await; + let _ = actors[0].report(Activity::Notarization(notarization_v1.clone())); + let _ = actors[0].report(Activity::Finalization(finalization_v1.clone())); // Validator 1: finalize the same terminal block via a same-digest // reproposal certified in a later view (21). Header view 19 < 21 and @@ -1255,12 +1787,8 @@ mod tests { }; let notarization_v2 = make_notarization(proposal_v2.clone(), &schemes, QUORUM); let finalization_v2 = make_finalization(proposal_v2.clone(), &schemes, QUORUM); - actors[1] - .report(Activity::Notarization(notarization_v2.clone())) - .await; - actors[1] - .report(Activity::Finalization(finalization_v2.clone())) - .await; + let _ = actors[1].report(Activity::Notarization(notarization_v2.clone())); + let _ = actors[1].report(Activity::Finalization(finalization_v2.clone())); // Wait for finalization processing context.sleep(Duration::from_millis(100)).await; @@ -1295,14 +1823,10 @@ mod tests { Some((Height::new(19), commitment)) ); - // Test that a validator receiving BOTH finalizations handles it correctly - // (the second one should be ignored since archive ignores duplicates for same height) - actors[0] - .report(Activity::Finalization(finalization_v2.clone())) - .await; - actors[1] - .report(Activity::Finalization(finalization_v1.clone())) - .await; + // Test that a validator receiving both finalizations retains its + // original finalization. + let _ = actors[0].report(Activity::Finalization(finalization_v2.clone())); + let _ = actors[1].report(Activity::Finalization(finalization_v1.clone())); context.sleep(Duration::from_millis(100)).await; // Validator 0 should still have the original finalization (view 19) @@ -1319,7 +1843,7 @@ mod tests { fn test_broadcast_caches_block() { let runner = deterministic::Runner::timed(Duration::from_secs(60)); runner.start(|mut context| async move { - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1329,7 +1853,7 @@ mod tests { // Set up one validator let (i, validator) = participants.iter().enumerate().next().unwrap(); let (_application, mut actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -1342,9 +1866,11 @@ mod tests { let commitment = block.digest(); // Broadcast the block - actor - .proposed(Round::new(Epoch::new(0), View::new(1)), block.clone()) - .await; + assert!( + actor + .proposed(Round::new(Epoch::new(0), View::new(1)), block.clone()) + .await + ); // Ensure the block is cached and retrievable; This should hit the in-memory cache // via `buffered::Mailbox`. @@ -1355,7 +1881,9 @@ mod tests { // Restart marshal, removing any in-memory cache let (_application, mut actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}_restart")), + context + .child("validator_restart") + .with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -1374,7 +1902,7 @@ mod tests { &schemes, QUORUM, ); - actor.report(Activity::Notarization(notarization)).await; + let _ = actor.report(Activity::Notarization(notarization)); // Ensure the block is cached and retrievable let fetched = actor @@ -1395,7 +1923,7 @@ mod tests { runner.start(|mut context| async move { use futures::FutureExt as _; - let mut oracle = setup_network(context.clone(), NZUsize!(1)); + let mut oracle = setup_network(context.child("network_parent"), NZUsize!(1)); let Fixture { participants, schemes, @@ -1403,14 +1931,12 @@ mod tests { } = bls12381_threshold::(&mut context, NUM_VALIDATORS); let mut manager = oracle.manager(); - manager - .track(0, ordered::Set::try_from(participants.clone()).unwrap()) - .await; + let _ = manager.track(0, ordered::Set::try_from(participants.clone()).unwrap()); let mut actors = Vec::new(); for (i, validator) in participants.iter().enumerate() { let (_application, actor, _processed_height) = setup_validator( - context.with_label(&format!("validator_{i}")), + context.child("validator").with_attribute("index", i), &mut oracle, validator.clone(), ConstantProvider::new(schemes[i].clone()), @@ -1429,18 +1955,20 @@ mod tests { // The target and a bystander both wait for the block. let mut target = actors[1].clone(); let mut bystander = actors[2].clone(); - let target_rx = target.subscribe(None, commitment).await; - let bystander_rx = bystander.subscribe(None, commitment).await; + let target_rx = target.subscribe(None, commitment); + let bystander_rx = bystander.subscribe(None, commitment); // The source caches the block locally WITHOUT broadcasting it // (Message::Verified only populates the cache). let mut source = actors[0].clone(); - source.verified(round, block.clone()).await; + assert!(source.verified(round, block.clone()).await); // Forward only to the target. - source - .forward(round, commitment, vec![participants[1].clone()]) - .await; + let _ = source.forward( + round, + commitment, + Recipients::Some(vec![participants[1].clone()]), + ); // The target receives the block via the targeted send. let received = target_rx.await.unwrap(); @@ -1455,4 +1983,915 @@ mod tests { ); }); } + + /// Port of marshal's durability/recovery contracts for proposed, verified, + /// certified, and same-round equivocated candidates. + #[test_traced("WARN")] + fn test_durable_block_acks_imply_recovery_after_restart() { + let runner = deterministic::Runner::timed(Duration::from_secs(60)); + let ((validator, scheme, blocks), checkpoint) = + 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 prefix = "durable-block-recovery"; + let (_application, mut mailbox, actor_handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + validator.clone(), + ConstantProvider::new(scheme.clone()), + prefix, + ) + .await; + + 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); + let verified_b = B::new::(proposed.digest(), Height::new(2), 3); + let certified = B::new::(verified_a.digest(), Height::new(3), 4); + let proposed_round = Round::new(Epoch::zero(), View::new(1)); + let equivocated_round = Round::new(Epoch::zero(), View::new(2)); + let certified_round = Round::new(Epoch::zero(), View::new(3)); + + assert!(mailbox.proposed(proposed_round, proposed.clone()).await); + assert!( + mailbox + .proposed(proposed_round, proposed_conflict.clone()) + .await + ); + assert!( + mailbox + .verified(equivocated_round, verified_a.clone()) + .await + ); + assert!( + mailbox + .verified(equivocated_round, verified_b.clone()) + .await + ); + assert!(mailbox.verified(certified_round, certified.clone()).await); + assert!(mailbox.certified(certified_round, certified.clone()).await); + + actor_handle.abort(); + ( + validator, + scheme, + vec![ + (proposed_round, proposed), + (proposed_round, proposed_conflict), + (equivocated_round, verified_a), + (equivocated_round, verified_b), + (certified_round, certified), + ], + ) + }); + + deterministic::Runner::from(checkpoint).start(|context| async move { + let mut oracle = setup_network(context.child("network_restart"), NZUsize!(1)); + let (_application, mut mailbox, _actor_handle) = setup_validator_with_prefix( + context.child("validator_restart"), + &mut oracle, + validator, + ConstantProvider::new(scheme), + "durable-block-recovery", + ) + .await; + + let mut candidates_by_round: BTreeMap> = BTreeMap::new(); + for (round, block) in blocks { + candidates_by_round + .entry(round) + .or_default() + .push(block.digest()); + assert_eq!( + mailbox + .get_block(&block.digest()) + .await + .expect("durable block missing after restart"), + block + ); + } + for (round, candidates) in candidates_by_round { + let recovered = mailbox + .get_verified(round) + .await + .expect("round must retain at least one verified candidate"); + assert!( + candidates.contains(&recovered.digest()), + "round lookup returned a block outside its stored candidates" + ); + } + }); + } + + /// Port of marshal's fatal durability policy: a real sync failure must + /// panic rather than become a recoverable `false` verification verdict. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync verified")] + fn test_verified_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _actor_handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "verified-sync-failure", + ) + .await; + + context.storage_fault_config().write().sync_rate = Some(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; + }); + } + + /// Proposal propagation happens before persistence, but the proposal + /// durability handshake must still apply the fatal storage-failure policy. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync verified")] + fn test_proposed_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _actor_handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "proposed-sync-failure", + ) + .await; + + context.storage_fault_config().write().sync_rate = Some(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; + }); + } + + /// Port of marshal's certify-barrier failure test: failure of the composed + /// block and notarization sync handle is fatal. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync certified")] + fn test_certified_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _actor_handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "certified-sync-failure", + ) + .await; + + context.storage_fault_config().write().sync_rate = Some(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; + }); + } + + /// Port of marshal's pooled notarization failure test: fire-and-forget + /// consensus input must not hide a storage sync failure. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync notarization")] + fn test_notarization_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _actor_handle) = setup_validator_with_prefix( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "notarization-sync-failure", + ) + .await; + + context.storage_fault_config().write().sync_rate = Some(1.0); + let round = Round::new(Epoch::zero(), View::new(1)); + let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let _ = mailbox.report(Activity::Notarization(make_notarization( + Proposal { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ))); + context.sleep(Duration::from_secs(5)).await; + }); + } + + /// A finalized block must not be dispatched when its archive fails to sync. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync finalized blocks")] + fn test_finalized_block_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _buffer, _resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "finalized-block-sync-failure", + Duration::ZERO, + NZUsize!(1), + FinalizedSyncFailure::Blocks, + ) + .await; + + let round = Round::new(Epoch::zero(), View::new(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 { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ))); + context.sleep(Duration::from_secs(5)).await; + }); + } + + /// A finalized block must not be dispatched when its certificate archive fails to sync. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync finalizations")] + fn test_finalization_certificate_sync_failure_panics() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _buffer, _resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "finalization-certificate-sync-failure", + Duration::ZERO, + NZUsize!(1), + FinalizedSyncFailure::Finalizations, + ) + .await; + + let round = Round::new(Epoch::zero(), View::new(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 { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ))); + context.sleep(Duration::from_secs(5)).await; + }); + } + + /// Port of marshal's covering-sync test: a certify barrier must cover a + /// notarization write whose original handle was not awaited. + #[test_traced("WARN")] + fn test_start_sync_notarizations_covers_prior_write() { + let runner = deterministic::Runner::timed(Duration::from_secs(30)); + 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 notarization = make_notarization( + Proposal { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ); + let config = cache::Config { + partition_prefix: "covering-notarization-sync".to_string(), + prunable_items_per_section: NZU64!(10), + replay_buffer: NZUsize!(1024), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE), + }; + let mut manager = + cache::Manager::<_, B, S>::init(context.child("cache"), config, ()).await; + drop( + manager + .put_notarization(round, block.digest(), notarization) + .await, + ); + manager + .start_sync_notarizations(round) + .await + .await + .expect("failed to sync notarizations"); + round + }); + + deterministic::Runner::from(checkpoint).start(|context| async move { + let config = cache::Config { + partition_prefix: "covering-notarization-sync".to_string(), + prunable_items_per_section: NZU64!(10), + replay_buffer: NZUsize!(1024), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE), + }; + let mut manager = + cache::Manager::<_, B, S>::init(context.child("cache_restart"), config, ()).await; + manager.load_persisted_epochs().await; + assert!(manager.get_notarization(round).await.is_some()); + }); + } + + /// The notarization half of the certified durability barrier must + /// re-surface a failure from an earlier write even when that write's + /// original handle was dropped. + #[test_traced("WARN")] + #[should_panic(expected = "failed to sync certified")] + fn test_certified_prior_notarization_sync_failure_panics() { + 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 notarization = make_notarization( + Proposal { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ); + let config = cache::Config { + partition_prefix: "certified-notarization-sync-failure".to_string(), + prunable_items_per_section: NZU64!(10), + replay_buffer: NZUsize!(1024), + key_write_buffer: NZUsize!(1024), + value_write_buffer: NZUsize!(1024), + key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE), + }; + let mut manager = + cache::Manager::<_, B, S>::init(context.child("cache"), config, ()).await; + + context.storage_fault_config().write().sync_rate = Some(1.0); + drop( + manager + .put_notarization(round, block.digest(), notarization) + .await, + ); + + manager + .start_sync_notarizations(round) + .await + .durable(round, "certified") + .await; + }); + } + + /// Summit reuses an epoch-terminal block digest in later views. Each view + /// must retain its own verified entry so view-based pruning cannot remove + /// the later reproposal together with the original. + #[test_traced("WARN")] + fn test_epoch_terminal_reproposal_stored_at_each_round() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _height) = setup_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + ) + .await; + 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)); + + assert!(mailbox.verified(original, block.clone()).await); + assert!(mailbox.verified(reproposal, block.clone()).await); + assert_eq!(mailbox.get_verified(original).await, Some(block.clone())); + assert_eq!(mailbox.get_verified(reproposal).await, Some(block)); + }); + } + + /// Port of marshal's buffer-waiter floor regression: a block delivered by + /// the broadcast buffer must both wake subscribers and install a pending floor. + #[test_traced("WARN")] + fn test_buffer_waiter_completion_installs_floor_anchor() { + deterministic::Runner::timed(Duration::from_secs(30)).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, buffer, _resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "buffer-waiter-floor-anchor", + Duration::ZERO, + NZUsize!(1), + FinalizedSyncFailure::None, + ) + .await; + + const ANCHOR_HEIGHT: u64 = 5; + 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); + parent = block.digest(); + anchor = Some(block); + } + let anchor = anchor.expect("anchor missing"); + let subscription = mailbox.subscribe(None, anchor.digest()); + let round = Round::new(Epoch::zero(), View::new(ANCHOR_HEIGHT)); + mailbox.set_floor(make_finalization( + Proposal { + round, + parent: View::new(ANCHOR_HEIGHT - 1), + payload: anchor.digest(), + }, + &schemes, + QUORUM, + )); + + assert!( + mailbox + .get_block(Identifier::Height(Height::new(ANCHOR_HEIGHT))) + .await + .is_none() + ); + assert!(buffer.broadcast(Recipients::All, anchor.clone()).accepted()); + assert_eq!( + subscription.await.expect("floor subscription closed"), + anchor + ); + wait_until( + &context, + Duration::from_secs(1), + "floor anchor dispatched", + || application.blocks().contains_key(&ANCHOR_HEIGHT), + ) + .await; + assert_eq!(application.tip(), Some((ANCHOR_HEIGHT, anchor.digest()))); + }); + } + + /// Summit-specific contract: after notarized data is durable, speculative + /// execution observes the update before finalization makes the block canonical. + #[test_traced("WARN")] + fn test_durable_notarized_block_reported_before_finalized_block() { + deterministic::Runner::timed(Duration::from_secs(30)).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, _height) = setup_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + ) + .await; + let round = Round::new(Epoch::zero(), View::new(1)); + let block = B::new::(Sha256::hash(b""), Height::new(1), 1); + let proposal = Proposal { + round, + parent: View::zero(), + payload: block.digest(), + }; + assert!(mailbox.verified(round, block.clone()).await); + let _ = mailbox.report(Activity::Notarization(make_notarization( + proposal.clone(), + &schemes, + QUORUM, + ))); + let _ = mailbox.report(Activity::Finalization(make_finalization( + proposal, &schemes, QUORUM, + ))); + + while application.updates().len() < 2 { + context.sleep(Duration::from_millis(1)).await; + } + assert_eq!( + application.updates(), + vec![ + RecordedUpdate::Notarized(block.digest()), + RecordedUpdate::Finalized(block.digest()), + ] + ); + }); + } + + /// Port of marshal's paced finalized-store regression: a non-blocking + /// finalized sync must keep the mailbox responsive while application + /// dispatch remains gated until both archives are durable. + #[test_traced("WARN")] + fn test_finalization_sync_does_not_block_mailbox() { + const PACE: Duration = Duration::from_millis(100); + deterministic::Runner::timed(Duration::from_secs(30)).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, _buffer, _resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "paced-finalized-sync", + PACE, + NZUsize!(1), + FinalizedSyncFailure::None, + ) + .await; + + let round = Round::new(Epoch::zero(), View::new(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 { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ))); + + wait_until( + &context, + Duration::from_secs(1), + "finalized tip buffered", + || application.tip() == Some((1, block.digest())), + ) + .await; + let finalized_at = context.current(); + + context.sleep(Duration::from_millis(1)).await; + let requested_at = context.current(); + assert_eq!(mailbox.get_verified(round).await, Some(block.clone())); + let elapsed = context + .current() + .duration_since(requested_at) + .expect("time went backwards"); + assert!( + elapsed < Duration::from_millis(5), + "get_verified queued behind finalized sync: took {elapsed:?}" + ); + + assert!( + !application.blocks().contains_key(&1), + "block dispatched before finalized archives were durable" + ); + context.sleep(Duration::from_millis(90)).await; + assert!( + !application.blocks().contains_key(&1), + "block dispatched before finalized archives were durable" + ); + wait_until( + &context, + Duration::from_millis(50), + "finalized block dispatched", + || application.blocks().contains_key(&1), + ) + .await; + let dispatched = context + .current() + .duration_since(finalized_at) + .expect("time went backwards"); + assert!( + dispatched >= PACE, + "block dispatched before paced sync completed: {dispatched:?}" + ); + }); + } + + /// Port of marshal's overlapping finalized-sync regression: each completed + /// sync releases only the writes in the batch it covers. + #[test_traced("WARN")] + fn test_overlapping_finalized_syncs_release_per_batch() { + const PACE: Duration = Duration::from_millis(100); + const STAGGER: Duration = Duration::from_millis(50); + deterministic::Runner::timed(Duration::from_secs(30)).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, _buffer, _resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "overlapping-finalized-syncs", + PACE, + NZUsize!(4), + FinalizedSyncFailure::None, + ) + .await; + + let first_round = Round::new(Epoch::zero(), View::new(1)); + 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); + assert!(mailbox.verified(second_round, second.clone()).await); + + let started_at = context.current(); + let _ = mailbox.report(Activity::Finalization(make_finalization( + Proposal { + round: first_round, + parent: View::zero(), + payload: first.digest(), + }, + &schemes, + QUORUM, + ))); + context.sleep(STAGGER).await; + let _ = mailbox.report(Activity::Finalization(make_finalization( + Proposal { + round: second_round, + parent: View::new(1), + payload: second.digest(), + }, + &schemes, + QUORUM, + ))); + + wait_until( + &context, + Duration::from_millis(150), + "first block dispatched", + || application.blocks().contains_key(&1), + ) + .await; + let first_dispatched = context + .current() + .duration_since(started_at) + .expect("time went backwards"); + assert!( + first_dispatched >= PACE, + "block dispatched before its sync completed: {first_dispatched:?}" + ); + assert!( + first_dispatched < STAGGER + PACE, + "first block waited for the second sync: {first_dispatched:?}" + ); + + assert!( + !application.blocks().contains_key(&2), + "block dispatched before its sync completed" + ); + context.sleep(Duration::from_millis(20)).await; + assert!( + !application.blocks().contains_key(&2), + "block dispatched before its sync completed" + ); + wait_until( + &context, + Duration::from_millis(50), + "second block dispatched", + || application.blocks().contains_key(&2), + ) + .await; + let second_dispatched = context + .current() + .duration_since(started_at) + .expect("time went backwards"); + assert!( + second_dispatched >= STAGGER + PACE, + "block dispatched before its sync completed: {second_dispatched:?}" + ); + }); + } + + /// Port of marshal's stale-floor regression: finalized repair writes must + /// gate dispatch from the moment they are buffered, even when a later item + /// in the same resolver batch triggers dispatch before the pooled sync starts. + #[test_traced("WARN")] + fn test_stale_floor_anchor_holds_dispatch_until_durable() { + const PACE: Duration = Duration::from_millis(100); + deterministic::Runner::timed(Duration::from_secs(30)).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, _buffer, resolver, _actor_handle) = + setup_paced_validator( + context.child("validator"), + &mut oracle, + participants[0].clone(), + ConstantProvider::new(schemes[0].clone()), + "stale-floor-anchor", + PACE, + NZUsize!(4), + FinalizedSyncFailure::None, + ) + .await; + + let round = Round::new(Epoch::zero(), View::new(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 { + round, + parent: View::zero(), + payload: block.digest(), + }, + &schemes, + QUORUM, + ))); + wait_until( + &context, + Duration::from_secs(1), + "block 1 processed", + || application.blocks().contains_key(&1), + ) + .await; + while mailbox.get_processed_height().await != Some(Height::new(1)) { + context.sleep(Duration::from_millis(1)).await; + } + + 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)), + parent: View::new(4), + payload: fork.digest(), + }, + &schemes, + QUORUM, + )); + wait_until( + &context, + Duration::from_secs(1), + "floor anchor fetch", + || { + resolver.fetches().iter().any(|fetch| { + matches!(fetch.key, Key::Block(commitment) if commitment == fork.digest()) + }) + }, + ) + .await; + let anchor_fetch = resolver + .fetches() + .into_iter() + .find(|fetch| { + matches!(fetch.key, Key::Block(commitment) if commitment == fork.digest()) + }) + .expect("anchor fetch missing"); + + let next = B::new::(block.digest(), Height::new(2), 2); + let (next_response, next_response_rx) = oneshot::channel(); + assert!( + resolver + .enqueue(handler::Message::Deliver { + delivery: Delivery { + key: Key::Block(next.digest()), + subscribers: NonEmptyVec::new(( + Annotation::Finalized(Finalized::ByHeight { + height: Height::new(2), + }), + tracing::Span::none(), + )), + }, + value: next.encode(), + response: next_response, + }) + .accepted() + ); + let above = B::new::(next.digest(), Height::new(3), 3); + let (above_response, above_response_rx) = oneshot::channel(); + assert!( + resolver + .enqueue(handler::Message::Deliver { + delivery: Delivery { + key: Key::Block(above.digest()), + subscribers: NonEmptyVec::new(( + Annotation::Finalized(Finalized::ByHeight { + height: Height::new(3), + }), + tracing::Span::none(), + )), + }, + value: above.encode(), + response: above_response, + }) + .accepted() + ); + let (anchor_response, anchor_response_rx) = oneshot::channel(); + assert!( + resolver + .enqueue(handler::Message::Deliver { + delivery: Delivery { + key: anchor_fetch.key, + subscribers: NonEmptyVec::new(( + anchor_fetch.subscriber, + tracing::Span::none(), + )), + }, + value: fork.encode(), + response: anchor_response, + }) + .accepted() + ); + let delivered_at = context.current(); + assert!(next_response_rx.await.expect("repair response missing")); + assert!(above_response_rx.await.expect("repair response missing")); + assert!(anchor_response_rx.await.expect("anchor response missing")); + + context.sleep(Duration::from_millis(1)).await; + assert!( + !application.blocks().contains_key(&2), + "repair block dispatched before finalized archives were durable" + ); + context.sleep(Duration::from_millis(90)).await; + assert!( + !application.blocks().contains_key(&2), + "repair block dispatched before finalized archives were durable" + ); + wait_until( + &context, + Duration::from_millis(50), + "repair blocks dispatched", + || application.blocks().contains_key(&3), + ) + .await; + assert!(application.blocks().contains_key(&2)); + let dispatched = context + .current() + .duration_since(delivered_at) + .expect("time went backwards"); + assert!( + dispatched >= PACE, + "repair blocks dispatched before paced sync completed: {dispatched:?}" + ); + }); + } } diff --git a/syncer/src/mocks/application.rs b/syncer/src/mocks/application.rs index 0a31fd52..bc79f28f 100644 --- a/syncer/src/mocks/application.rs +++ b/syncer/src/mocks/application.rs @@ -1,4 +1,5 @@ use crate::Update; +use commonware_actor::Feedback; use commonware_consensus::simplex::scheme::Scheme; use commonware_consensus::{Block, Reporter}; use commonware_utils::Acknowledgement; @@ -7,10 +8,17 @@ use std::{ sync::{Arc, Mutex}, }; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RecordedUpdate { + Finalized(D), + Notarized(D), +} + /// A mock application that stores finalized blocks. #[derive(Clone)] pub struct Application> { blocks: Arc>>, + updates: Arc>>>, #[allow(clippy::type_complexity)] tip: Arc>>, _phantom: std::marker::PhantomData, @@ -20,6 +28,7 @@ impl> Default for Application { fn default() -> Self { Self { blocks: Default::default(), + updates: Default::default(), tip: Default::default(), _phantom: std::marker::PhantomData, } @@ -36,26 +45,39 @@ impl> Application { pub fn tip(&self) -> Option<(u64, B::Digest)> { *self.tip.lock().unwrap() } + + /// Returns finalized and notarized block updates in report order. + pub fn updates(&self) -> Vec> { + self.updates.lock().unwrap().clone() + } } impl> Reporter for Application { type Activity = Update; - async fn report(&mut self, activity: Self::Activity) { + fn report(&mut self, activity: Self::Activity) -> Feedback { match activity { Update::Tip(height, commitment) => { *self.tip.lock().unwrap() = Some((height, commitment)); } Update::FinalizedBlock((block, _), ack_tx) => { + self.updates + .lock() + .unwrap() + .push(RecordedUpdate::Finalized(block.digest())); self.blocks .lock() .unwrap() .insert(block.height().get(), block); ack_tx.acknowledge(); } - Update::NotarizedBlock(_block) => { - // Mock application ignores notarized blocks + Update::NotarizedBlock(block) => { + self.updates + .lock() + .unwrap() + .push(RecordedUpdate::Notarized(block.digest())); } } + Feedback::Ok } } diff --git a/syncer/src/mocks/fixtures.rs b/syncer/src/mocks/fixtures.rs index 1878881b..6a18f72c 100644 --- a/syncer/src/mocks/fixtures.rs +++ b/syncer/src/mocks/fixtures.rs @@ -17,7 +17,7 @@ use commonware_cryptography::{ use commonware_math::algebra::Random; use commonware_utils::N3f1; use commonware_utils::ordered::{BiMap, Map}; -use rand::{CryptoRng, RngCore}; +use rand_core::{CryptoRng, Rng}; /// A test fixture consisting of ed25519 keys and signing schemes for each validator, and a single /// scheme verifier. @@ -33,7 +33,7 @@ pub struct Fixture { /// Generates ed25519 participants. pub fn ed25519_participants(rng: &mut R, n: u32) -> Map where - R: RngCore + CryptoRng, + R: Rng + CryptoRng, { let mut pairs = Vec::with_capacity(n as usize); for _ in 0..n { @@ -49,7 +49,7 @@ where /// Returns a [`Fixture`] whose keys and scheme instances share a consistent ordering. pub fn ed25519(rng: &mut R, n: u32) -> Fixture where - R: RngCore + CryptoRng, + R: Rng + CryptoRng, { assert!(n > 0); @@ -79,7 +79,7 @@ pub fn bls12381_multisig( ) -> Fixture> where V: Variant, - R: RngCore + CryptoRng, + R: Rng + CryptoRng, { assert!(n > 0); @@ -118,7 +118,7 @@ pub fn bls12381_threshold( ) -> Fixture> where V: Variant, - R: RngCore + CryptoRng, + R: Rng + CryptoRng, { assert!(n > 0); @@ -126,7 +126,7 @@ where let participants = ed25519_participants(rng, n).into_keys(); let (output, shares_map) = - dkg::deal::(rng, Mode::NonZeroCounter, participants.clone()) + dkg::feldman_desmedt::deal::(rng, Mode::NonZeroCounter, participants.clone()) .expect("deal should succeed"); let polynomial = output.public().clone(); diff --git a/syncer/src/resolver/p2p.rs b/syncer/src/resolver/p2p.rs index 530f25be..2a74aedc 100644 --- a/syncer/src/resolver/p2p.rs +++ b/syncer/src/resolver/p2p.rs @@ -1,14 +1,14 @@ //! P2P resolver initialization and config. -use crate::ingress::handler::{self, Handler}; +use crate::ingress::handler::{self, Annotation, Key, Receiver as HandlerReceiver}; +use commonware_actor::mailbox; use commonware_cryptography::{Digest, PublicKey}; -use commonware_p2p::{Blocker, Provider, Receiver, Sender}; +use commonware_p2p::{Blocker, Provider, Receiver as P2pReceiver, Sender}; use commonware_resolver::p2p; use commonware_runtime::{BufferPooler, Clock, Metrics, Spawner}; -use commonware_utils::channel::mpsc; use governor::clock::Clock as GClock; use rand::Rng; -use std::time::Duration; +use std::{num::NonZeroUsize, time::Duration}; /// Configuration for the P2P [Resolver](commonware_resolver::Resolver). pub struct Config, B: Blocker> { @@ -22,7 +22,7 @@ pub struct Config, B: Blocker, B: Blocker = p2p::Mailbox, P, Annotation>; + /// Initialize a P2P resolver. pub fn init( - ctx: &E, + context: E, config: Config, backfill: (S, R), -) -> ( - mpsc::Receiver>, - p2p::Mailbox, P>, -) +) -> (HandlerReceiver, Mailbox) where E: BufferPooler + Rng + Spawner + Clock + GClock + Metrics, C: Provider, Bl: Blocker, D: Digest, S: Sender, - R: Receiver, + R: P2pReceiver, P: PublicKey, { - let (handler, receiver) = mpsc::channel(config.mailbox_size); - let handler = Handler::new(handler); + let (sender, receiver) = mailbox::new_unreliable(context.child("handler"), config.mailbox_size); + let handler = handler::Handler::new(sender); let (resolver_engine, resolver) = p2p::Engine::new( - ctx.with_label("resolver"), + context.child("resolver"), p2p::Config { peer_provider: config.provider, blocker: config.blocker, @@ -77,5 +77,5 @@ where }, ); resolver_engine.start(backfill); - (receiver, resolver) + (HandlerReceiver::new(receiver), resolver) } diff --git a/syncer/src/standard/variant.rs b/syncer/src/standard/variant.rs index 1739c971..21eab8b5 100644 --- a/syncer/src/standard/variant.rs +++ b/syncer/src/standard/variant.rs @@ -4,11 +4,13 @@ //! receives the full block directly from the proposer or via gossip. use crate::variant::{Buffer, Variant}; -use commonware_broadcast::{Broadcaster, buffered}; +use commonware_broadcast::buffered; +use commonware_codec::Read; use commonware_consensus::{Block, types::Round}; use commonware_cryptography::{Digestible, PublicKey}; use commonware_p2p::Recipients; use commonware_utils::channel::oneshot; +use std::sync::Arc; /// The standard variant of the syncer, which broadcasts complete blocks. /// @@ -38,6 +40,13 @@ where block.parent() } + fn block_cfg( + block_cfg: &::Cfg, + _expected: Self::Commitment, + ) -> ::Cfg { + block_cfg.clone() + } + fn into_inner(block: Self::Block) -> Self::ApplicationBlock { block } @@ -49,34 +58,28 @@ where K: PublicKey, { type PublicKey = K; - type CachedBlock = B; - async fn find_by_digest(&self, digest: B::Digest) -> Option { + async fn find_by_digest(&self, digest: B::Digest) -> Option> { self.get(digest).await } - async fn find_by_commitment(&self, commitment: B::Digest) -> Option { + async fn find_by_commitment(&self, commitment: B::Digest) -> Option> { self.find_by_digest(commitment).await } - async fn subscribe_by_digest(&self, digest: B::Digest) -> oneshot::Receiver { - let (tx, rx) = oneshot::channel(); - self.subscribe_prepared(digest, tx).await; - rx + fn subscribe_by_digest(&self, digest: B::Digest) -> Option>> { + Some(self.subscribe(digest)) } - async fn subscribe_by_commitment( - &self, - commitment: B::Digest, - ) -> oneshot::Receiver { - self.subscribe_by_digest(commitment).await + fn subscribe_by_commitment(&self, commitment: B::Digest) -> Option>> { + self.subscribe_by_digest(commitment) } - async fn finalized(&self, _commitment: B::Digest) { + fn finalized(&self, _commitment: B::Digest) { // No cleanup needed in standard mode — the buffer handles its own pruning } - async fn send(&self, _round: Round, block: B, recipients: Recipients) { - let _peers = Broadcaster::broadcast(self, recipients, block).await; + fn send(&self, _round: Round, block: Arc, recipients: Recipients) { + self.broadcast_shared(recipients, block); } } diff --git a/syncer/src/stream.rs b/syncer/src/stream.rs new file mode 100644 index 00000000..62bbcada --- /dev/null +++ b/syncer/src/stream.rs @@ -0,0 +1,82 @@ +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/variant.rs b/syncer/src/variant.rs index 1cb44212..5b88a64d 100644 --- a/syncer/src/variant.rs +++ b/syncer/src/variant.rs @@ -11,9 +11,8 @@ //! contain extra information for optimized retrieval, though the digest must be extractable //! from the commitment for lookup purposes. -use crate::Block; use commonware_codec::{Codec, Read}; -use commonware_consensus::types::Round; +use commonware_consensus::{Block, types::Round}; use commonware_cryptography::{Digest, Digestible, PublicKey}; use commonware_p2p::Recipients; use commonware_utils::channel::oneshot; @@ -37,7 +36,7 @@ pub trait Variant: Clone + Send + Sync + 'static { type StoredBlock: Block::Digest> + Into + Clone - + Codec::Cfg>; + + Codec::Cfg>; /// The [`Digest`] type used by consensus. type Commitment: Digest; @@ -54,6 +53,15 @@ pub trait Variant: Clone + Send + Sync + 'static { /// Returns the parent commitment referenced by `block`. fn parent_commitment(block: &Self::Block) -> Self::Commitment; + /// Returns the codec configuration used to decode [`Self::Block`] received over the wire. + /// + /// The returned configuration may bind `expected` so that decoding rejects + /// blocks that do not match the expected commitment. + fn block_cfg( + block_cfg: &::Cfg, + expected: Self::Commitment, + ) -> ::Cfg; + /// Converts a working block to an application block. fn into_inner(block: Self::Block) -> Self::ApplicationBlock; } @@ -68,61 +76,41 @@ pub trait Buffer: Clone + Send + Sync + 'static { /// The public key type used to identify peers. type PublicKey: PublicKey; - /// The cached block type held internally by the buffer. - type CachedBlock: IntoBlock; - /// Attempt to find a block by its digest. fn find_by_digest( &self, digest: ::Digest, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; /// Attempt to find a block by its commitment. fn find_by_commitment( &self, commitment: V::Commitment, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; /// Subscribe to a block's availability by its digest. + /// + /// Returns a receiver that will resolve when the block becomes available. + /// If the block is already cached, the receiver may resolve immediately. + /// Returns `None` when the buffer cannot provide availability notifications. fn subscribe_by_digest( &self, digest: ::Digest, - ) -> impl Future> + Send; + ) -> Option>>; /// Subscribe to a block's availability by its commitment. + /// + /// Returns a receiver that will resolve when the block becomes available. + /// If the block is already cached, the receiver may resolve immediately. + /// Returns `None` when the buffer cannot provide availability notifications. fn subscribe_by_commitment( &self, commitment: V::Commitment, - ) -> impl Future> + Send; + ) -> Option>>; /// Notify the buffer that a block has been finalized. - fn finalized(&self, commitment: V::Commitment) -> impl Future + Send; + fn finalized(&self, commitment: V::Commitment); /// Send a block to peers. - fn send( - &self, - round: Round, - block: V::Block, - recipients: Recipients, - ) -> impl Future + Send; -} - -/// A trait for cached block types that can be converted to the underlying block. -pub trait IntoBlock: Clone + Send { - /// Convert this cached block into the underlying block type. - fn into_block(self) -> B; -} - -/// Blanket implementation for any cloneable block type. -impl IntoBlock for B { - fn into_block(self) -> B { - self - } -} - -/// Implementation for `Arc` to support the coding variant. -impl IntoBlock for Arc { - fn into_block(self) -> B { - Self::unwrap_or_clone(self) - } + fn send(&self, round: Round, block: Arc, recipients: Recipients); } diff --git a/types/Cargo.toml b/types/Cargo.toml index d9f11325..60e612ae 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -4,10 +4,12 @@ version.workspace = true edition.workspace = true [dependencies] +commonware-actor.workspace = true commonware-cryptography.workspace = true commonware-consensus.workspace = true commonware-codec.workspace = true commonware-math.workspace = true +commonware-formatting.workspace = true commonware-utils.workspace = true commonware-resolver.workspace = true commonware-p2p.workspace = true diff --git a/types/src/bootstrap.rs b/types/src/bootstrap.rs index 64dca8dd..67b775ca 100644 --- a/types/src/bootstrap.rs +++ b/types/src/bootstrap.rs @@ -1,8 +1,9 @@ use crate::PublicKey; use crate::utils::get_expanded_path; use commonware_codec::DecodeExt as _; +use commonware_formatting::from_hex; use commonware_p2p::Ingress; -use commonware_utils::{Hostname, from_hex_formatted}; +use commonware_utils::Hostname; use serde::Deserialize; use std::net::SocketAddr; @@ -59,8 +60,8 @@ impl Bootstrappers { pub fn to_ingress_list(&self) -> Result, Box> { let mut result = Vec::with_capacity(self.bootstrappers.len()); for entry in &self.bootstrappers { - let pk_bytes: Vec = from_hex_formatted(&entry.node_public_key) - .ok_or("Invalid hex-encoded public key")?; + let pk_bytes: Vec = + from_hex(&entry.node_public_key).ok_or("Invalid hex-encoded public key")?; let pk = PublicKey::decode(&pk_bytes[..]).map_err(|e| format!("Invalid public key: {e}"))?; let ingress = parse_ingress(&entry.address)?; @@ -74,12 +75,12 @@ impl Bootstrappers { mod tests { use super::*; use commonware_cryptography::{Signer, ed25519::PrivateKey}; + use commonware_formatting::hex; use commonware_math::algebra::Random; - use commonware_utils::hex; - use rand::rngs::OsRng; + use commonware_utils::TestRng; fn generate_test_pk_hex() -> String { - let private_key = PrivateKey::random(&mut OsRng); + let private_key = PrivateKey::random(TestRng::new(0)); let public_key = private_key.public_key(); format!("0x{}", hex(public_key.as_ref())) } diff --git a/types/src/checkpoint.rs b/types/src/checkpoint.rs index a3df47e8..80497e0e 100644 --- a/types/src/checkpoint.rs +++ b/types/src/checkpoint.rs @@ -9,12 +9,11 @@ use commonware_codec::{DecodeExt, Encode, EncodeSize, Error, Read, ReadExt, Writ use commonware_consensus::types::Epoch; use commonware_cryptography::bls12381::primitives::variant::{MinPk, Variant}; use commonware_cryptography::{Hasher, Sha256, ed25519}; +use commonware_formatting::from_hex; +use commonware_formatting::hex; use commonware_parallel::Sequential; -use commonware_utils::TryCollect; -use commonware_utils::from_hex_formatted; -use commonware_utils::hex; use commonware_utils::ordered::BiMap; -use rand::rngs::OsRng; +use commonware_utils::{TryCollect, sys_rng}; use ssz::{Decode, Encode as SszEncode}; use std::collections::BTreeMap; use std::{error, fmt}; @@ -348,7 +347,7 @@ pub fn verify_checkpoint_chain_with_weak_subjectivity( // `prev_epoch_header_hash` must equal the eth genesis hash (see // finalizer/src/actor.rs where `prev_header_hash` falls back to // `self.genesis_hash` when no prior finalized header exists). - let genesis_hash: Digest = from_hex_formatted(&genesis.eth_genesis_hash) + let genesis_hash: Digest = from_hex(&genesis.eth_genesis_hash) .and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()) .map(Digest::from) .ok_or_else(|| { @@ -380,7 +379,7 @@ pub fn verify_checkpoint_chain_with_weak_subjectivity( .collect(); participants.sort_by(|a, b| a.0.cmp(&b.0)); - let mut rng = OsRng; + let mut rng = sys_rng(); let mut signing_set = participants.clone(); for (i, finalized_header) in finalized_headers.iter().enumerate() { @@ -771,7 +770,7 @@ mod tests { fn parse_public_key(public_key: &str) -> ed25519::PublicKey { ed25519::PublicKey::decode( - commonware_utils::from_hex_formatted(public_key) + commonware_formatting::from_hex(public_key) .unwrap() .as_ref(), ) @@ -1535,9 +1534,9 @@ mod tests { use commonware_consensus::types::{Epoch, Round, View}; use commonware_cryptography::bls12381::primitives::group; use commonware_cryptography::bls12381::primitives::variant::{MinPk, Variant}; + use commonware_formatting::hex; use commonware_parallel::Sequential; use commonware_utils::TryCollect; - use commonware_utils::hex; use commonware_utils::ordered::BiMap; let namespace = "checkpoint-typed-header-test".to_string(); diff --git a/types/src/ext_private_key.rs b/types/src/ext_private_key.rs index e7f4e878..eb4ce6a9 100644 --- a/types/src/ext_private_key.rs +++ b/types/src/ext_private_key.rs @@ -6,7 +6,7 @@ use commonware_utils::union_unique; use curve25519_dalek::{ constants::ED25519_BASEPOINT_POINT, edwards::CompressedEdwardsY, scalar::Scalar, }; -use rand_core::CryptoRngCore; +use rand_core::CryptoRng; use sha2::{Digest, Sha512}; const DERIVE_TAG: &[u8] = b"ed25519-additive-derive/tweak/v1"; @@ -62,7 +62,7 @@ impl ExtPrivateKey { } impl Random for ExtPrivateKey { - fn random(rng: impl CryptoRngCore) -> Self { + fn random(rng: impl CryptoRng) -> Self { let master = PrivateKey::random(rng); // Random master => no cross-deployment collision concern; empty namespace. ExtPrivateKey::derive_child_signer(&master, b"", 0) @@ -168,12 +168,12 @@ fn compute_tweak(master_pub: &[u8; 32], namespace: &[u8], path: &[u8]) -> Scalar mod tests { use super::*; use commonware_cryptography::Verifier; - use rand_core::OsRng; + use commonware_utils::TestRng; #[test] fn test_derived_pubkey_matches() { // Create master key - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(0)); let master_pk = master_sk.public_key(); // Derive first child @@ -193,7 +193,7 @@ mod tests { let msg: &[u8] = b"hello, world!"; // Create master key - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(1)); let master_pk = master_sk.public_key(); // Derive first child @@ -213,7 +213,7 @@ mod tests { #[test] fn test_siblings_different() { // Create master key - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(2)); // Derive first child let child_index = 0; @@ -233,7 +233,7 @@ mod tests { #[test] fn test_derivation_deterministic() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(3)); let child_a = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 42); let child_b = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 42); @@ -246,8 +246,8 @@ mod tests { #[test] fn test_different_masters_different_children() { - let master_a = PrivateKey::random(&mut OsRng); - let master_b = PrivateKey::random(&mut OsRng); + let master_a = PrivateKey::random(TestRng::new(4)); + let master_b = PrivateKey::random(TestRng::new(5)); let index = 0; let child_a = ExtPrivateKey::derive_child_signer(&master_a, b"test-ns", index); @@ -258,7 +258,7 @@ mod tests { #[test] fn test_wrong_index_verify_fails() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(6)); let master_pk = master_sk.public_key(); let signer = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 5); let sig = signer.sign(b"ns", b"msg"); @@ -269,8 +269,8 @@ mod tests { #[test] fn test_wrong_master_verify_fails() { - let master_a = PrivateKey::random(&mut OsRng); - let master_b = PrivateKey::random(&mut OsRng); + let master_a = PrivateKey::random(TestRng::new(7)); + let master_b = PrivateKey::random(TestRng::new(8)); let index = 0; let signer = ExtPrivateKey::derive_child_signer(&master_a, b"test-ns", index); @@ -282,7 +282,7 @@ mod tests { #[test] fn test_tampered_message_verify_fails() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(9)); let signer = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 0); let pubkey = signer.public_key(); let sig = signer.sign(b"ns", b"original"); @@ -293,7 +293,7 @@ mod tests { #[test] fn test_clone_equivalence() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(10)); let original = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 7); let cloned = original.clone(); @@ -306,7 +306,7 @@ mod tests { #[test] fn test_empty_namespace_and_msg() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(11)); let signer = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 0); let pubkey = signer.public_key(); @@ -316,7 +316,7 @@ mod tests { #[test] fn test_index_boundaries() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(12)); let child_min = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", 0); let child_max = ExtPrivateKey::derive_child_signer(&master_sk, b"test-ns", u32::MAX); @@ -329,7 +329,7 @@ mod tests { // in the namespace identically (so they still match each other within a namespace). #[test] fn test_namespace_domain_separation() { - let master_sk = PrivateKey::random(&mut OsRng); + let master_sk = PrivateKey::random(TestRng::new(13)); let master_pk = master_sk.public_key(); let index = 3; diff --git a/types/src/genesis.rs b/types/src/genesis.rs index 1a832f6c..6f8dc4a9 100644 --- a/types/src/genesis.rs +++ b/types/src/genesis.rs @@ -8,7 +8,7 @@ use anyhow::Context; use commonware_codec::DecodeExt; use commonware_cryptography::bls12381; use commonware_cryptography::{Hasher as _, Sha256}; -use commonware_utils::{from_hex, from_hex_formatted}; +use commonware_formatting::from_hex; use serde::{Deserialize, Serialize}; use ssz::Encode as _; use std::net::SocketAddr; @@ -151,11 +151,11 @@ impl TryFrom<&GenesisValidator> for Validator { fn try_from(value: &GenesisValidator) -> Result { let node_key_bytes = - from_hex_formatted(&value.node_public_key).context("Node PublicKey bad format")?; + from_hex(&value.node_public_key).context("Node PublicKey bad format")?; let node_public_key = PublicKey::decode(&*node_key_bytes)?; - let consensus_key_bytes = from_hex_formatted(&value.consensus_public_key) - .context("Consensus PublicKey bad format")?; + let consensus_key_bytes = + from_hex(&value.consensus_public_key).context("Consensus PublicKey bad format")?; let consensus_public_key = bls12381::PublicKey::decode(&*consensus_key_bytes)?; Ok(Validator { @@ -197,7 +197,7 @@ impl Genesis { /// deployment. Panics if `eth_genesis_hash` is not a 32-byte hex string; /// genesis files are operator-provided and validated at load time. pub fn genesis_hash(&self) -> [u8; 32] { - from_hex_formatted(&self.eth_genesis_hash) + from_hex(&self.eth_genesis_hash) .map(|bytes| bytes.try_into()) .expect("bad eth_genesis_hash") .expect("bad eth_genesis_hash") @@ -323,7 +323,7 @@ impl Genesis { pub fn ip_of(&self, target_public_key: &PublicKey) -> Option { for validator in &self.validators { #[allow(clippy::collapsible_if)] - if let Some(public_key_bytes) = from_hex_formatted(&validator.node_public_key) { + if let Some(public_key_bytes) = from_hex(&validator.node_public_key) { if let Ok(pub_key) = PublicKey::decode(&*public_key_bytes) { if &pub_key == target_public_key { if let Ok(socket_addr) = validator.ip_address.parse() { @@ -353,11 +353,11 @@ impl Genesis { ) -> Result, Box> { let mut keys = Vec::new(); for validator in &self.validators { - let node_key_bytes = from_hex_formatted(&validator.node_public_key) + let node_key_bytes = from_hex(&validator.node_public_key) .ok_or("Invalid hex format for node public key")?; let node_key = PublicKey::decode(&*node_key_bytes)?; - let consensus_key_bytes = from_hex_formatted(&validator.consensus_public_key) + let consensus_key_bytes = from_hex(&validator.consensus_public_key) .ok_or("Invalid hex format for consensus public key")?; let consensus_key = bls12381::PublicKey::decode(&*consensus_key_bytes)?; diff --git a/types/src/key_paths.rs b/types/src/key_paths.rs index f81334e5..9cef1853 100644 --- a/types/src/key_paths.rs +++ b/types/src/key_paths.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; use commonware_codec::DecodeExt; use commonware_cryptography::Signer; use commonware_cryptography::bls12381::PrivateKey as BlsPrivateKey; -use commonware_utils::from_hex_formatted; +use commonware_formatting::from_hex; /// Helper struct for managing key paths and loading keys from a key store directory. /// @@ -70,7 +70,7 @@ impl KeyPaths { warn_if_permissions_too_open(&path); let encoded_pk = std::fs::read_to_string(&path) .context(format!("Failed to read node key from {:?}", path))?; - let key = from_hex_formatted(&encoded_pk).context("Invalid hex format for node key")?; + let key = from_hex(&encoded_pk).context("Invalid hex format for node key")?; let pk = PrivateKey::decode(&*key).context("Unable to decode node private key")?; Ok(pk) } @@ -81,7 +81,7 @@ impl KeyPaths { warn_if_permissions_too_open(&path); let encoded_pk = std::fs::read_to_string(&path) .context(format!("Failed to read BLS key from {:?}", path))?; - let key = from_hex_formatted(&encoded_pk).context("Invalid hex format for BLS key")?; + let key = from_hex(&encoded_pk).context("Invalid hex format for BLS key")?; let pk = BlsPrivateKey::decode(&*key).context("Unable to decode BLS private key")?; Ok(pk) } diff --git a/types/src/lib.rs b/types/src/lib.rs index 80e9bbb6..7fe13d72 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -154,9 +154,9 @@ mod chain_domain_tests { use crate::PrivateKey; use commonware_cryptography::{Signer, Verifier}; use commonware_math::algebra::Random; - use rand_core::OsRng; + use commonware_utils::TestRng; - let key = PrivateKey::random(&mut OsRng); + let key = PrivateKey::random(TestRng::new(0)); let pk = key.public_key(); let msg = b"peer-handshake"; diff --git a/types/src/network_oracle.rs b/types/src/network_oracle.rs index aaffb1f2..63deba75 100644 --- a/types/src/network_oracle.rs +++ b/types/src/network_oracle.rs @@ -1,10 +1,10 @@ +use commonware_actor::Feedback; use commonware_cryptography::PublicKey; use commonware_p2p::{ - Blocker, Manager, PeerSetUpdate, Provider, TrackedPeers, authenticated::discovery::Oracle, + Blocker, Manager, PeerSetSubscription, Provider, TrackedPeers, authenticated::discovery::Oracle, }; use commonware_utils::ordered::Set as OrderedSet; use std::future::Future; -use tokio::sync::mpsc::UnboundedReceiver; pub trait NetworkOracle: Send + Sync + 'static { fn track( @@ -30,17 +30,17 @@ impl NetworkOracle for DiscoveryOracle { async fn track(&mut self, index: u64, primary: Vec, secondary: Vec) { let primary = OrderedSet::from_iter_dedup(primary); let secondary = OrderedSet::from_iter_dedup(secondary); - self.oracle - .track(index, TrackedPeers::new(primary, secondary)) - .await; + let _ = self + .oracle + .track(index, TrackedPeers::new(primary, secondary)); } } impl Blocker for DiscoveryOracle { type PublicKey = C; - async fn block(&mut self, public_key: Self::PublicKey) { - self.oracle.block(public_key).await + fn block(&mut self, public_key: Self::PublicKey) -> Feedback { + self.oracle.block(public_key) } } @@ -51,16 +51,16 @@ impl Provider for DiscoveryOracle { self.oracle.peer_set(id).await } - async fn subscribe(&mut self) -> UnboundedReceiver> { + async fn subscribe(&mut self) -> PeerSetSubscription { self.oracle.subscribe().await } } impl Manager for DiscoveryOracle { - async fn track(&mut self, id: u64, peers: R) + fn track(&mut self, id: u64, peers: R) -> Feedback where R: Into> + Send, { - self.oracle.track(id, peers).await + self.oracle.track(id, peers) } } diff --git a/types/src/reth.rs b/types/src/reth.rs index a4304c28..53687ea6 100644 --- a/types/src/reth.rs +++ b/types/src/reth.rs @@ -2,7 +2,7 @@ //! Mostly taken from: https://docs.rs/alloy/latest/alloy/node_bindings/struct.RethInstance.html use alloy_genesis::Genesis; -use rand::Rng; +use rand::RngExt as _; use std::{ borrow::Cow, ffi::OsString, @@ -282,7 +282,7 @@ impl Reth { auth_port: DEFAULT_AUTH_PORT, p2p_port: DEFAULT_P2P_PORT, block_time: None, - instance: rand::thread_rng().gen_range(1..200), + instance: rand::rng().random_range(1..200), discovery_enabled: true, program: None, ipc_path: None, diff --git a/types/src/scheme.rs b/types/src/scheme.rs index 167a38b8..16ec0ea4 100644 --- a/types/src/scheme.rs +++ b/types/src/scheme.rs @@ -3,7 +3,8 @@ use commonware_consensus::simplex::scheme::{self, Scheme}; use commonware_consensus::types::Epoch; use commonware_cryptography::bls12381::primitives::group; use commonware_cryptography::bls12381::primitives::variant::{MinPk, Variant}; -use commonware_cryptography::certificate::Provider; +use commonware_cryptography::certificate::Scheme as _; +use commonware_cryptography::certificate::{Provider, Scoped}; use commonware_cryptography::{Digest, PublicKey, Signer, ed25519}; use commonware_utils::TryCollect; use commonware_utils::ordered::BiMap; @@ -102,9 +103,15 @@ impl Provider for SummitSchemeProvider { type Scope = Epoch; type Scheme = MultisigScheme; - fn scoped(&self, scope: Self::Scope) -> Option> { + fn scoped(&self, scope: Self::Scope) -> Option> { let schemes = self.schemes.lock().unwrap(); - schemes.get(&scope).cloned() + schemes.get(&scope).cloned().map(|scheme| { + if scheme.me().is_some() { + Scoped::scheme(scheme) + } else { + Scoped::verifier(scheme) + } + }) } } @@ -174,7 +181,6 @@ mod tests { use crate::{Digest, bls12381}; use commonware_consensus::simplex::types::{Notarize, Proposal}; use commonware_consensus::types::{Round, View}; - use commonware_cryptography::certificate::Scheme as _; const NAMESPACE: &[u8] = b"test-scheme"; @@ -231,4 +237,63 @@ mod tests { assert!(scheme.me().is_none()); assert!(Notarize::sign(&scheme, sample_proposal(epoch)).is_none()); } + + #[test] + fn provider_exposes_signing_scheme_for_validator() { + let node_key = ed25519::PrivateKey::from_seed(1); + let consensus_key = bls12381::PrivateKey::from_seed(2); + let epoch = Epoch::new(3); + let transition = EpochTransition { + epoch, + validator_keys: vec![(node_key.public_key(), consensus_key.public_key())], + }; + let provider = + SummitSchemeProvider::new(private_scalar(&consensus_key), NAMESPACE.to_vec()); + let scheme = >::scheme_for_epoch( + &provider, + &transition, + ); + assert!(provider.register(epoch, scheme)); + + assert!( + Provider::scoped(&provider, epoch) + .and_then(Scoped::into_scheme) + .is_some() + ); + assert!(Provider::scheme(&provider, epoch).is_some()); + } + + #[test] + fn provider_keeps_verifier_available_without_exposing_signer() { + let node_key = ed25519::PrivateKey::from_seed(1); + let consensus_key = bls12381::PrivateKey::from_seed(2); + let epoch = Epoch::new(3); + let transition = EpochTransition { + epoch, + validator_keys: vec![(node_key.public_key(), consensus_key.public_key())], + }; + let provider = SummitSchemeProvider::verifier_only(NAMESPACE.to_vec()); + let scheme = >::scheme_for_epoch( + &provider, + &transition, + ); + assert!(provider.register(epoch, scheme)); + + assert!( + Provider::scoped(&provider, epoch) + .is_some_and(|scoped| { scoped.into_scheme().is_none() }) + ); + assert!(Provider::scheme(&provider, epoch).is_none()); + } +} + +/// Provides the certified genesis payload digest for an epoch. +/// +/// Consensus no longer queries the automaton for the epoch genesis; the +/// orchestrator fetches it through this trait when spawning an epoch's engine +/// and passes it to consensus via `simplex::Config::floor`. +pub trait EpochGenesisProvider: Send + 'static { + /// Returns the genesis payload digest for the given epoch. + fn genesis(&mut self, epoch: Epoch) + -> impl core::future::Future + Send; }