From e0d0f4120ccb4e714c8d6ab52f64b66e09f30401 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Thu, 21 May 2026 02:18:37 -0700 Subject: [PATCH 01/11] spike --- Cargo.lock | 584 +++++++---- Cargo.toml | 30 +- chain/Cargo.toml | 1 + chain/src/application.rs | 381 ++----- chain/src/engine.rs | 54 +- chain/src/indexer/backfiller/consumer.rs | 16 +- chain/src/indexer/mod.rs | 4 +- chain/src/indexer/pusher.rs | 34 +- chain/src/lib.rs | 157 ++- client/Cargo.toml | 1 + client/src/lib.rs | 2 +- deploy/Cargo.toml | 1 + deploy/src/main.rs | 9 +- follower/Cargo.toml | 2 + follower/src/application.rs | 47 +- follower/src/archive.rs | 27 +- follower/src/engine.rs | 212 +--- follower/src/feeder.rs | 83 +- follower/src/main.rs | 89 +- follower/src/resolver.rs | 1144 +++++----------------- follower/src/test_utils.rs | 1 + indexer/Cargo.toml | 1 + indexer/src/lib.rs | 2 +- indexer/src/main.rs | 3 +- inspector/Cargo.toml | 1 + inspector/src/main.rs | 6 +- inspector/src/utils.rs | 3 +- types/Cargo.toml | 1 + types/src/lib.rs | 3 +- validator/Cargo.toml | 1 + validator/src/main.rs | 41 +- 31 files changed, 1075 insertions(+), 1866 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88802911..e329d1f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,7 @@ dependencies = [ "alto-client", "alto-types", "bytes", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-consensus", @@ -64,9 +65,9 @@ dependencies = [ "prometheus-client", "rand 0.8.5", "serde", - "thiserror 2.0.18", + "thiserror", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.22", ] [[package]] @@ -78,6 +79,7 @@ dependencies = [ "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-parallel", "commonware-utils", "futures", @@ -85,7 +87,7 @@ dependencies = [ "reqwest", "rustls", "rustls-native-certs", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-tungstenite", ] @@ -101,6 +103,7 @@ dependencies = [ "commonware-consensus", "commonware-cryptography", "commonware-deployer", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-utils", @@ -108,7 +111,7 @@ dependencies = [ "serde", "serde_yaml", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.22", "uuid", ] @@ -120,10 +123,12 @@ dependencies = [ "alto-types", "bytes", "clap", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-p2p", @@ -137,10 +142,10 @@ dependencies = [ "rand 0.8.5", "serde", "serde_yaml", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.22", ] [[package]] @@ -155,6 +160,7 @@ dependencies = [ "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-parallel", "commonware-utils", "futures", @@ -164,14 +170,14 @@ dependencies = [ "rcgen", "reqwest", "rustls", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-rustls", "tokio-tungstenite", "tower", "tower-http", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.22", ] [[package]] @@ -185,14 +191,15 @@ dependencies = [ "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-parallel", "commonware-utils", "futures", "rand 0.8.5", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.22", ] [[package]] @@ -203,6 +210,7 @@ dependencies = [ "commonware-codec", "commonware-consensus", "commonware-cryptography", + "commonware-formatting", "commonware-math", "commonware-parallel", "commonware-utils", @@ -210,7 +218,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen", - "thiserror 2.0.18", + "thiserror", "wasm-bindgen", ] @@ -226,6 +234,7 @@ dependencies = [ "commonware-consensus", "commonware-cryptography", "commonware-deployer", + "commonware-formatting", "commonware-p2p", "commonware-runtime", "commonware-utils", @@ -297,6 +306,173 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ed-on-bls12-381-bandersnatch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1786b2e3832f6f0f7c8d62d5d5a282f6952a1ab99981c54cd52b6ac1d8f02df5" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-std", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -324,7 +500,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -376,9 +552,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -387,9 +563,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -491,15 +667,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -527,12 +694,6 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.1" @@ -692,28 +853,39 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "commonware-actor" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +dependencies = [ + "cfg-if", + "commonware-macros", + "commonware-runtime", + "crossbeam-queue", + "futures-util", + "parking_lot", +] + [[package]] name = "commonware-broadcast" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b67e4fed57f8d77c8d92b7bdd7512aee03d9d27ce4c0d3d8f93dd50358e9efc" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", "commonware-p2p", "commonware-runtime", "commonware-utils", - "prometheus-client", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "commonware-codec" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af69c7b97ba7225a934e29fad444f35b5d365322efaaf724e1af66c99c8c6c3" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", "cfg-if", @@ -721,14 +893,13 @@ dependencies = [ "paste", "rand 0.8.5", "rand_chacha 0.3.1", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "commonware-coding" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b7b9371af121e60f7bb3f28ff891f03ba5f9b51cc22053994007d3083c30b" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", "commonware-codec", @@ -743,21 +914,22 @@ dependencies = [ "rand_core 0.6.4", "rayon", "reed-solomon-simd", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "commonware-consensus" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdbc3361f501adce253ff92e652a997758e57c1e6298a4bc06836952b66b0c" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", "cfg-if", + "commonware-actor", "commonware-broadcast", "commonware-codec", "commonware-coding", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-p2p", @@ -768,22 +940,26 @@ dependencies = [ "commonware-utils", "futures", "pin-project", - "prometheus-client", "rand 0.8.5", "rand_core 0.6.4", "rand_distr", "rayon", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "commonware-cryptography" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf9bba8bf1ad51f54c57bee0824987bb397bedb60a09917d6a93509828a2dff" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "anyhow", + "ark-ec", + "ark-ed-on-bls12-381-bandersnatch", + "ark-ff", + "ark-r1cs-std", + "ark-relations", + "ark-serialize", "aws-lc-rs", "blake3", "blst", @@ -791,14 +967,15 @@ dependencies = [ "cfg-if", "chacha20poly1305", "commonware-codec", + "commonware-formatting", "commonware-macros", "commonware-math", "commonware-parallel", "commonware-utils", "crc-fast", "ctutils", + "curve25519-dalek", "ecdsa", - "ed25519-consensus", "getrandom 0.2.17", "num-rational", "num-traits", @@ -806,29 +983,35 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", - "sha2 0.10.9", - "thiserror 2.0.18", + "sha2", + "thiserror", "x25519-dalek", "zeroize", ] [[package]] name = "commonware-deployer" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77675f193fb1ff392dc06bd34c90dd0d087072387253aa6bec07b8ec05feedf" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "cfg-if", "commonware-macros", "serde", - "serde_yaml", +] + +[[package]] +name = "commonware-formatting" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +dependencies = [ + "commonware-macros", + "const-hex", ] [[package]] name = "commonware-macros" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c8da60f9fe8357927327729fa7838b44555f4c578e4b07c12a2964664230a98" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "commonware-macros-impl", "tokio", @@ -836,9 +1019,8 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c4e1e7a838cff536ffbe38f441c555eee99322d755aed3e06af9407c127fd3" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -849,9 +1031,8 @@ dependencies = [ [[package]] name = "commonware-math" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a44f73b02286843967888c7aa750a7a91a5cd3ac8b9bc48cd1d81331836bbb" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", "commonware-codec", @@ -863,10 +1044,10 @@ dependencies = [ [[package]] name = "commonware-p2p" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b493b3ff98ef12a54162d9eae90175a6ff56e12df90f7b3381eff3cea84a8deb" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -880,19 +1061,17 @@ dependencies = [ "num-integer", "num-rational", "num-traits", - "prometheus-client", "rand 0.8.5", "rand_core 0.6.4", "rand_distr", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "commonware-parallel" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a590a49ee5d5389a2b966008b3367275772da7f336719fdb0f046ad257f6849" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "cfg-if", "commonware-macros", @@ -901,11 +1080,11 @@ dependencies = [ [[package]] name = "commonware-resolver" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e386bcb3a39c2ef1315df245cee346a8d9143961defebeabde518e189f6f54" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", + "commonware-actor", "commonware-codec", "commonware-cryptography", "commonware-macros", @@ -914,28 +1093,28 @@ dependencies = [ "commonware-stream", "commonware-utils", "futures", - "prometheus-client", "rand 0.8.5", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "commonware-runtime" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06938675df074ca7749a6d531c4706bcc339e842105e73b03a76d45bd860b349" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "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", "governor", @@ -947,20 +1126,30 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "sha2 0.10.9", + "sha2", "sysinfo", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "tracing-opentelemetry", - "tracing-subscriber", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "commonware-runtime-macros" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "commonware-storage" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3be226adc2994ba3b8181f9388855c7f62c5b041ebd8c6a2956651bd8cf95fe" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "ahash", "anyhow", @@ -968,48 +1157,47 @@ dependencies = [ "cfg-if", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-parallel", "commonware-runtime", "commonware-utils", "futures", "futures-util", - "prometheus-client", - "rayon", - "thiserror 2.0.18", + "thiserror", "tracing", "zstd", ] [[package]] name = "commonware-stream" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24852a37e87406ba53fc95a53574016362cc9f527faf68d298fb1a3f50159950" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "chacha20poly1305", "commonware-codec", "commonware-cryptography", + "commonware-formatting", "commonware-macros", "commonware-runtime", "commonware-utils", "futures", "rand 0.8.5", "rand_core 0.6.4", - "thiserror 2.0.18", + "thiserror", "x25519-dalek", "zeroize", ] [[package]] name = "commonware-utils" -version = "2026.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192eb390e3e0277770982e5e63de72f3c116c810873f3250318cae9ff0206560" +version = "2026.4.0" +source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" dependencies = [ "bytes", "cfg-if", "commonware-codec", + "commonware-formatting", "commonware-macros", "futures", "getrandom 0.2.17", @@ -1021,11 +1209,23 @@ dependencies = [ "parking_lot", "pin-project", "rand 0.8.5", - "thiserror 2.0.18", + "thiserror", "tokio", "zeroize", ] +[[package]] +name = "const-hex" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1069,7 +1269,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "digest 0.10.7", + "digest", "spin", ] @@ -1187,6 +1387,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", + "digest", "fiat-crypto", "rustc_version", "subtle", @@ -1204,19 +1405,6 @@ dependencies = [ "syn", ] -[[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 = "dashmap" version = "6.1.0" @@ -1270,22 +1458,13 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", "subtle", @@ -1321,7 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest 0.10.7", + "digest", "elliptic-curve", "rfc6979", "signature", @@ -1329,17 +1508,15 @@ dependencies = [ ] [[package]] -name = "ed25519-consensus" -version = "2.1.0" +name = "educe" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "thiserror 1.0.69", - "zeroize", + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1356,7 +1533,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "digest", "ff", "generic-array", "group", @@ -1367,6 +1544,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1671,6 +1868,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", "foldhash 0.1.5", ] @@ -1697,19 +1895,13 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -2269,7 +2461,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror", "tracing", ] @@ -2299,7 +2491,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.18", + "thiserror", "tracing", ] @@ -2328,7 +2520,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.2", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", ] @@ -2342,7 +2534,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -2567,6 +2759,21 @@ dependencies = [ "syn", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "prost" version = "0.14.3" @@ -2619,7 +2826,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -2640,7 +2847,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -2750,6 +2957,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -3175,20 +3391,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest", ] [[package]] @@ -3199,7 +3402,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -3233,7 +3436,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] @@ -3302,12 +3505,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 = "syn" version = "2.0.117" @@ -3353,33 +3550,13 @@ dependencies = [ "windows", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -3749,7 +3926,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", - "tracing-subscriber", + "tracing-subscriber 0.3.22", "web-time", ] @@ -3763,6 +3940,15 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -3805,7 +3991,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror", "utf-8", ] @@ -3815,6 +4001,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4506,7 +4698,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 647bc73b..0b41b7ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,20 +20,22 @@ license = "MIT OR Apache-2.0" alto-chain = { version = "2026.3.0", path = "chain" } alto-client = { version = "2026.3.0", path = "client" } alto-types = { version = "2026.3.0", path = "types" } -commonware-broadcast = "2026.3.0" -commonware-codec = "2026.3.0" -commonware-consensus = "2026.3.0" -commonware-cryptography = "2026.3.0" -commonware-deployer = { version = "2026.3.0", default-features = false } -commonware-macros = "2026.3.0" -commonware-p2p = "2026.3.0" -commonware-resolver = "2026.3.0" -commonware-runtime = "2026.3.0" -commonware-storage = "2026.3.0" -commonware-stream = "2026.3.0" -commonware-utils = "2026.3.0" -commonware-math = "2026.3.0" -commonware-parallel = "2026.3.0" +commonware-actor = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-consensus = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-cryptography = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-deployer = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224", default-features = false } +commonware-formatting = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-macros = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-p2p = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-resolver = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-runtime = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-storage = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-stream = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-utils = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-math = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-parallel = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } thiserror = "2.0.12" bytes = "1.7.1" rand = "0.8.5" diff --git a/chain/Cargo.toml b/chain/Cargo.toml index e820d8f2..9534c445 100644 --- a/chain/Cargo.toml +++ b/chain/Cargo.toml @@ -13,6 +13,7 @@ documentation = "https://docs.rs/alto-chain" [dependencies] alto-types = { workspace = true } alto-client = { workspace = true } +commonware-actor = { workspace = true } commonware-broadcast = { workspace = true } commonware-codec = { workspace = true } commonware-consensus = { workspace = true, features = ["mocks"] } diff --git a/chain/src/application.rs b/chain/src/application.rs index ec07479e..a89f5d75 100644 --- a/chain/src/application.rs +++ b/chain/src/application.rs @@ -1,10 +1,8 @@ use crate::indexer; use alto_types::{Block, Context, Scheme, EPOCH}; +use commonware_actor::Feedback; use commonware_consensus::{ - marshal::{ - ancestry::{AncestorStream, BlockProvider}, - Update, - }, + marshal::{ancestry::Ancestry, Update}, types::{Height, Round, View}, Heightable, Reporter, }; @@ -28,38 +26,43 @@ const GENESIS: &[u8] = b"commonware is neat"; /// timestamp to ensure consistent application of block validity rules. const MAX_BLOCK_TIMESTAMP_MS: u64 = 7_258_118_400_000; -#[derive(Clone)] pub struct Application { - genesis: Arc, - backfiller: Option>, + context: Arc, + backfiller: Option>>, +} + +impl Clone for Application { + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + backfiller: self.backfiller.clone(), + } + } } impl Application { - pub fn new() -> Self { + pub fn genesis() -> Block { let genesis_context = Context { round: Round::new(EPOCH, View::zero()), leader: ed25519::PrivateKey::from_seed(0).public_key(), parent: (View::zero(), sha256::Digest::EMPTY), }; - let genesis = Block::new(genesis_context, Sha256::hash(GENESIS), Height::zero(), 0); + Block::new(genesis_context, Sha256::hash(GENESIS), Height::zero(), 0) + } + + pub fn new(context: E) -> Self { Self { - genesis: Arc::new(genesis), + context: Arc::new(context), backfiller: None, } } pub(crate) fn with_backfiller(mut self, backfiller: indexer::Producer) -> Self { - self.backfiller = Some(backfiller); + self.backfiller = Some(Arc::new(backfiller)); self } } -impl Default for Application { - fn default() -> Self { - Self::new() - } -} - impl commonware_consensus::Application for Application where E: Rng + Spawner + Metrics + Clock + Storage, @@ -68,14 +71,10 @@ where type Context = Context; type Block = Block; - async fn genesis(&mut self) -> Self::Block { - self.genesis.as_ref().clone() - } - - async fn propose>( + async fn propose( &mut self, (runtime_context, context): (E, Self::Context), - mut ancestry: AncestorStream, + mut ancestry: impl Ancestry, ) -> Option { let parent = ancestry.next().await?; @@ -99,16 +98,11 @@ where current, )) } -} -impl commonware_consensus::VerifyingApplication for Application -where - E: Rng + Spawner + Metrics + Clock + Storage, -{ - async fn verify>( + async fn verify( &mut self, - (runtime_context, _): (E, Context), - mut ancestry: AncestorStream, + (runtime_context, _): (E, Self::Context), + mut ancestry: impl Ancestry, ) -> bool { let Some(block) = ancestry.next().await else { return false; @@ -133,53 +127,36 @@ where } } -impl Reporter for Application { +impl Reporter for Application { type Activity = Update; - async fn report(&mut self, activity: Self::Activity) { + fn report(&mut self, activity: Self::Activity) -> Feedback { if let Update::Block(block, ack_rx) = activity { - // Cache the finalized block in memory and enqueue its digest - // before acking so the consumer can recover it across restarts. - if let Some(backfiller) = &self.backfiller { - backfiller.record(&block).await; + if let Some(backfiller) = self.backfiller.clone() { + self.context + .child("backfill_record") + .spawn(move |_| async move { + // Cache the finalized block in memory and enqueue its digest + // before acking so the consumer can recover it across restarts. + backfiller.record(&block).await; + + info!(height = %block.height(), "finalized block"); + ack_rx.acknowledge(); + }); + } else { + info!(height = %block.height(), "finalized block"); + ack_rx.acknowledge(); } - - // Acknowledge the block. - info!(height = %block.height(), "finalized block"); - ack_rx.acknowledge(); } + Feedback::Ok } } #[cfg(test)] mod tests { use super::*; - use commonware_consensus::{ - marshal::{ - core::{Actor as MarshalActor, Buffer, Mailbox}, - resolver::handler, - standard::Standard, - Config as MarshalConfig, - }, - simplex::scheme::bls12381_threshold::vrf as bls12381_threshold, - }; - use commonware_cryptography::{ - bls12381::primitives::variant::MinSig, - certificate::{mocks::Fixture, ConstantProvider}, - }; - use commonware_parallel::Sequential; - use commonware_resolver::Resolver; - use commonware_runtime::{buffer::paged::CacheRef, deterministic, Runner as _}; - use commonware_storage::archive::immutable; - use commonware_utils::{ - channel::{mpsc, oneshot}, - vec::NonEmptyVec, - NZUsize, NZU16, NZU64, - }; - - const TEST_NAMESPACE: &[u8] = b"application-test"; - const TEST_PAGE_SIZE: u16 = 1024; - const TEST_PAGE_CACHE_SIZE: usize = 10; + use commonware_runtime::{deterministic, Runner as _, Supervisor as _}; + use futures::stream; fn test_context(view: u64, parent: (View, sha256::Digest)) -> Context { Context { @@ -189,227 +166,44 @@ mod tests { } } - #[derive(Clone)] - struct NoopBuffer; - - impl Buffer> for NoopBuffer { - type CachedBlock = Block; - - async fn find_by_digest(&self, _: sha256::Digest) -> Option { - None - } - - async fn find_by_commitment(&self, _: sha256::Digest) -> Option { - None - } - - async fn subscribe_by_digest( - &self, - _: sha256::Digest, - ) -> oneshot::Receiver { - let (_tx, rx) = oneshot::channel(); - rx - } - - async fn subscribe_by_commitment( - &self, - _: sha256::Digest, - ) -> oneshot::Receiver { - let (_tx, rx) = oneshot::channel(); - rx - } - - async fn finalized(&self, _: sha256::Digest) {} - - async fn proposed(&self, _: Round, _: Block) {} - } - - #[derive(Clone)] - struct NoopResolver; - - impl Resolver for NoopResolver { - type Key = handler::Request; - type PublicKey = alto_types::PublicKey; - - async fn fetch(&mut self, _: Self::Key) {} - - async fn fetch_all(&mut self, _: Vec) {} - - async fn fetch_targeted(&mut self, _: Self::Key, _: NonEmptyVec) {} - - async fn fetch_all_targeted(&mut self, _: Vec<(Self::Key, NonEmptyVec)>) {} - - async fn cancel(&mut self, _: Self::Key) {} - - async fn clear(&mut self) {} - - async fn retain(&mut self, _: impl Fn(&Self::Key) -> bool + Send + 'static) {} - } - - fn test_archive_config( - prefix: &str, - label: &str, - page_cache: CacheRef, - ) -> immutable::Config<()> { - immutable::Config { - metadata_partition: format!("{prefix}-{label}-metadata"), - freezer_table_partition: format!("{prefix}-{label}-freezer-table"), - freezer_table_initial_size: 64, - freezer_table_resize_frequency: 10, - freezer_table_resize_chunk_size: 10, - freezer_key_partition: format!("{prefix}-{label}-key"), - freezer_key_page_cache: page_cache, - freezer_value_partition: format!("{prefix}-{label}-value"), - freezer_value_target_size: 1024, - freezer_value_compression: None, - ordinal_partition: format!("{prefix}-{label}-ordinal"), - items_per_section: NZU64!(10), - codec_config: (), - replay_buffer: NZUsize!(1024), - freezer_key_write_buffer: NZUsize!(1024), - freezer_value_write_buffer: NZUsize!(1024), - ordinal_write_buffer: NZUsize!(1024), - } - } - - async fn init_mailbox( - context: deterministic::Context, - scheme: Scheme, - ) -> ( - Mailbox>, - mpsc::Sender>, - ) { - let page_cache = CacheRef::from_pooler( - &context, - NZU16!(TEST_PAGE_SIZE), - NZUsize!(TEST_PAGE_CACHE_SIZE), - ); - let partition_prefix = "application-test"; - - let finalizations_by_height = immutable::Archive::init( - context.with_label("finalizations_by_height"), - test_archive_config( - partition_prefix, - "finalizations-by-height", - page_cache.clone(), - ), - ) - .await - .expect("failed to initialize finalizations archive"); - let finalized_blocks = immutable::Archive::init( - context.with_label("finalized_blocks"), - test_archive_config(partition_prefix, "finalized-blocks", page_cache.clone()), - ) - .await - .expect("failed to initialize finalized blocks archive"); - - let (actor, mailbox, _) = MarshalActor::init( - context.clone(), - finalizations_by_height, - finalized_blocks, - MarshalConfig { - provider: ConstantProvider::new(scheme), - epocher: commonware_consensus::types::FixedEpocher::new(alto_types::EPOCH_LENGTH), - partition_prefix: partition_prefix.to_string(), - mailbox_size: 16, - view_retention_timeout: commonware_consensus::types::ViewDelta::new(4), - prunable_items_per_section: NZU64!(10), - replay_buffer: NZUsize!(1024), - key_write_buffer: NZUsize!(1024), - value_write_buffer: NZUsize!(1024), - block_codec_config: (), - max_repair: NZUsize!(4), - max_pending_acks: NZUsize!(4), - page_cache, - strategy: Sequential, - }, - ) - .await; - let (resolver_tx, resolver_rx) = mpsc::channel::>(1); - actor.start( - Application::::default(), - NoopBuffer, - (resolver_rx, NoopResolver), - ); - - (mailbox, resolver_tx) - } - async fn setup_application_test( - context: &mut deterministic::Context, - ) -> ( - Application, - Mailbox>, - mpsc::Sender>, - ) { - let Fixture { schemes, .. } = - bls12381_threshold::fixture::(context, TEST_NAMESPACE, 1); - let (mailbox, resolver_tx) = init_mailbox(context.clone(), schemes[0].clone()).await; - ( - Application::::default(), - mailbox, - resolver_tx, - ) - } - - async fn cache_block( - context: &deterministic::Context, - mailbox: &Mailbox>, - block: &Block, - ) { - mailbox.verified(block.context.round, block.clone()).await; - loop { - if mailbox.get_block(&block.digest()).await.is_some() { - break; - } - context.sleep(Duration::from_millis(1)).await; - } + context: deterministic::Context, + ) -> Application { + Application::new(context) } async fn verify_block( - context: &deterministic::Context, + context: deterministic::Context, application: &mut Application, - mailbox: &Mailbox>, block: &Block, + parent: &Block, ) -> bool { - let ancestry = mailbox - .ancestry((Some(block.context.round), block.digest())) - .await - .expect("expected cached ancestry"); - commonware_consensus::VerifyingApplication::verify( + let ancestry = stream::iter([block.clone(), parent.clone()]); + commonware_consensus::Application::verify( application, - (context.clone(), block.context.clone()), + (context, block.context.clone()), ancestry, ) .await } async fn propose_child( - context: &deterministic::Context, + context: deterministic::Context, application: &mut Application, - mailbox: &Mailbox>, child_context: Context, parent: &Block, ) -> Block { - let ancestry = mailbox - .ancestry((Some(parent.context.round), parent.digest())) + let ancestry = stream::iter([parent.clone()]); + commonware_consensus::Application::propose(application, (context, child_context), ancestry) .await - .expect("expected cached ancestry"); - commonware_consensus::Application::propose( - application, - (context.clone(), child_context), - ancestry, - ) - .await - .expect("expected proposal") + .expect("expected proposal") } #[test] fn verify_waits_for_far_future_block_timestamp() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; let now = context.current().epoch_millis(); let parent = Block::new( @@ -425,11 +219,8 @@ mod tests { now + 5_000, ); - cache_block(&context, &mailbox, &parent).await; - cache_block(&context, &mailbox, &block).await; - let start = context.current(); - assert!(verify_block(&context, &mut application, &mailbox, &block).await); + assert!(verify_block(context.child("verify"), &mut application, &block, &parent).await); let finished = context.current(); assert!(finished.duration_since(start).unwrap() > Duration::ZERO); assert!(finished.epoch_millis() >= block.timestamp); @@ -439,9 +230,8 @@ mod tests { #[test] fn verify_rejects_equal_parent_timestamp() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; let now = context.current().epoch_millis(); let parent = Block::new( @@ -457,19 +247,17 @@ mod tests { now, ); - cache_block(&context, &mailbox, &parent).await; - cache_block(&context, &mailbox, &block).await; - - assert!(!verify_block(&context, &mut application, &mailbox, &block).await); + assert!( + !verify_block(context.child("verify"), &mut application, &block, &parent).await + ); }); } #[test] fn verify_returns_immediately_for_mature_block_timestamp() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; context.sleep(Duration::from_millis(10)).await; let now = context.current().epoch_millis(); @@ -486,11 +274,8 @@ mod tests { now, ); - cache_block(&context, &mailbox, &parent).await; - cache_block(&context, &mailbox, &block).await; - let start = context.current(); - assert!(verify_block(&context, &mut application, &mailbox, &block).await); + assert!(verify_block(context.child("verify"), &mut application, &block, &parent).await); let finished = context.current(); assert!(finished.duration_since(start).unwrap() < Duration::from_millis(10)); }); @@ -499,9 +284,8 @@ mod tests { #[test] fn propose_uses_parent_timestamp_plus_one_when_clock_is_behind() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; let now = context.current().epoch_millis(); let parent = Block::new( @@ -510,12 +294,9 @@ mod tests { Height::new(1), now + 5_000, ); - cache_block(&context, &mailbox, &parent).await; - let proposal = propose_child( - &context, + context.child("propose"), &mut application, - &mailbox, test_context(2, (View::new(1), parent.digest())), &parent, ) @@ -530,9 +311,8 @@ mod tests { #[test] fn verify_rejects_timestamp_above_maximum() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; let now = context.current().epoch_millis(); let parent = Block::new( @@ -550,10 +330,9 @@ mod tests { MAX_BLOCK_TIMESTAMP_MS + 1, ); - cache_block(&context, &mailbox, &parent).await; - cache_block(&context, &mailbox, &block).await; - - assert!(!verify_block(&context, &mut application, &mailbox, &block).await); + assert!( + !verify_block(context.child("verify"), &mut application, &block, &parent).await + ); }); } @@ -561,9 +340,8 @@ mod tests { #[should_panic(expected = "proposed timestamp exceeded maximum")] fn propose_panics_when_parent_timestamp_is_maximum() { let runner = deterministic::Runner::default(); - runner.start(|mut context| async move { - let (mut application, mailbox, _resolver_tx) = - setup_application_test(&mut context).await; + runner.start(|context| async move { + let mut application = setup_application_test(context.child("application")).await; let parent = Block::new( test_context(1, (View::zero(), sha256::Digest::EMPTY)), @@ -573,12 +351,9 @@ mod tests { // require `parent.timestamp + 1`, which must be rejected. MAX_BLOCK_TIMESTAMP_MS, ); - cache_block(&context, &mailbox, &parent).await; - let _ = propose_child( - &context, + context.child("propose"), &mut application, - &mailbox, test_context(2, (View::new(1), parent.digest())), &parent, ) diff --git a/chain/src/engine.rs b/chain/src/engine.rs index 618ec725..be41f285 100644 --- a/chain/src/engine.rs +++ b/chain/src/engine.rs @@ -20,6 +20,7 @@ use commonware_cryptography::{ certificate::{ConstantProvider, Scheme as _}, ed25519::PublicKey, sha256::Digest, + Digestible, }; use commonware_p2p::{Blocker, Provider, Receiver, Sender}; use commonware_parallel::Strategy; @@ -29,7 +30,6 @@ use commonware_runtime::{ Spawner, Storage, ThreadPooler, }; use commonware_storage::{archive::immutable, queue}; -use commonware_utils::channel::mpsc; use commonware_utils::{ordered::Set, NZU16}; use commonware_utils::{NZUsize, NZU64}; use futures::future::try_join_all; @@ -145,10 +145,10 @@ where pub async fn new(context: E, cfg: Config) -> Self { // Create the buffer let (buffer, buffer_mailbox) = buffered::Engine::new( - context.with_label("buffer"), + context.child("buffer"), buffered::Config { public_key: cfg.me, - mailbox_size: cfg.mailbox_size, + mailbox_size: NZUsize!(cfg.mailbox_size), deque_size: cfg.deque_size, priority: true, codec_config: (), @@ -162,7 +162,7 @@ where // 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", @@ -205,7 +205,7 @@ where // 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", cfg.partition_prefix), freezer_table_partition: format!( @@ -244,20 +244,23 @@ where .expect("failed to create scheme"); let provider = ConstantProvider::new(scheme.clone()); let epocher = FixedEpocher::new(EPOCH_LENGTH); + let genesis = Application::::genesis(); + let genesis_digest = genesis.digest(); let (marshal, marshal_mailbox, _) = MarshalActor::init( - context.with_label("marshal"), + context.child("marshal"), finalizations_by_height, finalized_blocks, marshal::Config { provider, epocher: epocher.clone(), partition_prefix: cfg.partition_prefix.clone(), - mailbox_size: cfg.mailbox_size, + mailbox_size: NZUsize!(cfg.mailbox_size), view_retention_timeout: ViewDelta::new( cfg.activity_timeout .get() .saturating_mul(SYNCER_ACTIVITY_TIMEOUT_MULTIPLIER), ), + start: marshal::Start::Genesis(genesis), prunable_items_per_section: PRUNABLE_ITEMS_PER_SECTION, replay_buffer: REPLAY_BUFFER, key_write_buffer: WRITE_BUFFER, @@ -276,7 +279,7 @@ where // restarts. let (app, pusher, consumer) = if let Some(indexer) = cfg.indexer { let queue = queue::shared::init( - context.with_label("queue"), + context.child("queue"), queue::Config { partition: format!("{}-finalized-queue", cfg.partition_prefix), items_per_section: QUEUE_ITEMS_PER_SECTION, @@ -289,7 +292,7 @@ where .await .expect("failed to initialize finalized queue"); let indexer = indexer::Indexer::new( - context.with_label("indexer"), + context.child("indexer"), indexer, marshal_mailbox.clone(), queue, @@ -298,15 +301,15 @@ where ) .await; let (producer, pusher, consumer) = indexer.split(); - let app = Application::new().with_backfiller(producer); + let app = Application::new(context.child("application")).with_backfiller(producer); (app, Some(pusher), Some(consumer)) } else { - (Application::new(), None, None) + (Application::new(context.child("application")), None, None) }; // Create the application let marshaled = Marshaled::new( - context.with_label("marshaled"), + context.child("marshaled"), app, marshal_mailbox.clone(), epocher, @@ -317,7 +320,7 @@ where // Create the consensus engine let consensus = Consensus::new( - context.with_label("consensus"), + context.child("consensus"), simplex::Config { epoch: EPOCH, scheme, @@ -325,14 +328,16 @@ where relay: marshaled.clone(), reporter, partition: format!("{}-consensus", cfg.partition_prefix), - mailbox_size: cfg.mailbox_size, + mailbox_size: NZUsize!(cfg.mailbox_size), + floor: simplex::Floor::Genesis(genesis_digest), leader_timeout: cfg.leader_timeout, certification_timeout: cfg.certification_timeout, timeout_retry: cfg.nullify_retry, fetch_timeout: cfg.fetch_timeout, activity_timeout: cfg.activity_timeout, skip_timeout: cfg.skip_timeout, - fetch_concurrent: cfg.fetch_concurrent, + fetch_concurrent: NZUsize!(cfg.fetch_concurrent), + forwarding: simplex::ForwardingPolicy::Disabled, replay_buffer: REPLAY_BUFFER, write_buffer: WRITE_BUFFER, blocker: cfg.blocker, @@ -377,14 +382,17 @@ where impl Receiver, ), marshal: ( - mpsc::Receiver>, - impl Resolver, PublicKey = PublicKey>, + handler::Receiver, + impl Resolver< + Key = handler::Key, + Subscriber = handler::Annotation, + PublicKey = PublicKey, + >, ), ) -> Handle<()> { spawn_cell!( self.context, self.run(pending, recovered, resolver, broadcast, marshal) - .await ) } @@ -408,8 +416,12 @@ where impl Receiver, ), marshal: ( - mpsc::Receiver>, - impl Resolver, PublicKey = PublicKey>, + handler::Receiver, + impl Resolver< + Key = handler::Key, + Subscriber = handler::Annotation, + PublicKey = PublicKey, + >, ), ) { // Start the buffer @@ -418,7 +430,7 @@ where // Start marshal let marshal_handle = self .marshal - .start(self.marshaled, self.buffer_mailbox, marshal); + .start(self.marshaled, Some(self.buffer_mailbox), marshal); // Start draining queued block uploads before consensus so recovered work // resumes immediately on startup. diff --git a/chain/src/indexer/backfiller/consumer.rs b/chain/src/indexer/backfiller/consumer.rs index cdddc8f5..cb8c9053 100644 --- a/chain/src/indexer/backfiller/consumer.rs +++ b/chain/src/indexer/backfiller/consumer.rs @@ -7,9 +7,7 @@ use commonware_consensus::marshal::{ use commonware_cryptography::sha256::Digest; use commonware_macros::select_loop; use commonware_runtime::{ - spawn_cell, - telemetry::metrics::status::{self, CounterExt}, - Clock, ContextCell, Handle, Metrics, Spawner, Storage, + spawn_cell, telemetry::metrics::status, Clock, ContextCell, Handle, Metrics, Spawner, Storage, }; use commonware_storage::queue; use commonware_utils::futures::{OptionFuture, Pool}; @@ -53,11 +51,10 @@ impl Consumer { max_active: NonZeroUsize, retry: Duration, ) -> Self { - let upload_results = status::Counter::default(); - context.register( + let upload_results = context.register( "uploads", "Total number of finalized block upload attempt outcomes by status", - upload_results.clone(), + status::Raw::default(), ); let (writer, reader) = backfiller; Self { @@ -76,7 +73,7 @@ impl Consumer { /// Start the consumer loop that reads from the queue and uploads blocks. pub fn start(mut self) -> Handle<()> { - spawn_cell!(self.context, self.run().await) + spawn_cell!(self.context, self.run()) } async fn run(mut self) { @@ -158,10 +155,9 @@ impl Consumer { self.active.push({ let context = self .context - .with_label("upload") + .child("upload") .with_attribute("digest", digest) - .with_attribute("height", height) - .into_present(); + .with_attribute("height", height); let client = self.client.clone(); let marshal = self.marshal.clone(); let upload_results = self.upload_results.clone(); diff --git a/chain/src/indexer/mod.rs b/chain/src/indexer/mod.rs index e39d743b..8822b736 100644 --- a/chain/src/indexer/mod.rs +++ b/chain/src/indexer/mod.rs @@ -105,7 +105,7 @@ impl Indexer { ) -> Self { let uploads: SharedState = Arc::new(Mutex::new(State::new())); let pusher = Pusher::new( - context.with_label("pusher"), + context.child("pusher"), client.clone(), marshal.clone(), uploads.clone(), @@ -113,7 +113,7 @@ impl Indexer { let (writer, reader) = backfiller; let producer = Producer::new(uploads.clone(), writer.clone()); let consumer = Consumer::new( - context.with_label("consumer"), + context.child("consumer"), client, marshal, uploads, diff --git a/chain/src/indexer/pusher.rs b/chain/src/indexer/pusher.rs index 9529b3d2..a713c29f 100644 --- a/chain/src/indexer/pusher.rs +++ b/chain/src/indexer/pusher.rs @@ -1,13 +1,14 @@ use super::{Client, SharedState}; use alto_types::{Activity, Block, Scheme, Seed, Seedable}; +use commonware_actor::Feedback; use commonware_consensus::{ - marshal::{core::Mailbox as MarshalMailbox, standard::Standard}, + marshal::{core::DigestFallback, core::Mailbox as MarshalMailbox, standard::Standard}, types::{Round, View}, Reporter, Viewable, }; use commonware_cryptography::sha256::Digest; use commonware_runtime::{Metrics, Spawner}; -use std::future::Future; +use std::{future::Future, sync::Arc}; use tracing::{debug, warn}; /// Uploads live seeds and certificate-bearing objects to the indexer. @@ -17,14 +18,24 @@ use tracing::{debug, warn}; /// block in shared state, and uploads the certificate-bearing object. The /// shared state lets the backfiller reuse blocks and back off /// when the live path is already handling a digest. -#[derive(Clone)] pub(crate) struct Pusher { - context: E, + context: Arc, client: C, marshal: MarshalMailbox>, uploads: SharedState, } +impl Clone for Pusher { + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + client: self.client.clone(), + marshal: self.marshal.clone(), + uploads: self.uploads.clone(), + } + } +} + impl Pusher { /// Create a new [Pusher]. pub(crate) fn new( @@ -34,7 +45,7 @@ impl Pusher { uploads: SharedState, ) -> Self { Self { - context, + context: Arc::new(context), client, marshal, uploads, @@ -81,8 +92,8 @@ impl Drop for CertificateUploadGuard { } impl Pusher { - fn spawn_seed_upload(&self, label: &str, seed: Seed, view: View) { - self.context.with_label(label).spawn({ + fn spawn_seed_upload(&self, label: &'static str, seed: Seed, view: View) { + self.context.child(label).spawn({ let client = self.client.clone(); move |_| async move { if let Err(e) = client.seed_upload(seed).await { @@ -105,7 +116,7 @@ impl Pusher { F: FnOnce(C, Block) -> Fut + Send + 'static, Fut: Future> + Send, { - self.context.with_label(label).spawn({ + self.context.child(label).spawn({ let client = self.client.clone(); let marshal = self.marshal.clone(); let uploads = self.uploads.clone(); @@ -115,7 +126,9 @@ impl Pusher { // while the block is still being fetched. let mut guard = CertificateUploadGuard::new(uploads, digest); - let block = marshal.subscribe_by_digest(Some(round), digest).await.await; + let block = marshal + .subscribe_by_digest(digest, DigestFallback::FetchByRound { round }) + .await; let Ok(block) = block else { warn!(%view, "subscription for block cancelled"); return; @@ -138,7 +151,7 @@ impl Pusher { impl Reporter for Pusher { type Activity = Activity; - async fn report(&mut self, activity: Self::Activity) { + fn report(&mut self, activity: Self::Activity) -> Feedback { match activity { Activity::Notarization(notarization) => { let view = notarization.view(); @@ -172,5 +185,6 @@ impl Reporter for Pusher { } _ => {} } + Feedback::Ok } } diff --git a/chain/src/lib.rs b/chain/src/lib.rs index 36fc4491..d37f6cc6 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -77,9 +77,9 @@ mod tests { use commonware_parallel::Sequential; use commonware_runtime::{ deterministic::{self, Runner}, - Clock, Metrics, Runner as _, Spawner, + Clock, Metrics, Runner as _, Spawner, Supervisor as _, }; - use commonware_utils::{channel::oneshot, ordered::Set, NZU32}; + use commonware_utils::{channel::oneshot, ordered::Set, NZUsize, NZU32}; use engine::{Config, Engine}; use governor::Quota; use indexer::mocks; @@ -101,8 +101,7 @@ mod tests { ) -> HashMap { oracle .manager() - .track(0, Set::from_iter_dedup(validators.iter().cloned())) - .await; + .track(0, Set::from_iter_dedup(validators.iter().cloned())); let mut registrations = HashMap::new(); for validator in validators.iter() { let oracle = oracle.control(validator.clone()); @@ -174,17 +173,7 @@ mod tests { metrics .lines() .filter_map(|line| { - if !line.starts_with("validator_") { - return None; - } - let mut parts = line.split_whitespace(); - let metric = parts.next()?; - let value = parts.next()?; - let (name, labels) = metric - .split_once('{') - .map_or((metric, None), |(name, labels)| { - (name, Some(labels.trim_end_matches('}'))) - }); + let (name, labels, value) = validator_metric_sample(line)?; if !name.ends_with(suffix) { return None; } @@ -202,6 +191,25 @@ mod tests { .sum() } + fn validator_metric_sample(line: &str) -> Option<(&str, Option<&str>, &str)> { + let line = line.trim(); + if line.starts_with('#') { + return None; + } + let mut parts = line.split_whitespace(); + let metric = parts.next()?; + let value = parts.next()?; + let (name, labels) = metric + .split_once('{') + .map_or((metric, None), |(name, labels)| { + (name, Some(labels.trim_end_matches('}'))) + }); + if !name.starts_with("validator_") { + return None; + } + Some((name, labels, value)) + } + fn queue_outstanding(metrics: &str) -> i64 { sum_validator_metric::(metrics, "_queue_tip", None) - sum_validator_metric::(metrics, "_queue_floor", None) @@ -318,13 +326,13 @@ mod tests { indexer: cfg.indexer, strategy: Sequential, }; - let validator_context = context.with_label(&uid); + let validator_context = context.child("validator").with_attribute("id", &uid); let (pending, recovered, resolver, broadcast, backfill) = registration; let marshal_resolver_cfg = marshal::resolver::p2p::Config { public_key: public_key.clone(), peer_provider: oracle.manager(), blocker: oracle.control(public_key.clone()), - mailbox_size: 1024, + mailbox_size: NZUsize!(1024), initial: Duration::from_secs(1), timeout: Duration::from_secs(2), fetch_retry_timeout: Duration::from_millis(100), @@ -332,11 +340,11 @@ mod tests { priority_responses: false, }; let marshal_resolver = marshal::resolver::p2p::init( - &validator_context.with_label("backfill"), + validator_context.child("backfill"), marshal_resolver_cfg, backfill, ); - let engine = Engine::new(validator_context.with_label("engine"), config).await; + let engine = Engine::new(validator_context.child("engine"), config).await; engine.start(pending, recovered, resolver, broadcast, marshal_resolver); } @@ -345,12 +353,9 @@ mod tests { let metrics = context.encode(); let mut success = false; for line in metrics.lines() { - if !line.starts_with("validator_") { + let Some((metric, _, value)) = validator_metric_sample(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(); assert_eq!(value, 0); @@ -375,11 +380,11 @@ mod tests { let executor = Runner::from(cfg); executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -458,11 +463,11 @@ mod tests { let executor = Runner::timed(Duration::from_secs(30)); executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -558,11 +563,11 @@ mod tests { let f = |mut context: deterministic::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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); @@ -603,48 +608,40 @@ mod tests { .await; } - let poller = context - .with_label("metrics") - .spawn(move |context| async move { - loop { - let metrics = context.encode(); - - // Iterate over all lines - let mut success = false; - for line in metrics.lines() { - // Ensure it is a metrics line - if !line.starts_with("validator_") { - 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(); - assert_eq!(value, 0); - } + let poller = context.child("metrics").spawn(move |context| async move { + loop { + let metrics = context.encode(); + + // Iterate over all lines + let mut success = false; + for line in metrics.lines() { + let Some((metric, _, value)) = validator_metric_sample(line) else { + continue; + }; + + // If ends with peers_blocked, ensure it is zero + if metric.ends_with("_peers_blocked") { + let value = value.parse::().unwrap(); + assert_eq!(value, 0); + } - // If ends with contiguous_height, ensure it is at least required_container - if metric.ends_with("_marshal_processed_height") { - let value = value.parse::().unwrap(); - if value >= required_container { - success = true; - break; - } + // If ends with contiguous_height, ensure it is at least required_container + if metric.ends_with("_marshal_processed_height") { + let value = value.parse::().unwrap(); + if value >= required_container { + success = true; + break; } } - if success { - break; - } - - // Still waiting for all validators to complete - context.sleep(Duration::from_millis(10)).await; } - }); + if success { + break; + } + + // Still waiting for all validators to complete + context.sleep(Duration::from_millis(10)).await; + } + }); // Exit at random points until finished let wait = @@ -693,11 +690,11 @@ mod tests { executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); @@ -768,11 +765,11 @@ mod tests { let executor = Runner::timed(Duration::from_secs(30)); executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -851,11 +848,11 @@ mod tests { let executor = Runner::timed(Duration::from_secs(30)); executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -968,11 +965,11 @@ mod tests { let executor = Runner::timed(Duration::from_secs(30)); executor.start(|mut 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -1154,11 +1151,11 @@ mod tests { let indexer = first_run_indexer.clone(); 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, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); @@ -1278,11 +1275,11 @@ mod tests { let indexer = second_run_indexer.clone(); let expected_digests = expected_digests_for_recovery.clone(); let (network, mut oracle) = Network::new( - context.with_label("network"), + context.child("network"), simulated::Config { max_size: 1024 * 1024, disconnect_on_block: true, - tracked_peer_sets: Some(1), + tracked_peer_sets: NZUsize!(1), }, ); network.start(); diff --git a/client/Cargo.toml b/client/Cargo.toml index 2f9f3c58..bfe9a965 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -15,6 +15,7 @@ alto-types = { workspace = true } commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } +commonware-formatting = { workspace = true } commonware-utils = { workspace = true } commonware-parallel = { workspace = true } bytes = { workspace = true } diff --git a/client/src/lib.rs b/client/src/lib.rs index 1b35bc69..165ccf48 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,8 +2,8 @@ use alto_types::{Identity, Scheme, NAMESPACE}; use commonware_cryptography::sha256::Digest; +use commonware_formatting::hex; use commonware_parallel::Strategy; -use commonware_utils::hex; use std::sync::Arc; use thiserror::Error; diff --git a/deploy/Cargo.toml b/deploy/Cargo.toml index fd82f93a..59a6789e 100644 --- a/deploy/Cargo.toml +++ b/deploy/Cargo.toml @@ -13,6 +13,7 @@ commonware-codec = { workspace = true } commonware-consensus = { workspace = true, features = ["mocks"] } commonware-cryptography = { workspace = true, features = ["mocks"] } commonware-deployer = { workspace = true } +commonware-formatting = { workspace = true } commonware-macros = { workspace = true } commonware-math = { workspace = true } commonware-utils = { workspace = true } diff --git a/deploy/src/main.rs b/deploy/src/main.rs index a91e93b5..bc1460af 100644 --- a/deploy/src/main.rs +++ b/deploy/src/main.rs @@ -13,8 +13,9 @@ use commonware_cryptography::{ Signer, }; use commonware_deployer::aws::{self, METRICS_PORT}; +use commonware_formatting::{from_hex, hex}; use commonware_math::algebra::Random; -use commonware_utils::{from_hex_formatted, hex, NZU32}; +use commonware_utils::NZU32; use rand::{rngs::OsRng, seq::IteratorRandom}; use std::{ collections::{BTreeMap, HashMap}, @@ -730,7 +731,7 @@ fn explorer_local(dir: String, backend_url: String) { let peer_config: Config = serde_yaml::from_str(&peer_config_content).expect("failed to parse peer config"); let polynomial_hex = peer_config.polynomial; - let polynomial = from_hex_formatted(&polynomial_hex).expect("invalid polynomial"); + let polynomial = from_hex(&polynomial_hex).expect("invalid polynomial"); let polynomial = Sharing::::decode_cfg( polynomial.as_ref(), &(NZU32!(num_peers as u32), ModeVersion::v0()), @@ -762,7 +763,7 @@ fn explorer_remote(dir: String, backend_url: String) { let mut participants = BTreeMap::new(); for instance in &config.instances { let region = &instance.region; - let public_key = from_hex_formatted(&instance.name).expect("invalid public key"); + let public_key = from_hex(&instance.name).expect("invalid public key"); let public_key = PublicKey::decode(public_key.as_ref()).expect("invalid public key"); let (coords, city) = get_aws_location(region).expect("unknown region"); participants.insert( @@ -786,7 +787,7 @@ fn explorer_remote(dir: String, backend_url: String) { let peer_config: Config = serde_yaml::from_str(&peer_config_content).expect("failed to parse peer config"); let polynomial_hex = peer_config.polynomial; - let polynomial = from_hex_formatted(&polynomial_hex).expect("invalid polynomial"); + let polynomial = from_hex(&polynomial_hex).expect("invalid polynomial"); let polynomial = Sharing::::decode_cfg( polynomial.as_ref(), &(NZU32!(locations.len() as u32), ModeVersion::v0()), diff --git a/follower/Cargo.toml b/follower/Cargo.toml index a54558ae..7b153bb3 100644 --- a/follower/Cargo.toml +++ b/follower/Cargo.toml @@ -9,9 +9,11 @@ description = "Run a follower node for alto." alto-client = { workspace = true } alto-types = { workspace = true } commonware-broadcast = { workspace = true } +commonware-actor = { workspace = true } commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } +commonware-formatting = { workspace = true } commonware-macros = { workspace = true } commonware-math = { workspace = true } commonware-p2p = { workspace = true } diff --git a/follower/src/application.rs b/follower/src/application.rs index 1d48b35d..e6d9d6b1 100644 --- a/follower/src/application.rs +++ b/follower/src/application.rs @@ -1,13 +1,17 @@ use crate::throughput::Throughput; use alto_types::{Block, Scheme}; +use commonware_actor::{ + mailbox::{self, Policy}, + Feedback, +}; use commonware_consensus::{ marshal::{core::Mailbox as MarshalMailbox, standard::Standard, Update}, types::Height, Reporter, }; -use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Spawner}; +use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner}; use commonware_utils::Acknowledgement; -use futures::{channel::mpsc, SinkExt, StreamExt}; +use std::{collections::VecDeque, num::NonZeroUsize}; use tracing::info; const THROUGHPUT_WINDOW: std::time::Duration = std::time::Duration::from_secs(30); @@ -44,52 +48,63 @@ fn format_eta_maybe(remaining: Option, rate: f64) -> String { /// A forwarder of [Update] messages to the [Application]. #[derive(Clone)] pub(crate) struct Mailbox { - tx: mpsc::Sender>, + sender: mailbox::Sender, +} + +struct Message(Update); + +impl Policy for Message { + type Overflow = VecDeque; + + fn handle(overflow: &mut Self::Overflow, message: Self) -> bool { + overflow.push_back(message); + true + } } impl Reporter for Mailbox { type Activity = Update; - async fn report(&mut self, activity: Self::Activity) { - let _ = self.tx.send(activity).await; + fn report(&mut self, activity: Self::Activity) -> Feedback { + self.sender.enqueue(Message(activity)) } } /// A simple application that tracks just tracks the rate of block processing. -pub(crate) struct Application { +pub(crate) struct Application { context: ContextCell, - rx: mpsc::Receiver>, + receiver: mailbox::Receiver, throughput: Throughput, tip: Option, mailbox: MarshalMailbox>, pruning_depth: Option, } -impl Application { +impl Application { pub(crate) fn new( context: E, mailbox: MarshalMailbox>, - mailbox_size: usize, + mailbox_size: NonZeroUsize, pruning_depth: Option, ) -> (Self, Mailbox) { - let (tx, rx) = mpsc::channel(mailbox_size); + let (sender, receiver) = mailbox::new(context.child("mailbox"), mailbox_size); let app = Self { - context: ContextCell::new(context.clone()), - rx, + context: ContextCell::new(context), + receiver, throughput: Throughput::new(THROUGHPUT_WINDOW), tip: None, mailbox, pruning_depth, }; - (app, Mailbox { tx }) + (app, Mailbox { sender }) } pub(crate) fn start(mut self) -> Handle<()> { - spawn_cell!(self.context, self.run().await) + spawn_cell!(self.context, self.run()) } async fn run(mut self) { - while let Some(msg) = self.rx.next().await { + while let Some(Message(msg)) = self.receiver.recv().await { match msg { Update::Tip(_, height, _) => { self.tip = Some(height); @@ -115,7 +130,7 @@ impl Application { { let prune_to = height.saturating_sub(depth); if prune_to > 0 { - self.mailbox.prune(Height::new(prune_to)).await; + self.mailbox.prune(Height::new(prune_to)); } } } diff --git a/follower/src/archive.rs b/follower/src/archive.rs index 45bc53c5..d49d1f49 100644 --- a/follower/src/archive.rs +++ b/follower/src/archive.rs @@ -85,7 +85,7 @@ where if pruning_depth.is_some() { let fbh = prunable::Archive::init( - context.with_label("finalizations_by_height"), + context.child("finalizations_by_height"), prunable::Config { translator: FourCap, key_partition: PRUNABLE_FINALIZATIONS_BY_HEIGHT_KEY_PARTITION.to_string(), @@ -102,7 +102,7 @@ where .await .expect("failed to initialize finalizations by height archive"); let fb = prunable::Archive::init( - context.with_label("finalized_blocks"), + context.child("finalized_blocks"), prunable::Config { translator: FourCap, key_partition: PRUNABLE_FINALIZED_BLOCKS_KEY_PARTITION.to_string(), @@ -125,7 +125,7 @@ where ) } else { let fbh = immutable::Archive::init( - context.with_label("finalizations_by_height"), + context.child("finalizations_by_height"), immutable::Config { metadata_partition: IMMUTABLE_FINALIZATIONS_BY_HEIGHT_METADATA_PARTITION .to_string(), @@ -152,7 +152,7 @@ where .await .expect("failed to initialize finalizations by height archive"); let fb = immutable::Archive::init( - context.with_label("finalized_blocks"), + context.child("finalized_blocks"), immutable::Config { metadata_partition: IMMUTABLE_FINALIZED_BLOCKS_METADATA_PARTITION.to_string(), freezer_table_partition: IMMUTABLE_FINALIZED_BLOCKS_FREEZER_TABLE_PARTITION @@ -237,6 +237,18 @@ impl marshal::store::Certificates f Self::Prunable(a) => Archive::last_index(a).map(Height::new), } } + + fn ranges_from(&self, from: Height) -> impl Iterator { + match self { + Self::Immutable(a) => Archive::ranges_from(a, from.get()) + .map(|(start, end)| (Height::new(start), Height::new(end))) + .collect::>(), + Self::Prunable(a) => Archive::ranges_from(a, from.get()) + .map(|(start, end)| (Height::new(start), Height::new(end))) + .collect::>(), + } + .into_iter() + } } /// Wrapper over [immutable::Archive] and [prunable::Archive] for finalized @@ -301,4 +313,11 @@ impl marshal::store::Blocks for Blo }; (a.map(Height::new), b.map(Height::new)) } + + fn last_index(&self) -> Option { + match self { + Self::Immutable(a) => Archive::last_index(a).map(Height::new), + Self::Prunable(a) => Archive::last_index(a).map(Height::new), + } + } } diff --git a/follower/src/engine.rs b/follower/src/engine.rs index 85350614..f991c1aa 100644 --- a/follower/src/engine.rs +++ b/follower/src/engine.rs @@ -4,9 +4,8 @@ use crate::{ self, Blocks, Certificates, PRUNABLE_ITEMS_PER_SECTION, REPLAY_BUFFER, WRITE_BUFFER, }, resolver::Resolver, - NoopReceiver, NoopSender, }; -use alto_types::{Block, Scheme, EPOCH_LENGTH}; +use alto_types::{Block, Context, Scheme, EPOCH, EPOCH_LENGTH}; use commonware_broadcast::buffered; use commonware_consensus::{ marshal::{ @@ -20,25 +19,43 @@ use commonware_consensus::{ use commonware_cryptography::{ certificate::ConstantProvider, ed25519::{PrivateKey, PublicKey}, - sha256::Digest, - Signer, + sha256::{self, Digest, Sha256}, + Digest as _, Hasher, Signer, }; -use commonware_math::algebra::Random; -use commonware_p2p::utils::StaticProvider; use commonware_parallel::Strategy; use commonware_runtime::{ spawn_cell, BufferPooler, ContextCell, Handle, Metrics, Spawner, Storage, }; -use commonware_utils::{channel::mpsc, ordered::Set, NZUsize}; +use commonware_utils::NZUsize; use futures::future::try_join_all; use governor::clock::Clock as GClock; use rand::{CryptoRng, Rng}; -use std::num::NonZero; +use std::num::{NonZero, NonZeroUsize}; use tracing::{error, warn}; const VIEW_RETENTION_TIMEOUT: ViewDelta = ViewDelta::new(2560); -const DEQUE_SIZE: usize = 10; const MAX_PENDING_ACKS: NonZero = NZUsize!(1024); +const GENESIS: &[u8] = b"commonware is neat"; + +fn genesis() -> Block { + let genesis_context = Context { + round: commonware_consensus::types::Round::new( + EPOCH, + commonware_consensus::types::View::zero(), + ), + leader: PrivateKey::from_seed(0).public_key(), + parent: ( + commonware_consensus::types::View::zero(), + sha256::Digest::EMPTY, + ), + }; + Block::new( + genesis_context, + Sha256::hash(GENESIS), + commonware_consensus::types::Height::zero(), + 0, + ) +} /// The engine that drives the follower's [MarshalActor]. /// @@ -60,8 +77,6 @@ where T: Strategy, { context: ContextCell, - buffer: buffered::Engine>, - buffer_mailbox: buffered::Mailbox, marshal: MarshalActor< E, Standard, @@ -73,7 +88,7 @@ where >, pruning_depth: Option, marshal_mailbox: MarshalMailbox>, - mailbox_size: usize, + mailbox_size: NonZeroUsize, } impl Engine @@ -92,28 +107,11 @@ where pub async fn new( mut context: E, scheme: Scheme, - mailbox_size: usize, + mailbox_size: NonZeroUsize, max_repair: NonZero, strategy: T, pruning_depth: Option, ) -> (Self, MarshalMailbox>, Height) { - // Create the buffer - // - // The follower does not participate in p2p broadcast, so we use a dummy - // key and noop sender/receiver. The buffer is still required by marshal. - let dummy_key = PrivateKey::random(&mut context).public_key(); - let (buffer, buffer_mailbox) = buffered::Engine::new( - context.with_label("buffer"), - buffered::Config { - public_key: dummy_key, - mailbox_size, - deque_size: DEQUE_SIZE, - priority: false, - codec_config: (), - peer_provider: StaticProvider::new(0, Set::from_iter_dedup([])), - }, - ); - // Initialize the finalized certificate and block archives. Uses // prunable archives when pruning is enabled, immutable otherwise. let (finalizations_by_height, finalized_blocks, page_cache) = @@ -123,12 +121,13 @@ where let provider = ConstantProvider::new(scheme); let epocher = FixedEpocher::new(EPOCH_LENGTH); let (marshal, mailbox, last_processed_height) = MarshalActor::init( - context.with_label("marshal"), + context.child("marshal"), finalizations_by_height, finalized_blocks, marshal::Config { provider, epocher, + start: marshal::Start::Genesis(genesis()), partition_prefix: "follower-marshal".to_string(), mailbox_size, view_retention_timeout: VIEW_RETENTION_TIMEOUT, @@ -148,8 +147,6 @@ where // Return the engine and marshal mailbox let engine = Self { context: ContextCell::new(context), - buffer, - buffer_mailbox, marshal, pruning_depth, marshal_mailbox: mailbox.clone(), @@ -159,17 +156,11 @@ where } /// Start the [Engine]. - pub fn start( - mut self, - marshal: (mpsc::Receiver>, Resolver), - ) -> Handle<()> { - spawn_cell!(self.context, self.run(marshal).await) + pub fn start(mut self, marshal: (handler::Receiver, Resolver)) -> Handle<()> { + spawn_cell!(self.context, self.run(marshal)) } - async fn run(mut self, marshal: (mpsc::Receiver>, Resolver)) { - // Start the buffer - let buffer_handle = self.buffer.start((NoopSender, NoopReceiver)); - + async fn run(mut self, marshal: (handler::Receiver, Resolver)) { // Start the application actor let (app, mailbox) = Application::new( self.context.take(), @@ -180,142 +171,17 @@ where let app_handle = app.start(); // Start marshal - let marshal_handle = self.marshal.start(mailbox, self.buffer_mailbox, marshal); + let marshal_handle = self.marshal.start( + mailbox, + None::>, + marshal, + ); // Wait for any actor to finish - if let Err(e) = try_join_all(vec![buffer_handle, marshal_handle, app_handle]).await { + if let Err(e) = try_join_all(vec![marshal_handle, app_handle]).await { error!(?e, "engine failed"); } else { warn!("engine stopped"); } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::resolver::Actor; - use crate::test_utils::TestFixture; - use bytes::Bytes; - use commonware_codec::Encode; - use commonware_consensus::{ - marshal::resolver::handler, - types::{Height, Round, View}, - }; - use commonware_macros::test_traced; - use commonware_parallel::Sequential; - use commonware_runtime::{deterministic::Runner, Metrics, Runner as _}; - use commonware_utils::channel::{mpsc, oneshot}; - use commonware_utils::NZUsize; - use std::time::Duration; - - /// Verifies that marshal's Deliver handler rejects a finalization whose - /// threshold signature does not match the configured scheme. This is - /// the resolver path's signature verification (as opposed to the feeder - /// path tested in feeder::tests). - #[test_traced] - fn marshal_rejects_invalid_finalization_from_resolver() { - let fixture = TestFixture::new(); - let finalized = fixture.create_finalized(1, 1); - let wrong_verifier = fixture.wrong_verifier_scheme(); - - Runner::default().start(|context| async move { - let (engine, _mailbox, _) = Engine::new( - context.with_label("engine"), - wrong_verifier.clone(), - 16, - NZUsize!(256), - Sequential, - None, - ) - .await; - - // Wire up the resolver and start the engine - let (ingress_tx, ingress_rx) = mpsc::channel(16); - let source = crate::test_utils::MockSource::new(); - let (_, resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx.clone(), - 16, - Duration::from_secs(1), - ); - let _engine_handle = engine.start((ingress_rx, resolver)); - - // Manually inject a finalization into marshal's ingress channel, - // bypassing the resolver actor to control the payload directly. - let key = handler::Request::::Finalized { - height: Height::new(1), - }; - let value = Bytes::from((finalized.proof, finalized.block).encode().to_vec()); - let (response_tx, response_rx) = oneshot::channel(); - ingress_tx - .send(handler::Message::Deliver { - key, - value, - response: response_tx, - }) - .await - .expect("send failed"); - - // Marshal should reject the delivery due to signature mismatch - let accepted = response_rx.await.expect("response dropped"); - assert!( - !accepted, - "marshal should reject finalization with invalid signature" - ); - }); - } - - /// Verifies that marshal's Deliver handler rejects a notarization whose - /// threshold signature does not match the configured scheme. - #[test_traced] - fn marshal_rejects_invalid_notarization_from_resolver() { - let fixture = TestFixture::new(); - let notarized = fixture.create_notarized(1, 1); - let wrong_verifier = fixture.wrong_verifier_scheme(); - - Runner::default().start(|context| async move { - let (engine, _mailbox, _) = Engine::new( - context.with_label("engine"), - wrong_verifier.clone(), - 16, - NZUsize!(256), - Sequential, - None, - ) - .await; - - let (ingress_tx, ingress_rx) = mpsc::channel(16); - let source = crate::test_utils::MockSource::new(); - let (_, resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx.clone(), - 16, - Duration::from_secs(1), - ); - let _engine_handle = engine.start((ingress_rx, resolver)); - - // Inject a notarization directly into marshal's ingress channel - let round = Round::new(alto_types::EPOCH, View::new(1)); - let key = handler::Request::::Notarized { round }; - let value = Bytes::from((notarized.proof, notarized.block).encode().to_vec()); - let (response_tx, response_rx) = oneshot::channel(); - ingress_tx - .send(handler::Message::Deliver { - key, - value, - response: response_tx, - }) - .await - .expect("send failed"); - - let accepted = response_rx.await.expect("response dropped"); - assert!( - !accepted, - "marshal should reject notarization with invalid signature" - ); - }); - } -} diff --git a/follower/src/feeder.rs b/follower/src/feeder.rs index 0f6d337e..126a3e14 100644 --- a/follower/src/feeder.rs +++ b/follower/src/feeder.rs @@ -57,7 +57,7 @@ impl Feeder { /// Start the [Feeder] in a background task. pub fn start(mut self) -> Handle<()> { - spawn_cell!(self.context, self.run().await) + spawn_cell!(self.context, self.run()) } /// Run the feeder loop, reconnecting on stream disconnection. @@ -126,12 +126,19 @@ impl Feeder { // just prune it later. // // TODO (https://github.com/commonwarexyz/monorepo/pull/2208): create a dedicated cache for storing unverified (but notarized) blocks - self.marshal_mailbox + if !self + .marshal_mailbox .verified(round, notarized.block.clone()) - .await; + .await + { + warn!( + height = notarized.block.height.get(), + view = round.view().get(), + "failed to cache notarized block" + ); + } self.marshal_mailbox - .report(Activity::Notarization(notarized.proof.clone())) - .await; + .report(Activity::Notarization(notarized.proof.clone())); debug!( height = notarized.block.height.get(), view = round.view().get(), @@ -151,12 +158,19 @@ impl Feeder { // Cache the block and report the finalization proof to marshal let round = finalized.proof.round(); - self.marshal_mailbox + if !self + .marshal_mailbox .verified(round, finalized.block.clone()) - .await; + .await + { + warn!( + height = height.get(), + view = view.get(), + "failed to cache finalized block" + ); + } self.marshal_mailbox - .report(Activity::Finalization(finalized.proof.clone())) - .await; + .report(Activity::Finalization(finalized.proof.clone())); debug!( height = height.get(), view = view.get(), @@ -174,8 +188,9 @@ mod tests { use crate::test_utils::{MockSource, TestFixture}; use alto_client::consensus::Message; use commonware_macros::test_traced; - use commonware_runtime::{deterministic::Runner, Metrics, Runner as _}; + use commonware_runtime::{deterministic::Runner, Runner as _, Supervisor as _}; use commonware_utils::NZUsize; + use std::time::Duration; /// Verifies that a finalization with a valid threshold signature is /// accepted by handle_message without error. @@ -188,9 +203,9 @@ mod tests { Runner::default().start(|context| async move { // Engine is needed to provide the marshal mailbox let (_engine, mailbox, _) = crate::engine::Engine::new( - context.with_label("engine"), + context.child("engine"), verifier.clone(), - 16, + NZUsize!(16), NZUsize!(256), Sequential, None, @@ -198,7 +213,14 @@ mod tests { .await; let source = MockSource::new(); - let mut feeder = Feeder::new(context.with_label("feeder"), source, verifier, mailbox); + let resolver = crate::resolver::init( + context.child("resolver"), + source.clone(), + NZUsize!(16), + Duration::from_millis(10), + ); + let _engine_handle = _engine.start(resolver); + let mut feeder = Feeder::new(context.child("feeder"), source, verifier, mailbox); let result = feeder .handle_message(Message::Finalization(finalized)) @@ -220,9 +242,9 @@ mod tests { Runner::default().start(|context| async move { let (_engine, mailbox, _) = crate::engine::Engine::new( - context.with_label("engine"), + context.child("engine"), wrong_verifier.clone(), - 16, + NZUsize!(16), NZUsize!(256), Sequential, None, @@ -230,12 +252,7 @@ mod tests { .await; let source = MockSource::new(); - let mut feeder = Feeder::new( - context.with_label("feeder"), - source, - wrong_verifier, - mailbox, - ); + let mut feeder = Feeder::new(context.child("feeder"), source, wrong_verifier, mailbox); // Should panic on the assert! inside handle_message feeder @@ -255,9 +272,9 @@ mod tests { Runner::default().start(|context| async move { let (_engine, mailbox, _) = crate::engine::Engine::new( - context.with_label("engine"), + context.child("engine"), verifier.clone(), - 16, + NZUsize!(16), NZUsize!(256), Sequential, None, @@ -265,7 +282,14 @@ mod tests { .await; let source = MockSource::new(); - let mut feeder = Feeder::new(context.with_label("feeder"), source, verifier, mailbox); + let resolver = crate::resolver::init( + context.child("resolver"), + source.clone(), + NZUsize!(16), + Duration::from_millis(10), + ); + let _engine_handle = _engine.start(resolver); + let mut feeder = Feeder::new(context.child("feeder"), source, verifier, mailbox); let result = feeder .handle_message(Message::Notarization(notarized)) @@ -285,9 +309,9 @@ mod tests { Runner::default().start(|context| async move { let (_engine, mailbox, _) = crate::engine::Engine::new( - context.with_label("engine"), + context.child("engine"), wrong_verifier.clone(), - 16, + NZUsize!(16), NZUsize!(256), Sequential, None, @@ -295,12 +319,7 @@ mod tests { .await; let source = MockSource::new(); - let mut feeder = Feeder::new( - context.with_label("feeder"), - source, - wrong_verifier, - mailbox, - ); + let mut feeder = Feeder::new(context.child("feeder"), source, wrong_verifier, mailbox); feeder .handle_message(Message::Notarization(notarized)) diff --git a/follower/src/main.rs b/follower/src/main.rs index 75b18d7f..b4cadb2b 100644 --- a/follower/src/main.rs +++ b/follower/src/main.rs @@ -4,23 +4,19 @@ use alto_types::{Finalized, Identity, Notarized, Scheme, NAMESPACE}; use clap::{Arg, Command}; use commonware_codec::DecodeExt; use commonware_consensus::types::Height; -use commonware_cryptography::ed25519::PublicKey; -use commonware_cryptography::sha256::Digest; +use commonware_formatting::from_hex; use commonware_macros::select; -use commonware_p2p::Recipients; use commonware_parallel::Sequential; -use commonware_runtime::{tokio, Clock, IoBufs, Metrics, Runner, ThreadPooler}; -use commonware_utils::{channel::mpsc, from_hex_formatted, time::SystemTimeExt}; +use commonware_runtime::{tokio, Clock, Runner, Supervisor as _, ThreadPooler}; use futures::Stream; use serde::{Deserialize, Serialize}; use std::{ - fmt::Debug, future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, num::NonZero, path::PathBuf, str::FromStr, - time::{Duration, SystemTime}, + time::Duration, }; use tracing::{error, info, warn, Level}; @@ -123,58 +119,6 @@ impl Source for alto_client::Client { } } -/// Noop p2p sender used by the follower's buffer engine. -/// -/// The follower does not participate in p2p broadcast, so all send -/// operations are dropped. -#[derive(Clone)] -pub(crate) struct NoopSender; - -pub(crate) struct NoopCheckedSender; - -impl commonware_p2p::CheckedSender for NoopCheckedSender { - type PublicKey = PublicKey; - type Error = std::io::Error; - - async fn send( - self, - _message: impl Into + Send, - _priority: bool, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } -} - -impl commonware_p2p::LimitedSender for NoopSender { - type PublicKey = PublicKey; - type Checked<'a> = NoopCheckedSender; - - async fn check( - &mut self, - _recipients: Recipients, - ) -> Result, SystemTime> { - // Return the maximum possible time to ensure buffered never - // retries sending -- the follower has no p2p peers. - Err(SystemTime::limit()) - } -} - -/// Noop p2p receiver used by the follower's buffer engine. -/// -/// The follower does not participate in p2p broadcast, so recv blocks -/// forever (via [std::future::pending]). -#[derive(Debug)] -pub(crate) struct NoopReceiver; - -impl commonware_p2p::Receiver for NoopReceiver { - type Error = std::io::Error; - type PublicKey = PublicKey; - - async fn recv(&mut self) -> Result, Self::Error> { - std::future::pending().await - } -} - fn main() { // Parse arguments let matches = Command::new("follower") @@ -190,8 +134,7 @@ fn main() { }; // Parse identity - let identity_bytes = - from_hex_formatted(&config.identity).expect("Could not parse identity hex"); + let identity_bytes = from_hex(&config.identity).expect("Could not parse identity hex"); let identity = Identity::decode(identity_bytes.as_ref()).expect("Could not decode identity public key"); @@ -208,7 +151,7 @@ fn main() { // Configure telemetry let log_level = Level::from_str(&config.log_level).expect("Invalid log level"); tokio::telemetry::init( - context.with_label("telemetry"), + context.child("telemetry"), tokio::telemetry::Logging { level: log_level, json: false, @@ -254,9 +197,9 @@ fn main() { .create_strategy(config.signature_threads) .unwrap(); let (engine, mailbox, last_processed_height) = engine::Engine::new( - context.with_label("engine"), + context.child("engine"), scheme.clone(), - config.mailbox_size.get(), + config.mailbox_size, config.max_repair, strategy, config.pruning_depth, @@ -275,7 +218,7 @@ fn main() { ); let height = finalized.block.height; info!(height = height.get(), "setting checkpoint floor from latest finalized block"); - mailbox.set_floor(height).await; + mailbox.set_floor(finalized.proof.clone()); } Err(e) => { warn!(error = ?e, "failed to fetch latest finalized block for checkpoint, will backfill from genesis"); @@ -284,24 +227,19 @@ fn main() { } // Create resolver - let (ingress_tx, ingress_rx) = mpsc::channel::< - commonware_consensus::marshal::resolver::handler::Message, - >(config.mailbox_size.get()); - let (resolver_actor, resolver) = resolver::Actor::new( - context.with_label("resolver"), + let marshal_resolver = resolver::init( + context.child("resolver"), client.clone(), - ingress_tx, - config.mailbox_size.get(), + config.mailbox_size, Duration::from_millis(config.fetch_retry_timeout_ms), ); - let resolver_handle = resolver_actor.start(); // Start engine - let engine_handle = engine.start((ingress_rx, resolver)); + let engine_handle = engine.start(marshal_resolver); // Start certificate feeder let feeder = feeder::Feeder::new( - context.with_label("feeder"), + context.child("feeder"), client, scheme, mailbox, @@ -312,7 +250,6 @@ fn main() { select! { _ = engine_handle => {}, _ = feeder_handle => {}, - _ = resolver_handle => {}, }; error!("follower stopped unexpectedly"); }); diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index ed12a741..a32219d4 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -1,960 +1,310 @@ use crate::Source; -use alto_client::consensus::Payload; -use alto_client::{IndexQuery, Query}; -use bytes::Bytes; -use commonware_codec::Encode; -use commonware_consensus::{marshal::resolver::handler, types::Height}; -use commonware_cryptography::{ed25519::PublicKey, sha256::Digest}; -use commonware_macros::select_loop; -use commonware_resolver::Consumer; -use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Spawner}; -use commonware_utils::channel::mpsc; +use alto_client::{consensus::Payload, IndexQuery, Query}; +use bytes::{Buf, Bytes}; +use commonware_actor::Feedback; +use commonware_codec::{Encode, ReadExt, Write}; +use commonware_consensus::marshal::resolver::{ + handler, + p2p::{self as marshal_p2p, Mailbox}, +}; +use commonware_cryptography::{ + ed25519::{PrivateKey, PublicKey}, + sha256::Digest, + Signer, +}; +use commonware_p2p::{ + utils::StaticProvider, Blocker, CheckedSender, LimitedSender, Message, Receiver, Recipients, +}; +use commonware_runtime::{ + spawn_cell, BufferPooler, Clock, ContextCell, Handle, IoBuf, IoBufs, Metrics, Spawner, +}; use commonware_utils::{ - futures::{AbortablePool, Aborter}, - vec::NonEmptyVec, - SystemTimeExt, + channel::{fallible::AsyncFallibleExt as _, mpsc}, + ordered::Set, }; -use rand::{CryptoRng, RngCore}; +use rand::Rng; use std::{ - collections::{BTreeSet, HashMap}, + fmt, io, + num::NonZeroUsize, time::{Duration, SystemTime}, }; -use tracing::{debug, trace, warn}; - -/// Messages sent from the [Resolver] handle to the [Actor]. -#[allow(clippy::type_complexity)] -pub enum Message { - Fetch(handler::Request), - Cancel(handler::Request), - Clear, - Retain(Box) -> bool + Send>), -} - -/// Handle to the [Actor] that implements [Resolver] for marshal. -/// -/// All operations are forwarded as messages to the actor via a channel. -#[derive(Clone)] -pub struct Resolver { - mailbox_tx: mpsc::Sender, -} - -impl commonware_resolver::Resolver for Resolver { - type Key = handler::Request; - type PublicKey = PublicKey; +use tracing::{debug, warn}; - async fn fetch(&mut self, key: Self::Key) { - let msg = Message::Fetch(key); - if let Err(e) = self.mailbox_tx.send(msg).await { - warn!(error = ?e, "failed to send fetch request to resolver actor"); - } - } - - async fn fetch_all(&mut self, keys: Vec) { - for key in keys { - self.fetch(key).await; - } - } - - async fn fetch_targeted(&mut self, key: Self::Key, _targets: NonEmptyVec) { - self.fetch(key).await; - } - - async fn fetch_all_targeted( - &mut self, - requests: Vec<(Self::Key, NonEmptyVec)>, - ) { - for (key, _) in requests { - self.fetch(key).await; - } - } +pub type Resolver = Mailbox; - async fn cancel(&mut self, key: Self::Key) { - let msg = Message::Cancel(key); - if let Err(e) = self.mailbox_tx.send(msg).await { - warn!(error = ?e, "failed to send cancel request to resolver actor"); - } - } - - async fn clear(&mut self) { - let msg = Message::Clear; - if let Err(e) = self.mailbox_tx.send(msg).await { - warn!(error = ?e, "failed to send clear request to resolver actor"); - } - } - - async fn retain(&mut self, f: impl Fn(&Self::Key) -> bool + Send + 'static) { - let msg = Message::Retain(Box::new(f)); - if let Err(e) = self.mailbox_tx.send(msg).await { - warn!(error = ?e, "failed to send retain request to resolver actor"); - } - } +struct Request { + id: u64, + key: handler::Key, } -/// Actor that fetches blocks and certificates from a [Source] on behalf of marshal. -/// -/// This replaces the p2p-based resolver used by validators. When marshal needs -/// a block or certificate it does not have locally, it asks this actor to fetch -/// it from the HTTP source. -/// -/// The [Source] (client) should be constructed without verification because marshal's -/// Deliver handler verifies all signatures before accepting resolved data. -/// Rejections are logged as warnings and retried. -pub struct Actor { +struct SourceActor { context: ContextCell, client: C, - mailbox_rx: mpsc::Receiver, - handler: handler::Handler, - active: AbortablePool, - requests: HashMap, State>, - retry_schedule: BTreeSet<(SystemTime, handler::Request)>, - fetch_retry_timeout: Duration, - next_id: u64, + requests: mpsc::Receiver, + responses: mpsc::Sender>, + peer: PublicKey, } -enum State { - // A fetch is currently running. - // - // The id lets us ignore stale completions from an earlier attempt for - // the same key, and dropping the aborter cancels the current attempt. - // This ensures we only have 1 fetch in flight for a given key (regardless - // of the order of fetch, cancel, clear, retain messages). - Active { id: u64, aborter: Aborter }, - // A retry is queued for the recorded deadline. - Scheduled(SystemTime), -} - -struct Result { - key: handler::Request, - id: u64, - retry: bool, -} - -impl Actor { - /// Create a new [Actor] and its corresponding [Resolver] handle. - pub fn new( +impl SourceActor { + fn new( context: E, client: C, - ingress_tx: mpsc::Sender>, - mailbox_size: usize, - fetch_retry_timeout: Duration, - ) -> (Self, Resolver) { - let (mailbox_tx, mailbox_rx) = mpsc::channel(mailbox_size); - - let actor = Self { + requests: mpsc::Receiver, + responses: mpsc::Sender>, + peer: PublicKey, + ) -> Self { + Self { context: ContextCell::new(context), client, - mailbox_rx, - handler: handler::Handler::new(ingress_tx), - active: AbortablePool::default(), - requests: HashMap::new(), - retry_schedule: BTreeSet::new(), - fetch_retry_timeout, - next_id: 0, - }; - - let handle = Resolver { mailbox_tx }; - - (actor, handle) + requests, + responses, + peer, + } } - /// Start the [Actor] in a background task. - pub fn start(mut self) -> Handle<()> { - spawn_cell!(self.context, self.run().await) + fn start(mut self) -> Handle<()> { + spawn_cell!(self.context, self.run()) } - /// Run the actor loop, processing fetch/cancel/clear/retain messages. async fn run(mut self) { - select_loop! { - self.context, - on_stopped => {}, - Ok(result) = self.active.next_completed() else continue => { - self.handle_completed(result); - }, - _ = match self.retry_schedule.first() { - Some((deadline, _)) => futures::future::Either::Left(self.context.sleep_until(*deadline)), - None => futures::future::Either::Right(futures::future::pending()), - } => { - self.process_retries(); - }, - Some(msg) = self.mailbox_rx.recv() else break => { - match msg { - Message::Fetch(key) => { - if let Some(state) = self.requests.get(&key) { - match state { - State::Active { .. } => { - trace!(?key, "ignoring fetch request for active key"); - } - State::Scheduled(_) => { - trace!(?key, "ignoring fetch request for scheduled key"); - } - } - continue; - } - self.start_fetch(key); - } - Message::Cancel(key) => { - if let Some(state) = self.requests.remove(&key) { - match state { - State::Active { aborter, .. } => { - drop(aborter); - debug!(?key, "cancelled active request"); - } - State::Scheduled(deadline) => { - let removed = self.retry_schedule.remove(&(deadline, key.clone())); - assert!(removed, "scheduled retry entry missing"); - debug!(?key, ?deadline, "cancelled scheduled request"); - } - } - } - } - Message::Clear => { - let active = self - .requests - .values() - .filter(|state| matches!(state, State::Active { .. })) - .count(); - let scheduled = self.requests.len() - active; - self.requests.clear(); - self.retry_schedule.clear(); - debug!(active, scheduled, "cleared all pending requests"); - } - Message::Retain(f) => { - let to_remove = self - .requests - .keys() - .filter(|key| !f(key)) - .cloned() - .collect::>(); - let removed = to_remove.len(); - for key in to_remove { - if let Some(State::Scheduled(deadline)) = self.requests.remove(&key) { - let removed = self.retry_schedule.remove(&(deadline, key.clone())); - assert!(removed, "scheduled retry entry missing"); - } - } - debug!(removed, remaining = self.requests.len(), "retained pending requests"); - } - } - }, + while let Some(request) = self.requests.recv().await { + let response = match fetch(&self.client, request.key).await { + Some(value) => encode_response(request.id, Some(value)), + None => encode_response(request.id, None), + }; + let _ = self + .responses + .send((self.peer.clone(), IoBuf::from(response))) + .await; } } +} - /// Start a fetch for the given key. - fn start_fetch(&mut self, key: handler::Request) { - let id = self.next_id; - self.next_id = self.next_id.wrapping_add(1); - let future = - Self::process_fetch(key.clone(), id, self.client.clone(), self.handler.clone()); - let aborter = self.active.push(future); - let previous = self.requests.insert(key, State::Active { id, aborter }); - assert!(previous.is_none(), "request state already existed"); - } - - /// Handle a completed fetch. - fn handle_completed(&mut self, result: Result) { - // If the request has been removed or updated, ignore the completion. - let Some(state) = self.requests.get(&result.key) else { - trace!( - ?result.key, - id = result.id, - "ignoring stale fetch completion for removed request" - ); - return; - }; - match state { - State::Active { id, .. } if *id == result.id => {} - State::Active { id, .. } => { - trace!( - ?result.key, - completed_id = result.id, - active_id = *id, - "ignoring stale fetch completion for replaced request" - ); - return; - } - State::Scheduled(deadline) => { - trace!( - ?result.key, - id = result.id, - ?deadline, - "ignoring stale fetch completion for scheduled request" - ); - return; - } - } - - // Remove the request and (optionally) schedule a retry. - let removed = self.requests.remove(&result.key); - assert!( - matches!( - removed, - Some(State::Active { id, .. }) if id == result.id - ), - "active request state missing for completed fetch" - ); - if result.retry { - self.schedule_retry(result.key); - } - } +#[derive(Clone)] +struct SourceSender { + requests: mpsc::Sender, + peer: PublicKey, +} - /// Schedule a retry for the given key. - fn schedule_retry(&mut self, key: handler::Request) { - let deadline = self - .context - .current() - .add_jittered(&mut self.context, self.fetch_retry_timeout); - let previous = self - .requests - .insert(key.clone(), State::Scheduled(deadline)); - assert!( - matches!(previous, None | Some(State::Active { .. })), - "request was already scheduled" - ); - self.retry_schedule.insert((deadline, key.clone())); - debug!(?key, ?deadline, "scheduled fetch retry"); - } +struct SourceCheckedSender { + requests: mpsc::Sender, + peer: PublicKey, +} - /// Process any due retries. - fn process_retries(&mut self) { - let now = self.context.current(); - while let Some((deadline, key)) = self.retry_schedule.pop_first() { - if deadline > now { - self.retry_schedule.insert((deadline, key)); - break; - } - match self.requests.get(&key) { - Some(State::Scheduled(state_deadline)) if *state_deadline == deadline => { - self.requests.remove(&key); - debug!(?key, "retrying fetch request"); - self.start_fetch(key); - } - Some(State::Active { .. }) => { - trace!( - ?key, - "skipping stale retry because request is already in flight" - ); - } - Some(State::Scheduled(state_deadline)) => { - trace!( - ?key, - ?deadline, - ?state_deadline, - "skipping stale retry with outdated deadline" - ); - } - None => { - trace!(?key, ?deadline, "skipping stale retry for removed request"); - } - } - } - } +impl LimitedSender for SourceSender { + type PublicKey = PublicKey; + type Checked<'a> = SourceCheckedSender; - /// Process a fetch request. - async fn process_fetch( - key: handler::Request, - id: u64, - client: C, - handler: handler::Handler, - ) -> Result { - let retry = match &key { - handler::Request::Block(digest) => { - Self::fetch_block_by_digest(*digest, client, handler).await - } - handler::Request::Finalized { height } => { - Self::fetch_finalized_by_height(*height, client, handler).await - } - handler::Request::Notarized { round } => { - Self::fetch_notarized_by_round(*round, client, handler).await - } + fn check( + &mut self, + recipients: Recipients, + ) -> Result, SystemTime> { + let selected = match recipients { + Recipients::All => true, + Recipients::Some(peers) => peers.contains(&self.peer), + Recipients::One(peer) => peer == self.peer, }; - Result { key, id, retry } - } - - /// Fetch a block by digest. - async fn fetch_block_by_digest( - digest: Digest, - client: C, - mut handler: handler::Handler, - ) -> bool { - debug!(?digest, "fetching block by digest"); - - match client.block(alto_client::Query::Digest(digest)).await { - Ok(Payload::Block(block)) => { - let key = handler::Request::Block(digest); - let value = Bytes::from(block.encode().to_vec()); - if !handler.deliver(key, value).await { - warn!(?digest, "failed to deliver block to marshal"); - return true; - } - debug!(?digest, "fetched block by digest"); - false - } - Ok(_) => { - warn!(?digest, "wrong payload returned for block by digest"); - true - } - Err(e) => { - warn!(?digest, error=?e, "failed to fetch block by digest"); - true - } - } + selected + .then(|| SourceCheckedSender { + requests: self.requests.clone(), + peer: self.peer.clone(), + }) + .ok_or_else(SystemTime::now) } +} - /// Fetch a finalized block by height. - async fn fetch_finalized_by_height( - height: Height, - client: C, - mut handler: handler::Handler, - ) -> bool { - debug!(height = height.get(), "fetching finalized block by height"); +impl CheckedSender for SourceCheckedSender { + type PublicKey = PublicKey; - match client.block(Query::Index(height.get())).await { - Ok(Payload::Finalized(finalized)) => { - let key = handler::Request::Finalized { height }; - let finalization = finalized.proof.clone(); - let block = finalized.block.clone(); - let value = Bytes::from((finalization, block).encode().to_vec()); - if !handler.deliver(key, value).await { - warn!(height = height.get(), "marshal rejected finalized block"); - return true; - } - debug!(height = height.get(), "fetched finalized block by height"); - false - } - Ok(_) => { - warn!( - height = height.get(), - "wrong payload returned for finalized block by height" - ); - true - } - Err(e) => { - warn!(height = height.get(), error=?e, "failed to fetch finalized block by height"); - true - } - } + fn recipients(&self) -> Vec { + vec![self.peer.clone()] } - /// Fetch a notarized block by round. - async fn fetch_notarized_by_round( - round: commonware_consensus::types::Round, - client: C, - mut handler: handler::Handler, - ) -> bool { - let view = round.view().get(); - debug!(view, "fetching notarized block by round"); - - match client.notarized(IndexQuery::Index(view)).await { - Ok(notarized) => { - let key = handler::Request::Notarized { round }; - let notarization = notarized.proof.clone(); - let block = notarized.block.clone(); - let value = Bytes::from((notarization, block).encode().to_vec()); - if !handler.deliver(key, value).await { - warn!(view, "marshal rejected notarized block"); - return true; - } - debug!(view, "fetched notarized block by round"); - false - } - Err(e) => { - warn!(view, error=?e, "failed to fetch notarized block by round"); - true - } + fn send(self, message: impl Into + Send, _priority: bool) -> Feedback { + let Some(request) = decode_request(message) else { + return Feedback::Rejected; + }; + if self.requests.try_send_lossy(request) { + Feedback::Ok + } else { + Feedback::Rejected } } } -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::{MockSource, TestFixture}; - use alto_client::{consensus::Payload, Query}; - use commonware_consensus::{ - marshal::resolver::handler, - types::{Height, Round, View}, - }; - use commonware_cryptography::{sha256::Digest, Digestible}; - use commonware_macros::test_traced; - use commonware_resolver::Resolver as _; - use commonware_runtime::{deterministic::Runner, Clock, Metrics, Runner as _}; - use commonware_utils::channel::mpsc; - use std::sync::{Arc, Mutex}; - use std::time::Duration; - - const DEFAULT_FETCH_RETRY_TIMEOUT: Duration = Duration::from_secs(1); - - /// Exercises the full Resolver trait surface (fetch, cancel, clear, - /// retain) to ensure messages reach the actor without error. - #[test_traced] - fn fetch_cancel_clear_retain() { - Runner::default().start(|context| async move { - let source = MockSource::new(); - let (ingress_tx, _ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - let key = handler::Request::::Finalized { - height: Height::new(1), - }; - - resolver.fetch(key.clone()).await; - resolver.cancel(key).await; - resolver.clear().await; - resolver.retain(|_| true).await; - - // Allow the actor to process all queued messages - context.sleep(Duration::from_millis(100)).await; - }); - } - - /// Verifies that the actor fetches a block by digest from the source - /// and delivers it to marshal's ingress channel. - #[test_traced] - fn fetches_block_by_digest() { - let fixture = TestFixture::new(); - let block = fixture.create_block(1, 1); - let digest = block.digest(); - - // Configure the mock to return the block for any query - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |_| { - Some(Payload::Block(Box::new(block.clone()))) - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); +struct SourceReceiver { + responses: mpsc::Receiver>, +} - // Verify the actor delivered the block with the correct key - resolver.fetch(handler::Request::Block(digest)).await; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { key, .. } => { - assert!(matches!(key, handler::Request::Block(d) if d == digest)); - } - _ => panic!("expected Deliver message"), - } - }); +impl fmt::Debug for SourceReceiver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SourceReceiver").finish_non_exhaustive() } +} - /// Verifies that the actor fetches a finalized block by height from - /// the source and delivers it to marshal's ingress channel. - #[test_traced] - fn fetches_finalized_by_height() { - let fixture = TestFixture::new(); - let finalized = fixture.create_finalized(5, 5); - let height = Height::new(5); - - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |query| match query { - Query::Index(index) if index == height.get() => { - Some(Payload::Finalized(Box::new(finalized.clone()))) - } - _ => None, - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); +impl Receiver for SourceReceiver { + type Error = io::Error; + type PublicKey = PublicKey; - resolver.fetch(handler::Request::Finalized { height }).await; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { key, .. } => { - assert!( - matches!(key, handler::Request::Finalized { height: h } if h == height) - ); - } - _ => panic!("expected Deliver message"), - } - }); + async fn recv(&mut self) -> Result, Self::Error> { + self.responses.recv().await.ok_or_else(|| { + io::Error::new( + io::ErrorKind::UnexpectedEof, + "source response channel closed", + ) + }) } +} - /// Verifies that finalized backfill uses a height-indexed block query - /// (not a view-indexed finalization query) so diverged view/height still - /// resolves correctly. - #[test_traced] - fn fetches_finalized_by_height_uses_height_indexed_block_query() { - let fixture = TestFixture::new(); - let finalized = fixture.create_finalized(5, 8); - let height = Height::new(5); - let expected_height = height.get(); - - let block_call_count = Arc::new(Mutex::new(0u32)); - let block_call_count_inner = block_call_count.clone(); - let finalized_call_count = Arc::new(Mutex::new(0u32)); - let finalized_call_count_inner = finalized_call_count.clone(); - - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |query| { - *block_call_count_inner.lock().unwrap() += 1; - match query { - Query::Index(index) if index == expected_height => { - Some(Payload::Finalized(Box::new(finalized.clone()))) - } - _ => None, - } - })); - *source.finalized_handler.lock().unwrap() = Some(Box::new(move |_| { - *finalized_call_count_inner.lock().unwrap() += 1; - None - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - // Allow the fetch to run to completion. - resolver.fetch(handler::Request::Finalized { height }).await; - context.sleep(Duration::from_millis(100)).await; +#[derive(Clone)] +struct SourceBlocker; - assert_eq!( - *block_call_count.lock().unwrap(), - 1, - "expected finalized backfill to query block endpoint by height" - ); - assert_eq!( - *finalized_call_count.lock().unwrap(), - 0, - "expected finalized backfill to avoid view-indexed finalization endpoint" - ); +impl Blocker for SourceBlocker { + type PublicKey = PublicKey; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { key, .. } => { - assert!( - matches!(key, handler::Request::Finalized { height: h } if h == height) - ); - } - _ => panic!("expected Deliver message"), - } - }); + fn block(&mut self, peer: Self::PublicKey) -> Feedback { + warn!(?peer, "source-backed resolver peer blocked"); + Feedback::Ok } +} - /// Verifies that the actor fetches a notarized block by round from - /// the source and delivers it to marshal's ingress channel. - #[test_traced] - fn fetches_notarized_by_round() { - let fixture = TestFixture::new(); - let notarized = fixture.create_notarized(3, 3); - let round = Round::new(alto_types::EPOCH, View::new(3)); - - let source = MockSource::new(); - *source.notarized_handler.lock().unwrap() = - Some(Box::new(move |_| Some(notarized.clone()))); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); +pub fn init( + context: E, + client: C, + mailbox_size: NonZeroUsize, + fetch_retry_timeout: Duration, +) -> (handler::Receiver, Resolver) +where + E: BufferPooler + Rng + Spawner + Clock + Metrics, + C: Source, +{ + let local = PrivateKey::from_seed(0).public_key(); + let peer = PrivateKey::from_seed(1).public_key(); + let (requests_tx, requests_rx) = mpsc::channel(mailbox_size.get()); + let (responses_tx, responses_rx) = mpsc::channel(mailbox_size.get()); + + SourceActor::new( + context.child("source"), + client, + requests_rx, + responses_tx, + peer.clone(), + ) + .start(); + + marshal_p2p::init( + context.child("resolver"), + marshal_p2p::Config { + public_key: local, + peer_provider: StaticProvider::new(0, Set::from_iter_dedup([peer.clone()])), + blocker: SourceBlocker, + mailbox_size, + initial: fetch_retry_timeout, + timeout: fetch_retry_timeout, + fetch_retry_timeout, + priority_requests: false, + priority_responses: false, + }, + ( + SourceSender { + requests: requests_tx, + peer: peer.clone(), + }, + SourceReceiver { + responses: responses_rx, + }, + ), + ) +} - resolver.fetch(handler::Request::Notarized { round }).await; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { key, .. } => { - assert!(matches!(key, handler::Request::Notarized { round: r } if r == round)); - } - _ => panic!("expected Deliver message"), - } - }); +fn decode_request(message: impl Into) -> Option { + let mut message = message.into(); + let mut bytes = message.copy_to_bytes(message.remaining()); + let id = u64::read(&mut bytes).ok()?; + let payload = u8::read(&mut bytes).ok()?; + if payload != 0 { + return None; } + let key = handler::Key::::read(&mut bytes).ok()?; + Some(Request { id, key }) +} - /// Verifies that marshal rejecting a finalized delivery causes the - /// resolver to retry and redeliver it. - #[test_traced] - fn retries_when_marshal_rejects_finalized_delivery() { - let fixture = TestFixture::new(); - let finalized = fixture.create_finalized(1, 1); - let height = Height::new(1); - let call_count = Arc::new(Mutex::new(0u32)); - let call_count_inner = call_count.clone(); - - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |query| match query { - Query::Index(index) if index == height.get() => { - *call_count_inner.lock().unwrap() += 1; - Some(Payload::Finalized(Box::new(finalized.clone()))) - } - _ => None, - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - resolver.fetch(handler::Request::Finalized { height }).await; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { response, .. } => { - response.send(false).expect("deliver response dropped"); - } - _ => panic!("expected Deliver message"), - } - - let max_retry_wait = DEFAULT_FETCH_RETRY_TIMEOUT * 2 + Duration::from_millis(10); - context.sleep(max_retry_wait).await; - - let retry = ingress_rx.recv().await.unwrap(); - match retry { - handler::Message::Deliver { key, response, .. } => { - assert!( - matches!(key, handler::Request::Finalized { height: h } if h == height) - ); - response.send(true).expect("deliver response dropped"); - } - _ => panic!("expected Deliver message"), - } - - assert_eq!( - *call_count.lock().unwrap(), - 2, - "expected marshal rejection to trigger one retry" - ); - }); +fn encode_response(id: u64, value: Option) -> Vec { + let mut encoded = Vec::new(); + id.write(&mut encoded); + match value { + Some(value) => { + 1u8.write(&mut encoded); + value.write(&mut encoded); + } + None => 2u8.write(&mut encoded), } + encoded +} - /// Verifies that marshal rejecting a notarized delivery causes the - /// resolver to retry and redeliver it. - #[test_traced] - fn retries_when_marshal_rejects_notarized_delivery() { - let fixture = TestFixture::new(); - let notarized = fixture.create_notarized(3, 3); - let round = Round::new(alto_types::EPOCH, View::new(3)); - let call_count = Arc::new(Mutex::new(0u32)); - let call_count_inner = call_count.clone(); - - let source = MockSource::new(); - *source.notarized_handler.lock().unwrap() = Some(Box::new(move |_| { - *call_count_inner.lock().unwrap() += 1; - Some(notarized.clone()) - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - resolver.fetch(handler::Request::Notarized { round }).await; - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { response, .. } => { - response.send(false).expect("deliver response dropped"); - } - _ => panic!("expected Deliver message"), - } - - let max_retry_wait = DEFAULT_FETCH_RETRY_TIMEOUT * 2 + Duration::from_millis(10); - context.sleep(max_retry_wait).await; - - let retry = ingress_rx.recv().await.unwrap(); - match retry { - handler::Message::Deliver { key, response, .. } => { - assert!(matches!(key, handler::Request::Notarized { round: r } if r == round)); - response.send(true).expect("deliver response dropped"); - } - _ => panic!("expected Deliver message"), - } - - assert_eq!( - *call_count.lock().unwrap(), - 2, - "expected marshal rejection to trigger one retry" - ); - }); +async fn fetch(client: &impl Source, key: handler::Key) -> Option { + match key { + handler::Key::Block(digest) => fetch_block_by_digest(client, digest).await, + handler::Key::Finalized { height } => fetch_finalized_by_height(client, height).await, + handler::Key::Notarized { round } => fetch_notarized_by_round(client, round).await, } +} - /// Verifies that duplicate fetch requests for the same key are - /// deduplicated -- the source handler should only be called once. - #[test_traced] - fn dedup() { - let fixture = TestFixture::new(); - let block = fixture.create_block(1, 1); - let digest = block.digest(); - - let call_count = Arc::new(Mutex::new(0u32)); - let call_count_inner = call_count.clone(); - - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |_| { - *call_count_inner.lock().unwrap() += 1; - Some(Payload::Block(Box::new(block.clone()))) - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - // Send the same request twice - let key = handler::Request::::Block(digest); - resolver.fetch(key.clone()).await; - resolver.fetch(key).await; - - // Wait for the single fetch to complete - let _msg = ingress_rx.recv().await.unwrap(); - context.sleep(Duration::from_millis(100)).await; - - // Source should have been called exactly once - assert_eq!(*call_count.lock().unwrap(), 1); - }); +async fn fetch_block_by_digest(client: &impl Source, digest: Digest) -> Option { + debug!(?digest, "fetching block by digest"); + match client.block(Query::Digest(digest)).await { + Ok(Payload::Block(block)) => Some(Bytes::from(block.encode().to_vec())), + Ok(_) => { + warn!(?digest, "wrong payload returned for block by digest"); + None + } + Err(e) => { + warn!(?digest, error = ?e, "failed to fetch block by digest"); + None + } } +} - /// Verifies that repeated fetch failures continue retrying until a later - /// attempt succeeds and delivers the requested payload. - #[test_traced] - fn failed_fetch_eventually_resolves_after_multiple_retries() { - let fixture = TestFixture::new(); - let block = fixture.create_block(1, 1); - let digest = block.digest(); - - let call_count = Arc::new(Mutex::new(0u32)); - let call_count_inner = call_count.clone(); - - let source = MockSource::new(); - *source.block_handler.lock().unwrap() = Some(Box::new(move |_| { - let mut calls = call_count_inner.lock().unwrap(); - *calls += 1; - if *calls >= 3 { - Some(Payload::Block(Box::new(block.clone()))) - } else { - None - } - })); - - Runner::default().start(|context| async move { - let (ingress_tx, mut ingress_rx) = mpsc::channel(16); - let (actor, mut resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - let _actor_handle = actor.start(); - - resolver.fetch(handler::Request::Block(digest)).await; - let max_retry_wait = DEFAULT_FETCH_RETRY_TIMEOUT * 2 + Duration::from_millis(10); - for _ in 0..3 { - if *call_count.lock().unwrap() >= 3 { - break; - } - context.sleep(max_retry_wait).await; - } - - let msg = ingress_rx.recv().await.unwrap(); - match msg { - handler::Message::Deliver { key, .. } => { - assert!(matches!(key, handler::Request::Block(d) if d == digest)); - } - _ => panic!("expected Deliver message"), - } - - assert_eq!( - *call_count.lock().unwrap(), - 3, - "expected fetch to succeed on the third attempt" +async fn fetch_finalized_by_height( + client: &impl Source, + height: commonware_consensus::types::Height, +) -> Option { + debug!(height = height.get(), "fetching finalized block by height"); + match client.block(Query::Index(height.get())).await { + Ok(Payload::Finalized(finalized)) => Some(Bytes::from( + (finalized.proof.clone(), finalized.block.clone()) + .encode() + .to_vec(), + )), + Ok(_) => { + warn!( + height = height.get(), + "wrong payload returned for finalized block by height" ); - }); + None + } + Err(e) => { + warn!(height = height.get(), error = ?e, "failed to fetch finalized block by height"); + None + } } +} - /// Verifies that a stale completion from an earlier fetch attempt cannot - /// remove or reschedule a newer fetch for the same key after the original - /// request was removed and re-fetched. - #[test_traced] - fn stale_completion_does_not_mutate_replaced_request() { - let fixture = TestFixture::new(); - let digest = fixture.create_block(1, 1).digest(); - - Runner::default().start(|context| async move { - let source = MockSource::new(); - let (ingress_tx, _ingress_rx) = mpsc::channel(16); - let (mut actor, _resolver) = Actor::new( - context.with_label("resolver"), - source, - ingress_tx, - 16, - DEFAULT_FETCH_RETRY_TIMEOUT, - ); - - let key = handler::Request::::Block(digest); - actor.start_fetch(key.clone()); - let Some(State::Active { id: first_id, .. }) = actor.requests.remove(&key) else { - panic!("expected first fetch attempt to be active"); - }; - - actor.start_fetch(key.clone()); - let Some(State::Active { id: second_id, .. }) = actor.requests.get(&key) else { - panic!("expected second fetch attempt to be active"); - }; - let second_id = *second_id; - - actor.handle_completed(Result { - key: key.clone(), - id: first_id, - retry: true, - }); - - assert!(matches!( - actor.requests.get(&key), - Some(State::Active { id, .. }) if *id == second_id - )); - assert!( - actor.retry_schedule.is_empty(), - "stale completion should not schedule a retry for the replaced request" - ); - }); +async fn fetch_notarized_by_round( + client: &impl Source, + round: commonware_consensus::types::Round, +) -> Option { + let view = round.view().get(); + debug!(view, "fetching notarized block by round"); + match client.notarized(IndexQuery::Index(view)).await { + Ok(notarized) => Some(Bytes::from( + (notarized.proof.clone(), notarized.block.clone()) + .encode() + .to_vec(), + )), + Err(e) => { + warn!(view, error = ?e, "failed to fetch notarized block by round"); + None + } } } diff --git a/follower/src/test_utils.rs b/follower/src/test_utils.rs index 76c18ca7..1781fc33 100644 --- a/follower/src/test_utils.rs +++ b/follower/src/test_utils.rs @@ -36,6 +36,7 @@ pub type NotarizedHandler = #[derive(Clone)] pub struct MockSource { pub block_handler: BlockHandler, + #[allow(dead_code)] pub finalized_handler: FinalizedHandler, pub notarized_handler: NotarizedHandler, pub messages: Arc>>, diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml index fa683a71..9cb92ace 100644 --- a/indexer/Cargo.toml +++ b/indexer/Cargo.toml @@ -16,6 +16,7 @@ alto-types = { workspace = true } commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } +commonware-formatting = { workspace = true } commonware-utils = { workspace = true } commonware-parallel = { workspace = true } bytes = { workspace = true } diff --git a/indexer/src/lib.rs b/indexer/src/lib.rs index 581536be..0a43eb3e 100644 --- a/indexer/src/lib.rs +++ b/indexer/src/lib.rs @@ -11,8 +11,8 @@ use axum::{ use commonware_codec::{DecodeExt, Encode, EncodeSize, FixedSize, Write}; use commonware_consensus::{types::View, Viewable}; use commonware_cryptography::{sha256::Digest, Digestible}; +use commonware_formatting::from_hex; use commonware_parallel::Strategy; -use commonware_utils::from_hex; use futures::{SinkExt, StreamExt}; use std::{ collections::BTreeMap, diff --git a/indexer/src/main.rs b/indexer/src/main.rs index 9ea1f23e..dbd4af95 100644 --- a/indexer/src/main.rs +++ b/indexer/src/main.rs @@ -2,6 +2,7 @@ use alto_indexer::{Api, Indexer}; use alto_types::{Identity, Scheme, NAMESPACE}; use clap::Parser; use commonware_codec::DecodeExt; +use commonware_formatting::from_hex; use commonware_parallel::Sequential; use std::sync::Arc; use tracing::info; @@ -30,7 +31,7 @@ async fn main() -> Result<(), Box> { .init(); // Parse identity - let bytes = commonware_utils::from_hex(&args.identity).ok_or("Invalid identity hex format")?; + let bytes = from_hex(&args.identity).ok_or("Invalid identity hex format")?; let identity: Identity = Identity::decode(&mut bytes.as_slice()).map_err(|_| "Failed to decode identity")?; diff --git a/inspector/Cargo.toml b/inspector/Cargo.toml index 333903c8..19140ee2 100644 --- a/inspector/Cargo.toml +++ b/inspector/Cargo.toml @@ -14,6 +14,7 @@ documentation = "https://docs.rs/alto-inspector" commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } +commonware-formatting = { workspace = true } commonware-utils = { workspace = true } commonware-parallel = { workspace = true } alto-types = { workspace = true } diff --git a/inspector/src/main.rs b/inspector/src/main.rs index 25a7e201..03b3872d 100644 --- a/inspector/src/main.rs +++ b/inspector/src/main.rs @@ -83,8 +83,8 @@ use alto_client::{ use alto_types::Identity; use clap::{value_parser, Arg, Command}; use commonware_codec::DecodeExt; +use commonware_formatting::from_hex; use commonware_parallel::Sequential; -use commonware_utils::from_hex_formatted; use futures::StreamExt; use tracing::{info, warn, Level}; use utils::{ @@ -176,7 +176,7 @@ async fn main() { if let Some(matches) = matches.subcommand_matches("listen") { let indexer = matches.get_one::("indexer").unwrap(); let identity = matches.get_one::("identity").unwrap(); - let identity = from_hex_formatted(identity).expect("Failed to decode identity"); + let identity = from_hex(identity).expect("Failed to decode identity"); let identity = Identity::decode(identity.as_ref()).expect("Invalid identity"); let client = Client::new(indexer, identity, Sequential); @@ -195,7 +195,7 @@ async fn main() { let query_str = matches.get_one::("query").unwrap(); let indexer = matches.get_one::("indexer").unwrap(); let identity = matches.get_one::("identity").unwrap(); - let identity = from_hex_formatted(identity).expect("Failed to decode identity"); + let identity = from_hex(identity).expect("Failed to decode identity"); let identity = Identity::decode(identity.as_ref()).expect("Invalid identity"); let client = Client::new(indexer, identity, Sequential); let prepare_flag = matches.get_flag("prepare"); diff --git a/inspector/src/utils.rs b/inspector/src/utils.rs index 940712ab..836b7aea 100644 --- a/inspector/src/utils.rs +++ b/inspector/src/utils.rs @@ -3,6 +3,7 @@ use alto_types::{Finalized, Notarized, Seed}; use commonware_codec::DecodeExt; use commonware_consensus::Viewable; use commonware_cryptography::{sha256::Digest, Digestible}; +use commonware_formatting::from_hex; use commonware_utils::SystemTimeExt; use std::time; use tracing::{debug, info}; @@ -40,7 +41,7 @@ pub fn parse_query(query: &str) -> Option { } else if let Ok(index) = query.parse::() { Some(QueryKind::Single(Query::Index(index))) } else { - let bytes = commonware_utils::from_hex(query)?; + let bytes = from_hex(query)?; let digest = Digest::decode(bytes.as_ref()).ok()?; Some(QueryKind::Single(Query::Digest(digest))) } diff --git a/types/Cargo.toml b/types/Cargo.toml index 18488a82..fb5c8039 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["rlib", "cdylib"] commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } +commonware-formatting = { workspace = true } commonware-utils = { workspace = true } commonware-parallel = { workspace = true } bytes = { workspace = true } diff --git a/types/src/lib.rs b/types/src/lib.rs index 7c63f68e..aad79c91 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -1,7 +1,8 @@ //! Common types used throughout `alto`. use commonware_consensus::types::Epoch; -use commonware_utils::{hex, NZU64}; +use commonware_formatting::hex; +use commonware_utils::NZU64; use std::num::NonZero; mod block; diff --git a/validator/Cargo.toml b/validator/Cargo.toml index e4555686..b41d2149 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -13,6 +13,7 @@ commonware-codec = { workspace = true } commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } commonware-deployer = { workspace = true } +commonware-formatting = { workspace = true } commonware-p2p = { workspace = true } commonware-runtime = { workspace = true } commonware-utils = { workspace = true } diff --git a/validator/src/main.rs b/validator/src/main.rs index 5bfc6c3f..2e342231 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -14,9 +14,10 @@ use commonware_cryptography::{ Signer, }; use commonware_deployer::aws::Hosts; +use commonware_formatting::from_hex; use commonware_p2p::{authenticated::discovery as authenticated, Ingress, Manager}; -use commonware_runtime::{tokio, Metrics, Runner, ThreadPooler}; -use commonware_utils::{from_hex_formatted, ordered::Set, union_unique, NZUsize, NZU32}; +use commonware_runtime::{tokio, Runner, Supervisor as _, ThreadPooler}; +use commonware_utils::{ordered::Set, union_unique, NZUsize, NZU32}; use futures::future::try_join_all; use governor::Quota; use std::{ @@ -69,7 +70,7 @@ fn main() { let config_file = matches.get_one::("config").unwrap(); let config_file = std::fs::read_to_string(config_file).expect("Could not read config file"); let config: Config = serde_yaml::from_str(&config_file).expect("Could not parse config file"); - let key = from_hex_formatted(&config.private_key).expect("Could not parse private key"); + let key = from_hex(&config.private_key).expect("Could not parse private key"); let signer = PrivateKey::decode(key.as_ref()).expect("Private key is invalid"); let public_key = signer.public_key(); @@ -86,7 +87,7 @@ fn main() { // Configure telemetry let log_level = Level::from_str(&config.log_level).expect("Invalid log level"); tokio::telemetry::init( - context.with_label("telemetry"), + context.child("telemetry"), tokio::telemetry::Logging { level: log_level, // If we are using `commonware-deployer`, we should use structured logging. @@ -108,7 +109,7 @@ fn main() { .hosts .into_iter() .map(|peer| { - let key = from_hex_formatted(&peer.name).expect("Could not parse peer key"); + let key = from_hex(&peer.name).expect("Could not parse peer key"); let key = PublicKey::decode(key.as_ref()).expect("Peer key is invalid"); (key, peer.ip) }) @@ -117,8 +118,7 @@ fn main() { let peer_keys = peers.keys().cloned().collect::>(); let mut bootstrappers = Vec::new(); for bootstrapper in &config.bootstrappers { - let key = - from_hex_formatted(bootstrapper).expect("Could not parse bootstrapper key"); + let key = from_hex(bootstrapper).expect("Could not parse bootstrapper key"); let key = PublicKey::decode(key.as_ref()).expect("Bootstrapper key is invalid"); let ip = peers.get(&key).expect("Could not find bootstrapper in IPs"); let bootstrapper_socket = format!("{}:{}", ip, config.port); @@ -136,7 +136,7 @@ fn main() { .addresses .into_iter() .map(|peer| { - let key = from_hex_formatted(&peer.0).expect("Could not parse peer key"); + let key = from_hex(&peer.0).expect("Could not parse peer key"); let key = PublicKey::decode(key.as_ref()).expect("Peer key is invalid"); (key, peer.1) }) @@ -145,8 +145,7 @@ fn main() { let peer_keys = peers.keys().cloned().collect::>(); let mut bootstrappers = Vec::new(); for bootstrapper in &config.bootstrappers { - let key = - from_hex_formatted(bootstrapper).expect("Could not parse bootstrapper key"); + let key = from_hex(bootstrapper).expect("Could not parse bootstrapper key"); let key = PublicKey::decode(key.as_ref()).expect("Bootstrapper key is invalid"); let socket = peers.get(&key).expect("Could not find bootstrapper in IPs"); bootstrappers.push((key, Ingress::Socket(*socket))); @@ -161,10 +160,9 @@ fn main() { let peers_u32 = peers.len() as u32; // Parse config - let share = from_hex_formatted(&config.share).expect("Could not parse share"); + let share = from_hex(&config.share).expect("Could not parse share"); let share = group::Share::decode(share.as_ref()).expect("Share is invalid"); - let polynomial = - from_hex_formatted(&config.polynomial).expect("Could not parse polynomial"); + let polynomial = from_hex(&config.polynomial).expect("Could not parse polynomial"); let polynomial = Sharing::::decode_cfg( polynomial.as_ref(), &(NZU32!(peers_u32), ModeVersion::v0()), @@ -200,15 +198,15 @@ fn main() { MAX_MESSAGE_SIZE, ) }; - p2p_cfg.mailbox_size = config.mailbox_size; + p2p_cfg.mailbox_size = NZUsize!(config.mailbox_size); // Start p2p let (mut network, mut oracle) = - authenticated::Network::new(context.with_label("network"), p2p_cfg); + authenticated::Network::new(context.child("network"), p2p_cfg); // Provide authorized peers let participants: Set = Set::from_iter_dedup(peers.clone()); - oracle.track(EPOCH.get(), participants.clone()).await; + oracle.track(EPOCH.get(), participants.clone()); // Register pending channel let pending_limit = Quota::per_second(NonZeroU32::new(128).unwrap()); @@ -276,21 +274,24 @@ fn main() { share, strategy, }; - let engine = engine::Engine::new(context.with_label("engine"), engine_cfg).await; + let engine = engine::Engine::new(context.child("engine"), engine_cfg).await; let marshal_resolver_cfg = marshal::resolver::p2p::Config { public_key: public_key.clone(), peer_provider: oracle.clone(), blocker: oracle, - mailbox_size: config.mailbox_size, + mailbox_size: NZUsize!(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 marshal_resolver = - marshal::resolver::p2p::init(&context, marshal_resolver_cfg, marshal); + let marshal_resolver = marshal::resolver::p2p::init( + context.child("marshal_resolver"), + marshal_resolver_cfg, + marshal, + ); // Start engine let engine = engine.start(pending, recovered, resolver, broadcaster, marshal_resolver); From 0d8af23bbd4bd633611affef149966d7510a2a7d Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Thu, 21 May 2026 07:45:57 -0700 Subject: [PATCH 02/11] fix docs --- follower/src/engine.rs | 2 +- follower/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/follower/src/engine.rs b/follower/src/engine.rs index f991c1aa..78225e12 100644 --- a/follower/src/engine.rs +++ b/follower/src/engine.rs @@ -61,7 +61,7 @@ fn genesis() -> Block { /// /// Unlike the validator's engine, this does not run consensus. Instead, it /// relies on a [Feeder](crate::feeder::Feeder) to feed certificates from a -/// trusted source and an [Actor](crate::resolver::Actor) to backfill missing +/// trusted source and a [Resolver] to backfill missing /// blocks. #[allow(clippy::type_complexity)] pub struct Engine diff --git a/follower/src/main.rs b/follower/src/main.rs index b4cadb2b..c3327d23 100644 --- a/follower/src/main.rs +++ b/follower/src/main.rs @@ -48,7 +48,7 @@ pub struct Config { } /// Abstraction over the certificate source (HTTP client) used by the -/// [feeder::Feeder] and [resolver::Actor]. +/// [feeder::Feeder] and [resolver::Resolver]. #[allow(dead_code)] pub(crate) trait Source: Clone + Send + Sync + 'static { type Error: std::error::Error + Send + Sync + 'static; From 49e05ff64f867305697fa4b61e9d6daca057b88a Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Thu, 21 May 2026 13:03:08 -0700 Subject: [PATCH 03/11] fix buffer sizing --- chain/src/lib.rs | 29 ++++++++++++++- deploy/src/main.rs | 82 ++++++++++++++++++++++++++++++++++++++++++- validator/src/main.rs | 31 +++++++++++++++- 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/chain/src/lib.rs b/chain/src/lib.rs index d37f6cc6..1789541e 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -1,6 +1,10 @@ use commonware_utils::NZUsize; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize}; +use std::{ + collections::HashMap, + net::SocketAddr, + num::{NonZeroU32, NonZeroUsize}, +}; pub mod application; pub mod engine; @@ -10,6 +14,13 @@ pub mod utils; pub const DEFAULT_BACKFILLER_MAX_ACTIVE: NonZeroUsize = NZUsize!(16); pub const DEFAULT_BACKFILLER_RETRY_MS: u64 = 1_000; +/// Default Tokio blocking thread cap used by the validator. +/// +/// This must stay aligned with the storage buffer-pool parallelism calculation: +/// storage I/O can run on blocking threads, so undercounting them allows +/// thread-local caches to strand buffers and can look like pool exhaustion. +pub const DEFAULT_BLOCKING_THREADS: usize = 512; + fn default_backfiller_max_active() -> NonZeroUsize { DEFAULT_BACKFILLER_MAX_ACTIVE } @@ -18,6 +29,10 @@ fn default_backfiller_retry_ms() -> u64 { DEFAULT_BACKFILLER_RETRY_MS } +fn default_blocking_threads() -> usize { + DEFAULT_BLOCKING_THREADS +} + /// Configuration for the [engine::Engine]. #[derive(Deserialize, Serialize)] pub struct Config { @@ -29,6 +44,18 @@ pub struct Config { pub metrics_port: u16, pub directory: String, pub worker_threads: usize, + /// Maximum Tokio blocking threads. Storage buffer-pool parallelism includes + /// this count because blocking storage tasks can hold pool buffers. + #[serde(default = "default_blocking_threads")] + pub blocking_threads: usize, + #[serde(default)] + pub storage_buffer_pool_max_per_class: Option, + #[serde(default)] + pub network_buffer_pool_max_per_class: Option, + #[serde(default)] + pub storage_buffer_pool_parallelism: Option, + #[serde(default)] + pub network_buffer_pool_parallelism: Option, pub log_level: String, pub local: bool, diff --git a/deploy/src/main.rs b/deploy/src/main.rs index bc1460af..36e4f4fb 100644 --- a/deploy/src/main.rs +++ b/deploy/src/main.rs @@ -1,4 +1,7 @@ -use alto_chain::{Config, Peers, DEFAULT_BACKFILLER_MAX_ACTIVE, DEFAULT_BACKFILLER_RETRY_MS}; +use alto_chain::{ + Config, Peers, DEFAULT_BACKFILLER_MAX_ACTIVE, DEFAULT_BACKFILLER_RETRY_MS, + DEFAULT_BLOCKING_THREADS, +}; use alto_types::NAMESPACE; use clap::{value_parser, Arg, ArgMatches, Command}; use commonware_codec::{Decode, DecodeExt, Encode}; @@ -21,6 +24,7 @@ use std::{ collections::{BTreeMap, HashMap}, fs, net::{IpAddr, Ipv4Addr, SocketAddr}, + num::{NonZeroU32, NonZeroUsize}, }; use tracing::{error, info}; use uuid::Uuid; @@ -112,6 +116,36 @@ fn main() { .required(true) .value_parser(value_parser!(usize)), ) + .arg( + Arg::new("blocking_threads") + .long("blocking-threads") + .required(false) + .value_parser(value_parser!(usize)), + ) + .arg( + Arg::new("storage_buffer_pool_max_per_class") + .long("storage-buffer-pool-max-per-class") + .required(false) + .value_parser(value_parser!(NonZeroU32)), + ) + .arg( + Arg::new("network_buffer_pool_max_per_class") + .long("network-buffer-pool-max-per-class") + .required(false) + .value_parser(value_parser!(NonZeroU32)), + ) + .arg( + Arg::new("storage_buffer_pool_parallelism") + .long("storage-buffer-pool-parallelism") + .required(false) + .value_parser(value_parser!(NonZeroUsize)), + ) + .arg( + Arg::new("network_buffer_pool_parallelism") + .long("network-buffer-pool-parallelism") + .required(false) + .value_parser(value_parser!(NonZeroUsize)), + ) .arg( Arg::new("log_level") .long("log-level") @@ -238,6 +272,22 @@ fn main() { let peers = *sub_matches.get_one::("peers").unwrap(); let bootstrappers = *sub_matches.get_one::("bootstrappers").unwrap(); let worker_threads = *sub_matches.get_one::("worker_threads").unwrap(); + let blocking_threads = sub_matches + .get_one::("blocking_threads") + .copied() + .unwrap_or(DEFAULT_BLOCKING_THREADS); + let storage_buffer_pool_max_per_class = sub_matches + .get_one::("storage_buffer_pool_max_per_class") + .copied(); + let network_buffer_pool_max_per_class = sub_matches + .get_one::("network_buffer_pool_max_per_class") + .copied(); + let storage_buffer_pool_parallelism = sub_matches + .get_one::("storage_buffer_pool_parallelism") + .copied(); + let network_buffer_pool_parallelism = sub_matches + .get_one::("network_buffer_pool_parallelism") + .copied(); let log_level = sub_matches.get_one::("log_level").unwrap().clone(); let message_backlog = *sub_matches.get_one::("message_backlog").unwrap(); let mailbox_size = *sub_matches.get_one::("mailbox_size").unwrap(); @@ -250,6 +300,11 @@ fn main() { peers, bootstrappers, worker_threads, + blocking_threads, + storage_buffer_pool_max_per_class, + network_buffer_pool_max_per_class, + storage_buffer_pool_parallelism, + network_buffer_pool_parallelism, log_level, message_backlog, mailbox_size, @@ -262,6 +317,11 @@ fn main() { peers, bootstrappers, worker_threads, + blocking_threads, + storage_buffer_pool_max_per_class, + network_buffer_pool_max_per_class, + storage_buffer_pool_parallelism, + network_buffer_pool_parallelism, log_level, message_backlog, mailbox_size, @@ -303,6 +363,11 @@ fn generate_local( peers: usize, bootstrappers: usize, worker_threads: usize, + blocking_threads: usize, + storage_buffer_pool_max_per_class: Option, + network_buffer_pool_max_per_class: Option, + storage_buffer_pool_parallelism: Option, + network_buffer_pool_parallelism: Option, log_level: String, message_backlog: usize, mailbox_size: usize, @@ -376,6 +441,11 @@ fn generate_local( metrics_port: port + 1, directory, worker_threads, + blocking_threads, + storage_buffer_pool_max_per_class, + network_buffer_pool_max_per_class, + storage_buffer_pool_parallelism, + network_buffer_pool_parallelism, log_level: log_level.clone(), local: true, @@ -478,6 +548,11 @@ fn generate_remote( peers: usize, bootstrappers: usize, worker_threads: usize, + blocking_threads: usize, + storage_buffer_pool_max_per_class: Option, + network_buffer_pool_max_per_class: Option, + storage_buffer_pool_parallelism: Option, + network_buffer_pool_parallelism: Option, log_level: String, message_backlog: usize, mailbox_size: usize, @@ -569,6 +644,11 @@ fn generate_remote( metrics_port: METRICS_PORT, directory: "/home/ubuntu/data".to_string(), worker_threads, + blocking_threads, + storage_buffer_pool_max_per_class, + network_buffer_pool_max_per_class, + storage_buffer_pool_parallelism, + network_buffer_pool_parallelism, log_level: log_level.clone(), local: false, diff --git a/validator/src/main.rs b/validator/src/main.rs index 2e342231..3d93cf4a 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -16,7 +16,7 @@ use commonware_cryptography::{ use commonware_deployer::aws::Hosts; use commonware_formatting::from_hex; use commonware_p2p::{authenticated::discovery as authenticated, Ingress, Manager}; -use commonware_runtime::{tokio, Runner, Supervisor as _, ThreadPooler}; +use commonware_runtime::{tokio, BufferPoolConfig, Runner, Supervisor as _, ThreadPooler}; use commonware_utils::{ordered::Set, union_unique, NZUsize, NZU32}; use futures::future::try_join_all; use governor::Quota; @@ -75,10 +75,39 @@ fn main() { let public_key = signer.public_key(); // Initialize runtime + let network_buffer_pool_parallelism = config + .worker_threads + .checked_add(config.signature_threads) + .expect("network buffer pool parallelism overflowed"); + // Storage I/O runs on Tokio's blocking pool. Include those threads in the + // pool parallelism calculation so buffers cannot be stranded in too few + // thread-local caches and surface as exhaustion under restart pressure. + let storage_buffer_pool_parallelism = network_buffer_pool_parallelism + .checked_add(config.blocking_threads) + .expect("storage buffer pool parallelism overflowed"); + let mut storage_buffer_pool_cfg = BufferPoolConfig::for_storage().with_parallelism( + config + .storage_buffer_pool_parallelism + .unwrap_or(NZUsize!(storage_buffer_pool_parallelism)), + ); + if let Some(max_per_class) = config.storage_buffer_pool_max_per_class { + storage_buffer_pool_cfg = storage_buffer_pool_cfg.with_max_per_class(max_per_class); + } + let mut network_buffer_pool_cfg = BufferPoolConfig::for_network().with_parallelism( + config + .network_buffer_pool_parallelism + .unwrap_or(NZUsize!(network_buffer_pool_parallelism)), + ); + if let Some(max_per_class) = config.network_buffer_pool_max_per_class { + network_buffer_pool_cfg = network_buffer_pool_cfg.with_max_per_class(max_per_class); + } let cfg = tokio::Config::default() .with_tcp_nodelay(Some(true)) .with_worker_threads(config.worker_threads) + .with_max_blocking_threads(config.blocking_threads) .with_storage_directory(PathBuf::from(config.directory)) + .with_storage_buffer_pool_config(storage_buffer_pool_cfg) + .with_network_buffer_pool_config(network_buffer_pool_cfg) .with_catch_panics(false); let executor = tokio::Runner::new(cfg); From 177d5670ce9649504ef8d811b520890670aceadc Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 22 May 2026 15:24:23 -0700 Subject: [PATCH 04/11] nits --- Cargo.lock | 588 +++++++++++++++++++----------------- Cargo.toml | 32 +- chain/src/application.rs | 40 ++- follower/src/application.rs | 3 +- follower/src/resolver.rs | 10 +- 5 files changed, 376 insertions(+), 297 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e329d1f2..d15007b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,11 +63,11 @@ dependencies = [ "futures", "governor", "prometheus-client", - "rand 0.8.5", + "rand 0.8.6", "serde", "thiserror", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -83,13 +83,13 @@ dependencies = [ "commonware-parallel", "commonware-utils", "futures", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rustls", "rustls-native-certs", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", ] [[package]] @@ -107,11 +107,11 @@ dependencies = [ "commonware-macros", "commonware-math", "commonware-utils", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_yaml", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "uuid", ] @@ -139,13 +139,13 @@ dependencies = [ "commonware-utils", "futures", "governor", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_yaml", "thiserror", "tokio", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -166,18 +166,18 @@ dependencies = [ "futures", "hyper", "hyper-util", - "rand 0.8.5", + "rand 0.8.6", "rcgen", "reqwest", "rustls", "thiserror", "tokio", "tokio-rustls", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tower", "tower-http", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -195,11 +195,11 @@ dependencies = [ "commonware-parallel", "commonware-utils", "futures", - "rand 0.8.5", + "rand 0.8.6", "thiserror", "tokio", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -215,7 +215,7 @@ dependencies = [ "commonware-parallel", "commonware-utils", "getrandom 0.3.4", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde-wasm-bindgen", "thiserror", @@ -252,9 +252,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -267,15 +267,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -470,7 +470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -546,9 +546,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "base64", @@ -602,7 +602,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -646,24 +646,33 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.3.0", "zeroize", ] @@ -690,9 +699,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -708,9 +717,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -738,7 +747,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -794,9 +803,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -804,9 +813,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -816,9 +825,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -828,15 +837,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -849,14 +858,14 @@ checksum = "5417da527aa9bf6a1e10a781231effd1edd3ee82f27d5f8529ac9b279babce96" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commonware-actor" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "cfg-if", "commonware-macros", @@ -869,7 +878,7 @@ dependencies = [ [[package]] name = "commonware-broadcast" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "commonware-actor", "commonware-codec", @@ -885,13 +894,13 @@ dependencies = [ [[package]] name = "commonware-codec" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "cfg-if", "commonware-macros", "paste", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "thiserror", ] @@ -899,7 +908,7 @@ dependencies = [ [[package]] name = "commonware-coding" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "commonware-codec", @@ -910,7 +919,7 @@ dependencies = [ "commonware-storage", "commonware-utils", "num-rational", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rayon", "reed-solomon-simd", @@ -920,7 +929,7 @@ dependencies = [ [[package]] name = "commonware-consensus" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "cfg-if", @@ -940,7 +949,7 @@ dependencies = [ "commonware-utils", "futures", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rand_distr", "rayon", @@ -951,7 +960,7 @@ dependencies = [ [[package]] name = "commonware-cryptography" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "anyhow", "ark-ec", @@ -980,7 +989,7 @@ dependencies = [ "num-rational", "num-traits", "p256", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_core 0.6.4", "sha2", @@ -992,7 +1001,7 @@ dependencies = [ [[package]] name = "commonware-deployer" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "cfg-if", "commonware-macros", @@ -1002,7 +1011,7 @@ dependencies = [ [[package]] name = "commonware-formatting" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "commonware-macros", "const-hex", @@ -1011,7 +1020,7 @@ dependencies = [ [[package]] name = "commonware-macros" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "commonware-macros-impl", "tokio", @@ -1020,7 +1029,7 @@ dependencies = [ [[package]] name = "commonware-macros-impl" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1032,7 +1041,7 @@ dependencies = [ [[package]] name = "commonware-math" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "commonware-codec", @@ -1045,7 +1054,7 @@ dependencies = [ [[package]] name = "commonware-p2p" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "commonware-actor", "commonware-codec", @@ -1061,7 +1070,7 @@ dependencies = [ "num-integer", "num-rational", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rand_distr", "thiserror", @@ -1071,7 +1080,7 @@ dependencies = [ [[package]] name = "commonware-parallel" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "cfg-if", "commonware-macros", @@ -1081,7 +1090,7 @@ dependencies = [ [[package]] name = "commonware-resolver" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "commonware-actor", @@ -1093,7 +1102,7 @@ dependencies = [ "commonware-stream", "commonware-utils", "futures", - "rand 0.8.5", + "rand 0.8.6", "thiserror", "tracing", ] @@ -1101,7 +1110,7 @@ dependencies = [ [[package]] name = "commonware-runtime" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "axum", "bytes", @@ -1123,7 +1132,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "prometheus-client", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rayon", "sha2", @@ -1132,13 +1141,13 @@ dependencies = [ "tokio", "tracing", "tracing-opentelemetry", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] name = "commonware-runtime-macros" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1149,7 +1158,7 @@ dependencies = [ [[package]] name = "commonware-storage" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "ahash", "anyhow", @@ -1172,7 +1181,7 @@ dependencies = [ [[package]] name = "commonware-stream" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "chacha20poly1305", "commonware-codec", @@ -1182,7 +1191,7 @@ dependencies = [ "commonware-runtime", "commonware-utils", "futures", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "thiserror", "x25519-dalek", @@ -1192,7 +1201,7 @@ dependencies = [ [[package]] name = "commonware-utils" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=d8589a085d88216f730e71aeb0488c27aa8ef224#d8589a085d88216f730e71aeb0488c27aa8ef224" +source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" dependencies = [ "bytes", "cfg-if", @@ -1208,7 +1217,7 @@ dependencies = [ "num-traits", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "thiserror", "tokio", "zeroize", @@ -1221,7 +1230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1263,6 +1272,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-fast" version = "1.10.0" @@ -1385,7 +1403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1407,9 +1425,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1421,9 +1439,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -1521,9 +1539,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -1714,9 +1732,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -1809,7 +1827,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.9.2", + "rand 0.9.4", "smallvec", "spinning_top", "web-time", @@ -1828,9 +1846,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1883,6 +1901,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -1951,9 +1975,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1966,7 +1990,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1974,16 +1997,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -2015,12 +2037,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2028,9 +2051,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2041,9 +2064,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2055,15 +2078,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2075,15 +2098,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2113,9 +2136,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2123,12 +2146,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2148,16 +2171,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2184,9 +2197,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -2200,10 +2213,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2222,9 +2237,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -2234,9 +2249,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2294,9 +2309,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2349,9 +2364,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2423,9 +2438,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2480,9 +2495,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ "http", "opentelemetry", @@ -2519,7 +2534,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", + "rand 0.9.4", "thiserror", "tokio", "tokio-stream", @@ -2584,18 +2599,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2608,12 +2623,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkcs8" version = "0.10.2" @@ -2626,9 +2635,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plotters" @@ -2664,7 +2673,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", ] @@ -2677,9 +2686,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2738,9 +2747,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" +checksum = "cca3d75b4566b9a29fe1ed623587fb058e826eb329a0be4b7c4da1ebb2d7a6ca" dependencies = [ "dtoa", "itoa", @@ -2767,7 +2776,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bitflags", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -2841,7 +2850,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -2890,9 +2899,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2901,9 +2910,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2954,7 +2963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2977,9 +2986,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2997,9 +3006,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", @@ -3030,7 +3039,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffef0520d30fbd4151fb20e262947ae47fb0ab276a744a19b6398438105a072" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "fixedbitset", "once_cell", "readme-rustdocifier", @@ -3133,9 +3142,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3157,9 +3166,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -3185,9 +3194,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -3195,9 +3204,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3228,9 +3237,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3280,9 +3289,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -3327,9 +3336,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3351,9 +3360,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3390,7 +3399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3401,7 +3410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3621,9 +3630,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3641,9 +3650,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3656,9 +3665,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3673,9 +3682,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3716,7 +3725,19 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tungstenite", + "tungstenite 0.28.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", ] [[package]] @@ -3744,7 +3765,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3758,45 +3779,45 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "base64", @@ -3815,9 +3836,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -3842,20 +3863,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3926,7 +3947,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "web-time", ] @@ -3951,9 +3972,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3987,7 +4008,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "rustls", "rustls-pki-types", "sha1", @@ -3995,11 +4016,27 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror", +] + [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unarray" @@ -4079,9 +4116,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4127,11 +4164,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -4140,14 +4177,14 @@ 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", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -4158,23 +4195,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4182,9 +4215,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -4195,9 +4228,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -4238,9 +4271,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -4258,9 +4291,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -4574,6 +4607,12 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -4587,6 +4626,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -4668,9 +4713,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x25519-dalek" @@ -4704,18 +4749,19 @@ dependencies = [ [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec", "time", ] [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4724,9 +4770,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4736,18 +4782,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4756,18 +4802,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4797,9 +4843,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4808,9 +4854,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4819,9 +4865,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0b41b7ee..7cd8d8fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,22 +20,22 @@ license = "MIT OR Apache-2.0" alto-chain = { version = "2026.3.0", path = "chain" } alto-client = { version = "2026.3.0", path = "client" } alto-types = { version = "2026.3.0", path = "types" } -commonware-actor = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-consensus = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-cryptography = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-deployer = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224", default-features = false } -commonware-formatting = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-macros = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-p2p = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-resolver = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-runtime = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-storage = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-stream = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-utils = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-math = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } -commonware-parallel = { git = "https://github.com/commonwarexyz/monorepo", rev = "d8589a085d88216f730e71aeb0488c27aa8ef224" } +commonware-actor = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-consensus = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-cryptography = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-deployer = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124", default-features = false } +commonware-formatting = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-macros = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-p2p = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-resolver = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-runtime = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-storage = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-stream = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-utils = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-math = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-parallel = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } thiserror = "2.0.12" bytes = "1.7.1" rand = "0.8.5" diff --git a/chain/src/application.rs b/chain/src/application.rs index a89f5d75..1bb9061f 100644 --- a/chain/src/application.rs +++ b/chain/src/application.rs @@ -156,7 +156,41 @@ impl Reporter for Application { mod tests { use super::*; use commonware_runtime::{deterministic, Runner as _, Supervisor as _}; - use futures::stream; + use futures::Stream; + use std::{ + collections::VecDeque, + pin::Pin, + task::{Context as TaskContext, Poll}, + }; + + struct TestAncestry { + blocks: VecDeque, + } + + impl TestAncestry { + fn new(blocks: impl IntoIterator) -> Self { + Self { + blocks: blocks.into_iter().collect(), + } + } + } + + impl Stream for TestAncestry { + type Item = Block; + + fn poll_next( + mut self: Pin<&mut Self>, + _: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(self.blocks.pop_front()) + } + } + + impl Ancestry for TestAncestry { + fn peek(&self) -> Option<&Block> { + self.blocks.front() + } + } fn test_context(view: u64, parent: (View, sha256::Digest)) -> Context { Context { @@ -178,7 +212,7 @@ mod tests { block: &Block, parent: &Block, ) -> bool { - let ancestry = stream::iter([block.clone(), parent.clone()]); + let ancestry = TestAncestry::new([block.clone(), parent.clone()]); commonware_consensus::Application::verify( application, (context, block.context.clone()), @@ -193,7 +227,7 @@ mod tests { child_context: Context, parent: &Block, ) -> Block { - let ancestry = stream::iter([parent.clone()]); + let ancestry = TestAncestry::new([parent.clone()]); commonware_consensus::Application::propose(application, (context, child_context), ancestry) .await .expect("expected proposal") diff --git a/follower/src/application.rs b/follower/src/application.rs index e6d9d6b1..183046a2 100644 --- a/follower/src/application.rs +++ b/follower/src/application.rs @@ -56,9 +56,8 @@ struct Message(Update); impl Policy for Message { type Overflow = VecDeque; - fn handle(overflow: &mut Self::Overflow, message: Self) -> bool { + fn handle(overflow: &mut Self::Overflow, message: Self) { overflow.push_back(message); - true } } diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index a32219d4..169c8f4b 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -1,7 +1,7 @@ use crate::Source; use alto_client::{consensus::Payload, IndexQuery, Query}; use bytes::{Buf, Bytes}; -use commonware_actor::Feedback; +use commonware_actor::{Feedback, Unreliable}; use commonware_codec::{Encode, ReadExt, Write}; use commonware_consensus::marshal::resolver::{ handler, @@ -120,14 +120,14 @@ impl CheckedSender for SourceCheckedSender { vec![self.peer.clone()] } - fn send(self, message: impl Into + Send, _priority: bool) -> Feedback { + fn send(self, message: impl Into + Send, _priority: bool) -> Unreliable { let Some(request) = decode_request(message) else { - return Feedback::Rejected; + return Unreliable::Rejected; }; if self.requests.try_send_lossy(request) { - Feedback::Ok + Unreliable::new(Feedback::Ok) } else { - Feedback::Rejected + Unreliable::Rejected } } } From 016f1778e989ce38b3bb869f62e927fc6bf58adf Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 22 May 2026 16:44:12 -0700 Subject: [PATCH 05/11] cleanup --- Cargo.lock | 40 ++- Cargo.toml | 32 +- chain/src/application.rs | 40 +-- chain/src/engine.rs | 2 +- follower/Cargo.toml | 2 - follower/src/engine.rs | 15 +- follower/src/main.rs | 2 +- follower/src/resolver.rs | 657 +++++++++++++++++++++++++-------------- 8 files changed, 468 insertions(+), 322 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d15007b1..675bc52d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,14 +124,12 @@ dependencies = [ "bytes", "clap", "commonware-actor", - "commonware-broadcast", "commonware-codec", "commonware-consensus", "commonware-cryptography", "commonware-formatting", "commonware-macros", "commonware-math", - "commonware-p2p", "commonware-parallel", "commonware-resolver", "commonware-runtime", @@ -865,7 +863,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commonware-actor" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "cfg-if", "commonware-macros", @@ -878,7 +876,7 @@ dependencies = [ [[package]] name = "commonware-broadcast" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "commonware-actor", "commonware-codec", @@ -894,7 +892,7 @@ dependencies = [ [[package]] name = "commonware-codec" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "cfg-if", @@ -908,7 +906,7 @@ dependencies = [ [[package]] name = "commonware-coding" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "commonware-codec", @@ -929,7 +927,7 @@ dependencies = [ [[package]] name = "commonware-consensus" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "cfg-if", @@ -960,7 +958,7 @@ dependencies = [ [[package]] name = "commonware-cryptography" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "anyhow", "ark-ec", @@ -1001,7 +999,7 @@ dependencies = [ [[package]] name = "commonware-deployer" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "cfg-if", "commonware-macros", @@ -1011,7 +1009,7 @@ dependencies = [ [[package]] name = "commonware-formatting" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "commonware-macros", "const-hex", @@ -1020,7 +1018,7 @@ dependencies = [ [[package]] name = "commonware-macros" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "commonware-macros-impl", "tokio", @@ -1029,7 +1027,7 @@ dependencies = [ [[package]] name = "commonware-macros-impl" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1041,7 +1039,7 @@ dependencies = [ [[package]] name = "commonware-math" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "commonware-codec", @@ -1054,7 +1052,7 @@ dependencies = [ [[package]] name = "commonware-p2p" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "commonware-actor", "commonware-codec", @@ -1080,7 +1078,7 @@ dependencies = [ [[package]] name = "commonware-parallel" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "cfg-if", "commonware-macros", @@ -1090,7 +1088,7 @@ dependencies = [ [[package]] name = "commonware-resolver" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "commonware-actor", @@ -1110,7 +1108,7 @@ dependencies = [ [[package]] name = "commonware-runtime" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "axum", "bytes", @@ -1147,7 +1145,7 @@ dependencies = [ [[package]] name = "commonware-runtime-macros" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1158,7 +1156,7 @@ dependencies = [ [[package]] name = "commonware-storage" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "ahash", "anyhow", @@ -1181,7 +1179,7 @@ dependencies = [ [[package]] name = "commonware-stream" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "chacha20poly1305", "commonware-codec", @@ -1201,7 +1199,7 @@ dependencies = [ [[package]] name = "commonware-utils" version = "2026.4.0" -source = "git+https://github.com/commonwarexyz/monorepo?rev=86f9be7c6339339b37a420eed7ef640164999124#86f9be7c6339339b37a420eed7ef640164999124" +source = "git+https://github.com/commonwarexyz/monorepo?rev=325c42c643b8a43ee2d14387d8bc9771d7a40985#325c42c643b8a43ee2d14387d8bc9771d7a40985" dependencies = [ "bytes", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 7cd8d8fd..1d2faef8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,22 +20,22 @@ license = "MIT OR Apache-2.0" alto-chain = { version = "2026.3.0", path = "chain" } alto-client = { version = "2026.3.0", path = "client" } alto-types = { version = "2026.3.0", path = "types" } -commonware-actor = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-consensus = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-cryptography = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-deployer = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124", default-features = false } -commonware-formatting = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-macros = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-p2p = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-resolver = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-runtime = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-storage = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-stream = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-utils = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-math = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } -commonware-parallel = { git = "https://github.com/commonwarexyz/monorepo", rev = "86f9be7c6339339b37a420eed7ef640164999124" } +commonware-actor = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-broadcast = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-codec = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-consensus = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-cryptography = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-deployer = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985", default-features = false } +commonware-formatting = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-macros = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-p2p = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-resolver = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-runtime = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-storage = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-stream = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-utils = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-math = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } +commonware-parallel = { git = "https://github.com/commonwarexyz/monorepo", rev = "325c42c643b8a43ee2d14387d8bc9771d7a40985" } thiserror = "2.0.12" bytes = "1.7.1" rand = "0.8.5" diff --git a/chain/src/application.rs b/chain/src/application.rs index 1bb9061f..a486a25b 100644 --- a/chain/src/application.rs +++ b/chain/src/application.rs @@ -155,42 +155,8 @@ impl Reporter for Application { #[cfg(test)] mod tests { use super::*; + use commonware_consensus::marshal::ancestry; use commonware_runtime::{deterministic, Runner as _, Supervisor as _}; - use futures::Stream; - use std::{ - collections::VecDeque, - pin::Pin, - task::{Context as TaskContext, Poll}, - }; - - struct TestAncestry { - blocks: VecDeque, - } - - impl TestAncestry { - fn new(blocks: impl IntoIterator) -> Self { - Self { - blocks: blocks.into_iter().collect(), - } - } - } - - impl Stream for TestAncestry { - type Item = Block; - - fn poll_next( - mut self: Pin<&mut Self>, - _: &mut TaskContext<'_>, - ) -> Poll> { - Poll::Ready(self.blocks.pop_front()) - } - } - - impl Ancestry for TestAncestry { - fn peek(&self) -> Option<&Block> { - self.blocks.front() - } - } fn test_context(view: u64, parent: (View, sha256::Digest)) -> Context { Context { @@ -212,7 +178,7 @@ mod tests { block: &Block, parent: &Block, ) -> bool { - let ancestry = TestAncestry::new([block.clone(), parent.clone()]); + let ancestry = ancestry::from_iter([block.clone(), parent.clone()]); commonware_consensus::Application::verify( application, (context, block.context.clone()), @@ -227,7 +193,7 @@ mod tests { child_context: Context, parent: &Block, ) -> Block { - let ancestry = TestAncestry::new([parent.clone()]); + let ancestry = ancestry::from_iter([parent.clone()]); commonware_consensus::Application::propose(application, (context, child_context), ancestry) .await .expect("expected proposal") diff --git a/chain/src/engine.rs b/chain/src/engine.rs index be41f285..a61b2717 100644 --- a/chain/src/engine.rs +++ b/chain/src/engine.rs @@ -430,7 +430,7 @@ where // Start marshal let marshal_handle = self .marshal - .start(self.marshaled, Some(self.buffer_mailbox), marshal); + .start(self.marshaled, self.buffer_mailbox, marshal); // Start draining queued block uploads before consensus so recovered work // resumes immediately on startup. diff --git a/follower/Cargo.toml b/follower/Cargo.toml index 7b153bb3..7d6189e1 100644 --- a/follower/Cargo.toml +++ b/follower/Cargo.toml @@ -8,7 +8,6 @@ description = "Run a follower node for alto." [dependencies] alto-client = { workspace = true } alto-types = { workspace = true } -commonware-broadcast = { workspace = true } commonware-actor = { workspace = true } commonware-codec = { workspace = true } commonware-consensus = { workspace = true } @@ -16,7 +15,6 @@ commonware-cryptography = { workspace = true } commonware-formatting = { workspace = true } commonware-macros = { workspace = true } commonware-math = { workspace = true } -commonware-p2p = { workspace = true } commonware-resolver = { workspace = true } commonware-runtime = { workspace = true } commonware-storage = { workspace = true } diff --git a/follower/src/engine.rs b/follower/src/engine.rs index 78225e12..66e2852d 100644 --- a/follower/src/engine.rs +++ b/follower/src/engine.rs @@ -6,7 +6,6 @@ use crate::{ resolver::Resolver, }; use alto_types::{Block, Context, Scheme, EPOCH, EPOCH_LENGTH}; -use commonware_broadcast::buffered; use commonware_consensus::{ marshal::{ self, @@ -18,7 +17,7 @@ use commonware_consensus::{ }; use commonware_cryptography::{ certificate::ConstantProvider, - ed25519::{PrivateKey, PublicKey}, + ed25519::PrivateKey, sha256::{self, Digest, Sha256}, Digest as _, Hasher, Signer, }; @@ -111,7 +110,11 @@ where max_repair: NonZero, strategy: T, pruning_depth: Option, - ) -> (Self, MarshalMailbox>, Height) { + ) -> ( + Self, + MarshalMailbox>, + Option, + ) { // Initialize the finalized certificate and block archives. Uses // prunable archives when pruning is enabled, immutable otherwise. let (finalizations_by_height, finalized_blocks, page_cache) = @@ -171,11 +174,7 @@ where let app_handle = app.start(); // Start marshal - let marshal_handle = self.marshal.start( - mailbox, - None::>, - marshal, - ); + let marshal_handle = self.marshal.start_unbuffered(mailbox, marshal); // Wait for any actor to finish if let Err(e) = try_join_all(vec![marshal_handle, app_handle]).await { diff --git a/follower/src/main.rs b/follower/src/main.rs index c3327d23..8f6b2a67 100644 --- a/follower/src/main.rs +++ b/follower/src/main.rs @@ -209,7 +209,7 @@ fn main() { // On the first run (no previously synced data), optionally skip to the // latest finalized height so the follower starts near tip instead of // backfilling from genesis. - if config.tip && last_processed_height == Height::zero() { + if config.tip && last_processed_height.is_none_or(|height| height == Height::zero()) { match client.finalized_get(IndexQuery::Latest).await { Ok(finalized) => { assert!( diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index 169c8f4b..4ae6407e 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -1,171 +1,130 @@ use crate::Source; use alto_client::{consensus::Payload, IndexQuery, Query}; -use bytes::{Buf, Bytes}; -use commonware_actor::{Feedback, Unreliable}; -use commonware_codec::{Encode, ReadExt, Write}; -use commonware_consensus::marshal::resolver::{ - handler, - p2p::{self as marshal_p2p, Mailbox}, -}; -use commonware_cryptography::{ - ed25519::{PrivateKey, PublicKey}, - sha256::Digest, - Signer, -}; -use commonware_p2p::{ - utils::StaticProvider, Blocker, CheckedSender, LimitedSender, Message, Receiver, Recipients, -}; -use commonware_runtime::{ - spawn_cell, BufferPooler, Clock, ContextCell, Handle, IoBuf, IoBufs, Metrics, Spawner, +use bytes::Bytes; +use commonware_actor::{mailbox, Feedback}; +use commonware_codec::Encode; +use commonware_consensus::{ + marshal::resolver::handler, + types::{Height, Round}, }; +use commonware_cryptography::{ed25519::PublicKey, sha256::Digest}; +use commonware_macros::select_loop; +use commonware_resolver::{Consumer as _, Delivery, Fetch}; +use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner}; use commonware_utils::{ - channel::{fallible::AsyncFallibleExt as _, mpsc}, - ordered::Set, + futures::{AbortablePool, Aborter}, + vec::NonEmptyVec, }; -use rand::Rng; +use futures::future::{self, Either}; use std::{ - fmt, io, + collections::{BTreeMap, BTreeSet, VecDeque}, num::NonZeroUsize, + sync::{Arc, Mutex}, time::{Duration, SystemTime}, }; -use tracing::{debug, warn}; +use tracing::{debug, trace, warn}; -pub type Resolver = Mailbox; +type Key = handler::Key; +type Subscriber = handler::Annotation; +type FetchRequest = Fetch; +type Subscribers = Arc>>; -struct Request { - id: u64, - key: handler::Key, +/// Handle to the source-backed resolver actor used by marshal. +#[derive(Clone)] +pub struct Resolver { + mailbox: mailbox::Sender, } -struct SourceActor { - context: ContextCell, - client: C, - requests: mpsc::Receiver, - responses: mpsc::Sender>, - peer: PublicKey, -} +impl commonware_resolver::Resolver for Resolver { + type Key = Key; + type Subscriber = Subscriber; + type PublicKey = PublicKey; -impl SourceActor { - fn new( - context: E, - client: C, - requests: mpsc::Receiver, - responses: mpsc::Sender>, - peer: PublicKey, - ) -> Self { - Self { - context: ContextCell::new(context), - client, - requests, - responses, - peer, - } + fn fetch(&mut self, fetch: F) -> Feedback + where + F: Into> + Send, + { + self.send(Message::Fetch(fetch.into())) } - fn start(mut self) -> Handle<()> { - spawn_cell!(self.context, self.run()) + fn fetch_all(&mut self, fetches: Vec) -> Feedback + where + F: Into> + Send, + { + self.send(Message::FetchAll( + fetches.into_iter().map(Into::into).collect(), + )) } - async fn run(mut self) { - while let Some(request) = self.requests.recv().await { - let response = match fetch(&self.client, request.key).await { - Some(value) => encode_response(request.id, Some(value)), - None => encode_response(request.id, None), - }; - let _ = self - .responses - .send((self.peer.clone(), IoBuf::from(response))) - .await; - } + fn fetch_targeted( + &mut self, + fetch: impl Into> + Send, + _targets: NonEmptyVec, + ) -> Feedback { + self.fetch(fetch) } -} - -#[derive(Clone)] -struct SourceSender { - requests: mpsc::Sender, - peer: PublicKey, -} -struct SourceCheckedSender { - requests: mpsc::Sender, - peer: PublicKey, -} - -impl LimitedSender for SourceSender { - type PublicKey = PublicKey; - type Checked<'a> = SourceCheckedSender; + fn fetch_all_targeted(&mut self, fetches: Vec<(F, NonEmptyVec)>) -> Feedback + where + F: Into> + Send, + { + self.fetch_all(fetches.into_iter().map(|(fetch, _)| fetch).collect()) + } - fn check( + fn retain( &mut self, - recipients: Recipients, - ) -> Result, SystemTime> { - let selected = match recipients { - Recipients::All => true, - Recipients::Some(peers) => peers.contains(&self.peer), - Recipients::One(peer) => peer == self.peer, - }; - selected - .then(|| SourceCheckedSender { - requests: self.requests.clone(), - peer: self.peer.clone(), - }) - .ok_or_else(SystemTime::now) + predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static, + ) -> Feedback { + self.send(Message::Retain(Box::new(predicate))) } } -impl CheckedSender for SourceCheckedSender { - type PublicKey = PublicKey; - - fn recipients(&self) -> Vec { - vec![self.peer.clone()] - } - - fn send(self, message: impl Into + Send, _priority: bool) -> Unreliable { - let Some(request) = decode_request(message) else { - return Unreliable::Rejected; - }; - if self.requests.try_send_lossy(request) { - Unreliable::new(Feedback::Ok) - } else { - Unreliable::Rejected - } +impl Resolver { + fn send(&self, message: Message) -> Feedback { + self.mailbox.enqueue(message) } } -struct SourceReceiver { - responses: mpsc::Receiver>, +enum Message { + Fetch(FetchRequest), + FetchAll(Vec), + Retain(Box bool + Send>), } -impl fmt::Debug for SourceReceiver { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SourceReceiver").finish_non_exhaustive() +impl mailbox::Policy for Message { + type Overflow = VecDeque; + + fn handle(overflow: &mut Self::Overflow, message: Self) { + overflow.push_back(message); } } -impl Receiver for SourceReceiver { - type Error = io::Error; - type PublicKey = PublicKey; - - async fn recv(&mut self) -> Result, Self::Error> { - self.responses.recv().await.ok_or_else(|| { - io::Error::new( - io::ErrorKind::UnexpectedEof, - "source response channel closed", - ) - }) - } +struct Actor { + context: ContextCell, + client: C, + mailbox: mailbox::Receiver, + handler: handler::Handler, + active: AbortablePool, + requests: BTreeMap, + retry_schedule: BTreeSet<(SystemTime, Key)>, + fetch_retry_timeout: Duration, + next_id: u64, } -#[derive(Clone)] -struct SourceBlocker; +struct RequestState { + subscribers: Subscribers, + attempt: Attempt, +} -impl Blocker for SourceBlocker { - type PublicKey = PublicKey; +enum Attempt { + Active { id: u64, _aborter: Aborter }, + Scheduled(SystemTime), +} - fn block(&mut self, peer: Self::PublicKey) -> Feedback { - warn!(?peer, "source-backed resolver peer blocked"); - Feedback::Ok - } +struct Result { + key: Key, + id: u64, + retry: bool, } pub fn init( @@ -175,136 +134,362 @@ pub fn init( fetch_retry_timeout: Duration, ) -> (handler::Receiver, Resolver) where - E: BufferPooler + Rng + Spawner + Clock + Metrics, + E: Clock + Spawner + Metrics, C: Source, { - let local = PrivateKey::from_seed(0).public_key(); - let peer = PrivateKey::from_seed(1).public_key(); - let (requests_tx, requests_rx) = mpsc::channel(mailbox_size.get()); - let (responses_tx, responses_rx) = mpsc::channel(mailbox_size.get()); - - SourceActor::new( - context.child("source"), + let (handler_rx, handler) = handler::init(context.child("handler"), mailbox_size); + let (mailbox_tx, mailbox_rx) = mailbox::new(context.child("mailbox"), mailbox_size); + Actor::new( + context.child("actor"), client, - requests_rx, - responses_tx, - peer.clone(), + mailbox_rx, + handler, + fetch_retry_timeout, ) .start(); + ( + handler_rx, + Resolver { + mailbox: mailbox_tx, + }, + ) +} - marshal_p2p::init( - context.child("resolver"), - marshal_p2p::Config { - public_key: local, - peer_provider: StaticProvider::new(0, Set::from_iter_dedup([peer.clone()])), - blocker: SourceBlocker, - mailbox_size, - initial: fetch_retry_timeout, - timeout: fetch_retry_timeout, +impl Actor +where + E: Clock + Spawner, + C: Source, +{ + fn new( + context: E, + client: C, + mailbox: mailbox::Receiver, + handler: handler::Handler, + fetch_retry_timeout: Duration, + ) -> Self { + Self { + context: ContextCell::new(context), + client, + mailbox, + handler, + active: AbortablePool::default(), + requests: BTreeMap::new(), + retry_schedule: BTreeSet::new(), fetch_retry_timeout, - priority_requests: false, - priority_responses: false, - }, - ( - SourceSender { - requests: requests_tx, - peer: peer.clone(), + next_id: 0, + } + } + + fn start(mut self) -> Handle<()> { + spawn_cell!(self.context, self.run()) + } + + async fn run(mut self) { + select_loop! { + self.context, + on_stopped => {}, + Ok(result) = self.active.next_completed() else continue => { + self.handle_completed(result); }, - SourceReceiver { - responses: responses_rx, + _ = match self.retry_schedule.first() { + Some((deadline, _)) => Either::Left(self.context.sleep_until(*deadline)), + None => Either::Right(future::pending()), + } => { + self.process_retries(); }, - ), - ) -} + Some(message) = self.mailbox.recv() else break => { + self.handle_message(message); + }, + } + } -fn decode_request(message: impl Into) -> Option { - let mut message = message.into(); - let mut bytes = message.copy_to_bytes(message.remaining()); - let id = u64::read(&mut bytes).ok()?; - let payload = u8::read(&mut bytes).ok()?; - if payload != 0 { - return None; + fn handle_message(&mut self, message: Message) { + match message { + Message::Fetch(fetch) => self.add_fetch(fetch), + Message::FetchAll(fetches) => { + for fetch in fetches { + self.add_fetch(fetch); + } + } + Message::Retain(predicate) => self.retain(predicate), + } } - let key = handler::Key::::read(&mut bytes).ok()?; - Some(Request { id, key }) -} -fn encode_response(id: u64, value: Option) -> Vec { - let mut encoded = Vec::new(); - id.write(&mut encoded); - match value { - Some(value) => { - 1u8.write(&mut encoded); - value.write(&mut encoded); + fn add_fetch(&mut self, fetch: FetchRequest) { + let Fetch { key, subscriber } = fetch; + if let Some(state) = self.requests.get_mut(&key) { + let mut subscribers = state + .subscribers + .lock() + .expect("subscribers mutex poisoned"); + if !subscribers.contains(&subscriber) { + subscribers.push(subscriber); + } + return; } - None => 2u8.write(&mut encoded), + + let subscribers = Arc::new(Mutex::new(vec![subscriber])); + self.requests.insert( + key, + RequestState { + subscribers, + attempt: Attempt::Scheduled(self.context.current()), + }, + ); + self.start_fetch(key); } - encoded -} -async fn fetch(client: &impl Source, key: handler::Key) -> Option { - match key { - handler::Key::Block(digest) => fetch_block_by_digest(client, digest).await, - handler::Key::Finalized { height } => fetch_finalized_by_height(client, height).await, - handler::Key::Notarized { round } => fetch_notarized_by_round(client, round).await, + fn retain(&mut self, predicate: Box bool + Send>) { + let mut retained = Vec::new(); + for (key, state) in &mut self.requests { + let mut subscribers = state + .subscribers + .lock() + .expect("subscribers mutex poisoned"); + subscribers.retain(|subscriber| predicate(key, subscriber)); + if !subscribers.is_empty() { + retained.push(*key); + } + } + + let retained = retained.into_iter().collect::>(); + let removed = self + .requests + .keys() + .filter(|key| !retained.contains(key)) + .copied() + .collect::>(); + for key in removed { + if let Some(state) = self.requests.remove(&key) { + if let Attempt::Scheduled(deadline) = state.attempt { + self.retry_schedule.remove(&(deadline, key)); + } + } + } } -} -async fn fetch_block_by_digest(client: &impl Source, digest: Digest) -> Option { - debug!(?digest, "fetching block by digest"); - match client.block(Query::Digest(digest)).await { - Ok(Payload::Block(block)) => Some(Bytes::from(block.encode().to_vec())), - Ok(_) => { - warn!(?digest, "wrong payload returned for block by digest"); - None + fn start_fetch(&mut self, key: Key) { + let subscribers = self + .requests + .get(&key) + .expect("request missing") + .subscribers + .clone(); + let id = self.next_id; + self.next_id = self.next_id.wrapping_add(1); + let future = Self::process_fetch( + key, + id, + self.client.clone(), + self.handler.clone(), + subscribers, + ); + let aborter = self.active.push(future); + let state = self.requests.get_mut(&key).expect("request missing"); + state.attempt = Attempt::Active { + id, + _aborter: aborter, + }; + } + + fn handle_completed(&mut self, result: Result) { + let Some(state) = self.requests.get(&result.key) else { + trace!(?result.key, id = result.id, "ignoring stale fetch completion"); + return; + }; + match state.attempt { + Attempt::Active { id, .. } if id == result.id => {} + Attempt::Active { id, .. } => { + trace!( + ?result.key, + completed_id = result.id, + active_id = id, + "ignoring replaced fetch completion" + ); + return; + } + Attempt::Scheduled(deadline) => { + trace!( + ?result.key, + id = result.id, + ?deadline, + "ignoring scheduled fetch completion" + ); + return; + } } - Err(e) => { - warn!(?digest, error = ?e, "failed to fetch block by digest"); - None + + if result.retry { + self.schedule_retry(result.key); + } else { + self.requests.remove(&result.key); } } -} -async fn fetch_finalized_by_height( - client: &impl Source, - height: commonware_consensus::types::Height, -) -> Option { - debug!(height = height.get(), "fetching finalized block by height"); - match client.block(Query::Index(height.get())).await { - Ok(Payload::Finalized(finalized)) => Some(Bytes::from( - (finalized.proof.clone(), finalized.block.clone()) - .encode() - .to_vec(), - )), - Ok(_) => { - warn!( - height = height.get(), - "wrong payload returned for finalized block by height" - ); - None + fn schedule_retry(&mut self, key: Key) { + let deadline = self.context.current() + self.fetch_retry_timeout; + let Some(state) = self.requests.get_mut(&key) else { + return; + }; + state.attempt = Attempt::Scheduled(deadline); + self.retry_schedule.insert((deadline, key)); + debug!(?key, ?deadline, "scheduled source resolver retry"); + } + + fn process_retries(&mut self) { + let now = self.context.current(); + while let Some((deadline, key)) = self.retry_schedule.pop_first() { + if deadline > now { + self.retry_schedule.insert((deadline, key)); + break; + } + + let Some(state) = self.requests.get(&key) else { + continue; + }; + match state.attempt { + Attempt::Scheduled(state_deadline) if state_deadline == deadline => { + debug!(?key, "retrying source resolver fetch"); + self.start_fetch(key); + } + Attempt::Scheduled(_) | Attempt::Active { .. } => {} + } } - Err(e) => { - warn!(height = height.get(), error = ?e, "failed to fetch finalized block by height"); - None + } + + async fn process_fetch( + key: Key, + id: u64, + client: C, + handler: handler::Handler, + subscribers: Subscribers, + ) -> Result { + let retry = match key { + handler::Key::Block(digest) => { + Self::fetch_block_by_digest(key, digest, client, handler, subscribers).await + } + handler::Key::Finalized { height } => { + Self::fetch_finalized_by_height(key, height, client, handler, subscribers).await + } + handler::Key::Notarized { round } => { + Self::fetch_notarized_by_round(key, round, client, handler, subscribers).await + } + }; + Result { key, id, retry } + } + + async fn fetch_block_by_digest( + key: Key, + digest: Digest, + client: C, + handler: handler::Handler, + subscribers: Subscribers, + ) -> bool { + debug!(?digest, "fetching block by digest"); + match client.block(Query::Digest(digest)).await { + Ok(Payload::Block(block)) => { + let value = Bytes::from(block.encode().to_vec()); + Self::deliver(key, value, handler, subscribers).await + } + Ok(_) => { + warn!(?digest, "wrong payload returned for block by digest"); + true + } + Err(error) => { + warn!(?digest, ?error, "failed to fetch block by digest"); + true + } + } + } + + async fn fetch_finalized_by_height( + key: Key, + height: Height, + client: C, + handler: handler::Handler, + subscribers: Subscribers, + ) -> bool { + debug!(height = height.get(), "fetching finalized block by height"); + match client.block(Query::Index(height.get())).await { + Ok(Payload::Finalized(finalized)) => { + let value = Bytes::from( + (finalized.proof.clone(), finalized.block.clone()) + .encode() + .to_vec(), + ); + Self::deliver(key, value, handler, subscribers).await + } + Ok(_) => { + warn!( + height = height.get(), + "wrong payload returned for finalized block by height" + ); + true + } + Err(error) => { + warn!( + height = height.get(), + ?error, + "failed to fetch finalized block by height" + ); + true + } } } -} -async fn fetch_notarized_by_round( - client: &impl Source, - round: commonware_consensus::types::Round, -) -> Option { - let view = round.view().get(); - debug!(view, "fetching notarized block by round"); - match client.notarized(IndexQuery::Index(view)).await { - Ok(notarized) => Some(Bytes::from( - (notarized.proof.clone(), notarized.block.clone()) - .encode() - .to_vec(), - )), - Err(e) => { - warn!(view, error = ?e, "failed to fetch notarized block by round"); - None + async fn fetch_notarized_by_round( + key: Key, + round: Round, + client: C, + handler: handler::Handler, + subscribers: Subscribers, + ) -> bool { + let view = round.view().get(); + debug!(view, "fetching notarized block by round"); + match client.notarized(IndexQuery::Index(view)).await { + Ok(notarized) => { + let value = Bytes::from( + (notarized.proof.clone(), notarized.block.clone()) + .encode() + .to_vec(), + ); + Self::deliver(key, value, handler, subscribers).await + } + Err(error) => { + warn!(view, ?error, "failed to fetch notarized block by round"); + true + } + } + } + + async fn deliver( + key: Key, + value: Bytes, + mut handler: handler::Handler, + subscribers: Subscribers, + ) -> bool { + let subscribers = subscribers + .lock() + .expect("subscribers mutex poisoned") + .clone(); + let Ok(subscribers) = NonEmptyVec::try_from(subscribers) else { + return false; + }; + let response = handler.deliver(Delivery { key, subscribers }, value); + match response.await { + Ok(true) => false, + Ok(false) => { + warn!(?key, "marshal rejected source resolver delivery"); + true + } + Err(error) => { + warn!( + ?key, + ?error, + "marshal dropped source resolver delivery response" + ); + true + } } } } From 37b07cdb9176984e04e030bfd43126515a594f8d Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 22 May 2026 16:47:54 -0700 Subject: [PATCH 06/11] progress --- follower/src/resolver.rs | 18 +++++------------- follower/src/test_utils.rs | 14 ++++++-------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index 4ae6407e..096875d5 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -13,13 +13,14 @@ use commonware_resolver::{Consumer as _, Delivery, Fetch}; use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner}; use commonware_utils::{ futures::{AbortablePool, Aborter}, + sync::Mutex, vec::NonEmptyVec, }; use futures::future::{self, Either}; use std::{ collections::{BTreeMap, BTreeSet, VecDeque}, num::NonZeroUsize, - sync::{Arc, Mutex}, + sync::Arc, time::{Duration, SystemTime}, }; use tracing::{debug, trace, warn}; @@ -218,10 +219,7 @@ where fn add_fetch(&mut self, fetch: FetchRequest) { let Fetch { key, subscriber } = fetch; if let Some(state) = self.requests.get_mut(&key) { - let mut subscribers = state - .subscribers - .lock() - .expect("subscribers mutex poisoned"); + let mut subscribers = state.subscribers.lock(); if !subscribers.contains(&subscriber) { subscribers.push(subscriber); } @@ -242,10 +240,7 @@ where fn retain(&mut self, predicate: Box bool + Send>) { let mut retained = Vec::new(); for (key, state) in &mut self.requests { - let mut subscribers = state - .subscribers - .lock() - .expect("subscribers mutex poisoned"); + let mut subscribers = state.subscribers.lock(); subscribers.retain(|subscriber| predicate(key, subscriber)); if !subscribers.is_empty() { retained.push(*key); @@ -468,10 +463,7 @@ where mut handler: handler::Handler, subscribers: Subscribers, ) -> bool { - let subscribers = subscribers - .lock() - .expect("subscribers mutex poisoned") - .clone(); + let subscribers = subscribers.lock().clone(); let Ok(subscribers) = NonEmptyVec::try_from(subscribers) else { return false; }; diff --git a/follower/src/test_utils.rs b/follower/src/test_utils.rs index 1781fc33..a6bd2293 100644 --- a/follower/src/test_utils.rs +++ b/follower/src/test_utils.rs @@ -16,11 +16,9 @@ use commonware_cryptography::{ Digestible, Hasher, Sha256, Signer, }; use commonware_parallel::Sequential; +use commonware_utils::sync::Mutex; use rand::{rngs::StdRng, SeedableRng}; -use std::{ - future::Future, - sync::{Arc, Mutex}, -}; +use std::{future::Future, sync::Arc}; use thiserror::Error; #[derive(Debug, Error)] @@ -62,7 +60,7 @@ impl Source for MockSource { async fn block(&self, query: Query) -> Result { let handler = self.block_handler.clone(); - let guard = handler.lock().unwrap(); + let guard = handler.lock(); match guard.as_ref().and_then(|f| f(query)) { Some(payload) => Ok(payload), None => Err(MockError("block not found".to_string())), @@ -71,7 +69,7 @@ impl Source for MockSource { async fn notarized(&self, query: IndexQuery) -> Result { let handler = self.notarized_handler.clone(); - let guard = handler.lock().unwrap(); + let guard = handler.lock(); match guard.as_ref().and_then(|f| f(query)) { Some(notarized) => Ok(notarized), None => Err(MockError("notarized not found".to_string())), @@ -80,7 +78,7 @@ impl Source for MockSource { async fn finalized(&self, query: IndexQuery) -> Result { let handler = self.finalized_handler.clone(); - let guard = handler.lock().unwrap(); + let guard = handler.lock(); match guard.as_ref().and_then(|f| f(query)) { Some(finalized) => Ok(finalized), None => Err(MockError("finalized not found".to_string())), @@ -97,7 +95,7 @@ impl Source for MockSource { > + Send { let messages = self.messages.clone(); async move { - let msgs = messages.lock().unwrap().drain(..).collect::>(); + let msgs = messages.lock().drain(..).collect::>(); Ok(futures::stream::iter(msgs.into_iter().map(Ok))) } } From 0d395445666979cfc60ee882facbb53cab2cad28 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 23 May 2026 14:25:03 -0700 Subject: [PATCH 07/11] nits --- chain/src/indexer/backfiller/state.rs | 15 +++++++++++++++ follower/src/resolver.rs | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/chain/src/indexer/backfiller/state.rs b/chain/src/indexer/backfiller/state.rs index 9e18c222..7cd78e30 100644 --- a/chain/src/indexer/backfiller/state.rs +++ b/chain/src/indexer/backfiller/state.rs @@ -83,6 +83,11 @@ impl State { } pub fn record(&mut self, block: &Block) -> Option { + // Genesis is local bootstrap state and has no certificate upload to backfill. + if block.height.get() == 0 { + return None; + } + let entry = Entry { height: block.height.get(), digest: block.digest(), @@ -195,6 +200,16 @@ mod tests { ) } + #[test] + fn test_upload_state_ignores_genesis() { + let mut uploads = State::new(); + let genesis = test_block(0, 0, b"genesis"); + let digest = genesis.digest(); + + assert!(uploads.record(&genesis).is_none()); + assert!(uploads.cached_block(&digest).is_none()); + } + #[test] fn test_upload_state_prunes_only_after_queue_floor_progress() { let mut uploads = State::new(); diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index 096875d5..b0cc97ed 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -28,6 +28,7 @@ use tracing::{debug, trace, warn}; type Key = handler::Key; type Subscriber = handler::Annotation; type FetchRequest = Fetch; +type RetainPredicate = Box bool + Send>; type Subscribers = Arc>>; /// Handle to the source-backed resolver actor used by marshal. @@ -89,7 +90,7 @@ impl Resolver { enum Message { Fetch(FetchRequest), FetchAll(Vec), - Retain(Box bool + Send>), + Retain(RetainPredicate), } impl mailbox::Policy for Message { @@ -237,7 +238,7 @@ where self.start_fetch(key); } - fn retain(&mut self, predicate: Box bool + Send>) { + fn retain(&mut self, predicate: RetainPredicate) { let mut retained = Vec::new(); for (key, state) in &mut self.requests { let mut subscribers = state.subscribers.lock(); From b9df9f46c8a542168b00786951a10055aa963295 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 23 May 2026 15:49:45 -0700 Subject: [PATCH 08/11] cleanup --- chain/src/application.rs | 19 +- chain/src/engine.rs | 1 + chain/src/indexer/backfiller/mod.rs | 2 +- chain/src/indexer/backfiller/producer.rs | 80 ++- chain/src/indexer/mod.rs | 12 +- follower/src/engine.rs | 101 ++++ follower/src/resolver.rs | 607 +++++++++++++++++++++-- follower/src/test_utils.rs | 1 - validator/src/main.rs | 1 + 9 files changed, 768 insertions(+), 56 deletions(-) diff --git a/chain/src/application.rs b/chain/src/application.rs index a486a25b..00427fc5 100644 --- a/chain/src/application.rs +++ b/chain/src/application.rs @@ -28,7 +28,7 @@ const MAX_BLOCK_TIMESTAMP_MS: u64 = 7_258_118_400_000; pub struct Application { context: Arc, - backfiller: Option>>, + backfiller: Option>, } impl Clone for Application { @@ -57,7 +57,7 @@ impl Application { } } - pub(crate) fn with_backfiller(mut self, backfiller: indexer::Producer) -> Self { + pub(crate) fn with_backfiller(mut self, backfiller: indexer::Producer) -> Self { self.backfiller = Some(Arc::new(backfiller)); self } @@ -127,22 +127,15 @@ where } } -impl Reporter for Application { +impl Reporter for Application { type Activity = Update; fn report(&mut self, activity: Self::Activity) -> Feedback { if let Update::Block(block, ack_rx) = activity { if let Some(backfiller) = self.backfiller.clone() { - self.context - .child("backfill_record") - .spawn(move |_| async move { - // Cache the finalized block in memory and enqueue its digest - // before acking so the consumer can recover it across restarts. - backfiller.record(&block).await; - - info!(height = %block.height(), "finalized block"); - ack_rx.acknowledge(); - }); + let height = block.height(); + info!(height = %height, "finalized block"); + return backfiller.record(block, ack_rx); } else { info!(height = %block.height(), "finalized block"); ack_rx.acknowledge(); diff --git a/chain/src/engine.rs b/chain/src/engine.rs index a61b2717..fda9eb01 100644 --- a/chain/src/engine.rs +++ b/chain/src/engine.rs @@ -296,6 +296,7 @@ where indexer, marshal_mailbox.clone(), queue, + NZUsize!(cfg.mailbox_size), cfg.backfiller_max_active, cfg.backfiller_retry, ) diff --git a/chain/src/indexer/backfiller/mod.rs b/chain/src/indexer/backfiller/mod.rs index 289af81c..120a1072 100644 --- a/chain/src/indexer/backfiller/mod.rs +++ b/chain/src/indexer/backfiller/mod.rs @@ -12,5 +12,5 @@ mod producer; mod state; pub use consumer::Consumer; -pub use producer::Producer; +pub(crate) use producer::{init as init_producer, Producer}; pub use state::{Decision, Entry, SharedState, State}; diff --git a/chain/src/indexer/backfiller/producer.rs b/chain/src/indexer/backfiller/producer.rs index ba7e14d5..1755e267 100644 --- a/chain/src/indexer/backfiller/producer.rs +++ b/chain/src/indexer/backfiller/producer.rs @@ -1,22 +1,76 @@ use super::{Entry, SharedState}; use alto_types::Block; -use commonware_runtime::{Clock, Metrics, Storage}; +use commonware_actor::{ + mailbox::{self, Policy}, + Feedback, +}; +use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner, Storage}; use commonware_storage::queue; +use commonware_utils::{acknowledgement::Exact, Acknowledgement}; +use std::{collections::VecDeque, num::NonZeroUsize}; /// Records finalized block digests in the backfill queue from the application's /// block stream. #[derive(Clone)] -pub struct Producer { +pub struct Producer { + sender: mailbox::Sender, +} + +struct Message { + block: Block, + ack: Exact, +} + +impl Policy for Message { + type Overflow = VecDeque; + + fn handle(overflow: &mut Self::Overflow, message: Self) { + overflow.push_back(message); + } +} + +struct Actor { + context: ContextCell, uploads: SharedState, writer: queue::Writer, + receiver: mailbox::Receiver, +} + +impl Producer { + pub fn record(&self, block: Block, ack: Exact) -> Feedback { + self.sender.enqueue(Message { block, ack }) + } } -impl Producer { - pub fn new(uploads: SharedState, writer: queue::Writer) -> Self { - Self { uploads, writer } +impl Actor { + pub fn new( + context: E, + uploads: SharedState, + writer: queue::Writer, + mailbox_size: NonZeroUsize, + ) -> (Self, Producer) { + let (sender, receiver) = mailbox::new(context.child("mailbox"), mailbox_size); + let actor = Self { + context: ContextCell::new(context), + uploads, + writer, + receiver, + }; + (actor, Producer { sender }) + } + + pub fn start(mut self) -> Handle<()> { + spawn_cell!(self.context, self.run()) + } + + async fn run(mut self) { + while let Some(Message { block, ack }) = self.receiver.recv().await { + self.record(&block).await; + ack.acknowledge(); + } } - pub async fn record(&self, block: &Block) { + async fn record(&mut self, block: &Block) { let Some(entry) = self.uploads.lock().record(block) else { return; }; @@ -31,3 +85,17 @@ impl Producer { .expect("failed to enqueue finalized digest"); } } + +pub fn init( + context: E, + uploads: SharedState, + writer: queue::Writer, + mailbox_size: NonZeroUsize, +) -> Producer +where + E: Clock + Storage + Metrics + Spawner, +{ + let (actor, producer) = Actor::new(context, uploads, writer, mailbox_size); + actor.start(); + producer +} diff --git a/chain/src/indexer/mod.rs b/chain/src/indexer/mod.rs index 8822b736..bc5eaed1 100644 --- a/chain/src/indexer/mod.rs +++ b/chain/src/indexer/mod.rs @@ -89,7 +89,7 @@ impl Client for alto_client::Client { /// - a pusher for consensus activity; /// - a consumer for the background retry task. pub(crate) struct Indexer { - producer: Producer, + producer: Producer, pusher: Pusher, consumer: Consumer, } @@ -100,6 +100,7 @@ impl Indexer { client: C, marshal: MarshalMailbox>, backfiller: (queue::Writer, queue::Reader), + mailbox_size: NonZeroUsize, backfiller_max_active: NonZeroUsize, backfiller_retry: Duration, ) -> Self { @@ -111,7 +112,12 @@ impl Indexer { uploads.clone(), ); let (writer, reader) = backfiller; - let producer = Producer::new(uploads.clone(), writer.clone()); + let producer = backfiller::init_producer( + context.child("producer"), + uploads.clone(), + writer.clone(), + mailbox_size, + ); let consumer = Consumer::new( context.child("consumer"), client, @@ -130,7 +136,7 @@ impl Indexer { } /// Consumes the runtime and returns the actor handles it constructed. - pub(crate) fn split(self) -> (Producer, Pusher, Consumer) { + pub(crate) fn split(self) -> (Producer, Pusher, Consumer) { let Self { producer, pusher, diff --git a/follower/src/engine.rs b/follower/src/engine.rs index 66e2852d..576f3de7 100644 --- a/follower/src/engine.rs +++ b/follower/src/engine.rs @@ -184,3 +184,104 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{MockSource, TestFixture}; + use bytes::Bytes; + use commonware_codec::Encode; + use commonware_consensus::types::{Round, View}; + use commonware_macros::test_traced; + use commonware_parallel::Sequential; + use commonware_resolver::{Consumer, Delivery}; + use commonware_runtime::{deterministic::Runner, Runner as _, Supervisor as _}; + use commonware_utils::{vec::NonEmptyVec, NZUsize}; + use std::time::Duration; + + async fn start_engine_with_handler( + context: commonware_runtime::deterministic::Context, + scheme: Scheme, + ) -> handler::Handler { + let (engine, _, _) = Engine::new( + context.child("engine"), + scheme, + NZUsize!(16), + NZUsize!(256), + Sequential, + None, + ) + .await; + let (receiver, handler) = handler::init(context.child("handler"), NZUsize!(16)); + let (_, resolver) = crate::resolver::init( + context.child("resolver"), + MockSource::new(), + NZUsize!(16), + Duration::from_secs(1), + ); + engine.start((receiver, resolver)); + handler + } + + /// Verifies that marshal's Deliver handler rejects a finalization whose + /// threshold signature does not match the configured scheme. This is + /// the resolver path's signature verification, as opposed to the feeder + /// path tested in feeder::tests. + #[test_traced] + fn marshal_rejects_invalid_finalization_from_resolver() { + let fixture = TestFixture::new(); + let finalized = fixture.create_finalized(1, 1); + let wrong_verifier = fixture.wrong_verifier_scheme(); + + Runner::default().start(|context| async move { + let mut handler = start_engine_with_handler(context, wrong_verifier).await; + + let height = Height::new(1); + let value = Bytes::from((finalized.proof, finalized.block).encode().to_vec()); + let response = handler.deliver( + Delivery { + key: handler::Key::Finalized { height }, + subscribers: NonEmptyVec::new(handler::Annotation::Finalized( + handler::Finalized::ByHeight { height }, + )), + }, + value, + ); + + let accepted = response.await.expect("response dropped"); + assert!( + !accepted, + "marshal should reject finalization with invalid signature" + ); + }); + } + + /// Verifies that marshal's Deliver handler rejects a notarization whose + /// threshold signature does not match the configured scheme. + #[test_traced] + fn marshal_rejects_invalid_notarization_from_resolver() { + let fixture = TestFixture::new(); + let notarized = fixture.create_notarized(1, 1); + let wrong_verifier = fixture.wrong_verifier_scheme(); + + Runner::default().start(|context| async move { + let mut handler = start_engine_with_handler(context, wrong_verifier).await; + + let round = Round::new(EPOCH, View::new(1)); + let value = Bytes::from((notarized.proof, notarized.block).encode().to_vec()); + let response = handler.deliver( + Delivery { + key: handler::Key::Notarized { round }, + subscribers: NonEmptyVec::new(handler::Annotation::Notarization { round }), + }, + value, + ); + + let accepted = response.await.expect("response dropped"); + assert!( + !accepted, + "marshal should reject notarization with invalid signature" + ); + }); + } +} diff --git a/follower/src/resolver.rs b/follower/src/resolver.rs index b0cc97ed..6f50cf20 100644 --- a/follower/src/resolver.rs +++ b/follower/src/resolver.rs @@ -9,7 +9,7 @@ use commonware_consensus::{ }; use commonware_cryptography::{ed25519::PublicKey, sha256::Digest}; use commonware_macros::select_loop; -use commonware_resolver::{Consumer as _, Delivery, Fetch}; +use commonware_resolver::{Consumer, Delivery, Fetch}; use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner}; use commonware_utils::{ futures::{AbortablePool, Aborter}, @@ -101,11 +101,20 @@ impl mailbox::Policy for Message { } } -struct Actor { +/// Actor that fetches blocks and certificates from a [Source] on behalf of marshal. +/// +/// The [Source] should be constructed without verification because marshal +/// validates all signatures before accepting resolved data. Rejections are +/// logged and retried. +struct Actor< + E: Clock + Spawner, + C: Source, + H: Consumer, +> { context: ContextCell, client: C, mailbox: mailbox::Receiver, - handler: handler::Handler, + handler: H, active: AbortablePool, requests: BTreeMap, retry_schedule: BTreeSet<(SystemTime, Key)>, @@ -114,12 +123,18 @@ struct Actor { } struct RequestState { + // Subscribers not yet included in a delivery attempt. subscribers: Subscribers, attempt: Attempt, } enum Attempt { + // A source fetch or marshal delivery is currently running. + // + // The id lets us ignore stale completions from an earlier attempt for the + // same key, and dropping the aborter cancels the current attempt. Active { id: u64, _aborter: Aborter }, + // A retry is queued for the recorded deadline. Scheduled(SystemTime), } @@ -157,16 +172,17 @@ where ) } -impl Actor +impl Actor where E: Clock + Spawner, C: Source, + H: Consumer, { fn new( context: E, client: C, mailbox: mailbox::Receiver, - handler: handler::Handler, + handler: H, fetch_retry_timeout: Duration, ) -> Self { Self { @@ -317,6 +333,12 @@ where if result.retry { self.schedule_retry(result.key); + } else if self + .requests + .get(&result.key) + .is_some_and(|state| !state.subscribers.lock().is_empty()) + { + self.start_fetch(result.key); } else { self.requests.remove(&result.key); } @@ -357,7 +379,7 @@ where key: Key, id: u64, client: C, - handler: handler::Handler, + handler: H, subscribers: Subscribers, ) -> Result { let retry = match key { @@ -378,7 +400,7 @@ where key: Key, digest: Digest, client: C, - handler: handler::Handler, + handler: H, subscribers: Subscribers, ) -> bool { debug!(?digest, "fetching block by digest"); @@ -402,7 +424,7 @@ where key: Key, height: Height, client: C, - handler: handler::Handler, + handler: H, subscribers: Subscribers, ) -> bool { debug!(height = height.get(), "fetching finalized block by height"); @@ -437,7 +459,7 @@ where key: Key, round: Round, client: C, - handler: handler::Handler, + handler: H, subscribers: Subscribers, ) -> bool { let view = round.view().get(); @@ -458,31 +480,552 @@ where } } - async fn deliver( - key: Key, - value: Bytes, - mut handler: handler::Handler, - subscribers: Subscribers, - ) -> bool { - let subscribers = subscribers.lock().clone(); - let Ok(subscribers) = NonEmptyVec::try_from(subscribers) else { - return false; - }; - let response = handler.deliver(Delivery { key, subscribers }, value); - match response.await { - Ok(true) => false, - Ok(false) => { - warn!(?key, "marshal rejected source resolver delivery"); - true + async fn deliver(key: Key, value: Bytes, mut handler: H, subscribers: Subscribers) -> bool { + loop { + let pending = { + let mut subscribers = subscribers.lock(); + std::mem::take(&mut *subscribers) + }; + let Ok(delivered) = NonEmptyVec::try_from(pending) else { + return false; + }; + let response = handler.deliver( + Delivery { + key, + subscribers: delivered.clone(), + }, + value.clone(), + ); + match response.await { + Ok(true) => { + let mut pending = subscribers.lock(); + pending.retain(|subscriber| !delivered.contains(subscriber)); + if pending.is_empty() { + return false; + } + } + Ok(false) => { + Self::restore_subscribers(&subscribers, delivered); + warn!(?key, "marshal rejected source resolver delivery"); + return true; + } + Err(error) => { + Self::restore_subscribers(&subscribers, delivered); + warn!( + ?key, + ?error, + "marshal dropped source resolver delivery response" + ); + return true; + } } - Err(error) => { - warn!( - ?key, - ?error, - "marshal dropped source resolver delivery response" - ); - true + } + } + + fn restore_subscribers(subscribers: &Subscribers, delivered: NonEmptyVec) { + let mut subscribers = subscribers.lock(); + for subscriber in delivered { + if !subscribers.contains(&subscriber) { + subscribers.push(subscriber); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{MockSource, TestFixture}; + use alto_client::Query; + use commonware_cryptography::{ed25519::PrivateKey, Digestible, Signer}; + use commonware_macros::test_traced; + use commonware_resolver::Resolver as _; + use commonware_runtime::{deterministic, Clock, Runner as _, Supervisor as _}; + use commonware_utils::{channel::oneshot, NZUsize}; + use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }; + + const DEFAULT_FETCH_RETRY_TIMEOUT: Duration = Duration::from_secs(1); + + struct CapturedDelivery { + delivery: Delivery, + value: Bytes, + response: oneshot::Sender, + } + + #[derive(Clone, Default)] + struct TestConsumer { + deliveries: Arc>>, + } + + impl TestConsumer { + fn pop(&self) -> Option { + self.deliveries.lock().pop_front() + } + + fn len(&self) -> usize { + self.deliveries.lock().len() + } + } + + impl Consumer for TestConsumer { + type Key = Key; + type Value = Bytes; + type Subscriber = Subscriber; + + fn deliver( + &mut self, + delivery: Delivery, + value: Self::Value, + ) -> oneshot::Receiver { + let (response, receiver) = oneshot::channel(); + self.deliveries.lock().push_back(CapturedDelivery { + delivery, + value, + response, + }); + receiver + } + } + + fn start_resolver( + context: deterministic::Context, + source: MockSource, + consumer: TestConsumer, + ) -> Resolver { + let (mailbox_tx, mailbox_rx) = mailbox::new(context.child("mailbox"), NZUsize!(16)); + Actor::new( + context.child("actor"), + source, + mailbox_rx, + consumer, + DEFAULT_FETCH_RETRY_TIMEOUT, + ) + .start(); + Resolver { + mailbox: mailbox_tx, + } + } + + async fn wait_for_delivery( + context: &deterministic::Context, + consumer: &TestConsumer, + ) -> CapturedDelivery { + for _ in 0..50 { + if let Some(delivery) = consumer.pop() { + return delivery; } + context.sleep(Duration::from_millis(100)).await; + } + panic!("timed out waiting for delivery"); + } + + #[test_traced] + fn fetches_block_by_digest() { + let fixture = TestFixture::new(); + let block = fixture.create_block(1, 1); + let digest = block.digest(); + + let source = MockSource::new(); + *source.block_handler.lock() = Some(Box::new(move |_| { + Some(Payload::Block(Box::new(block.clone()))) + })); + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + let height = Height::new(1); + + assert!(resolver + .fetch(handler::Request::certified_block(digest, height)) + .accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + + assert!(matches!(delivery.delivery.key, handler::Key::Block(d) if d == digest)); + assert!(delivery + .delivery + .subscribers + .contains(&handler::Annotation::Certified { height })); + assert!(!delivery.value.is_empty()); + delivery.response.send(true).expect("response dropped"); + }); + } + + #[test_traced] + fn fetches_finalized_by_height_uses_height_indexed_block_query() { + let fixture = TestFixture::new(); + let finalized = fixture.create_finalized(5, 8); + let height = Height::new(5); + let block_calls = Arc::new(AtomicU32::new(0)); + let finalized_calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let block_calls = block_calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |query| { + block_calls.fetch_add(1, Ordering::Relaxed); + match query { + Query::Index(index) if index == height.get() => { + Some(Payload::Finalized(Box::new(finalized.clone()))) + } + _ => None, + } + })); + } + { + let finalized_calls = finalized_calls.clone(); + *source.finalized_handler.lock() = Some(Box::new(move |_| { + finalized_calls.fetch_add(1, Ordering::Relaxed); + None + })); } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = + start_resolver(context.child("resolver"), source, consumer.clone()); + + assert!(resolver.fetch(handler::Request::finalized(height)).accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + assert!( + matches!(delivery.delivery.key, handler::Key::Finalized { height: h } if h == height) + ); + delivery.response.send(true).expect("response dropped"); + + assert_eq!(block_calls.load(Ordering::Relaxed), 1); + assert_eq!(finalized_calls.load(Ordering::Relaxed), 0); + }); + } + + #[test_traced] + fn fetches_notarized_by_round() { + let fixture = TestFixture::new(); + let notarized = fixture.create_notarized(3, 3); + let round = Round::new(alto_types::EPOCH, commonware_consensus::types::View::new(3)); + + let source = MockSource::new(); + *source.notarized_handler.lock() = Some(Box::new(move |_| Some(notarized.clone()))); + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + + assert!(resolver + .fetch(handler::Request::notarized(round)) + .accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + assert!( + matches!(delivery.delivery.key, handler::Key::Notarized { round: r } if r == round) + ); + delivery.response.send(true).expect("response dropped"); + }); + } + + #[test_traced] + fn retries_when_marshal_rejects_finalized_delivery() { + let fixture = TestFixture::new(); + let finalized = fixture.create_finalized(1, 1); + let height = Height::new(1); + let calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let calls = calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |query| match query { + Query::Index(index) if index == height.get() => { + calls.fetch_add(1, Ordering::Relaxed); + Some(Payload::Finalized(Box::new(finalized.clone()))) + } + _ => None, + })); + } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + + assert!(resolver + .fetch(handler::Request::finalized(height)) + .accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + delivery.response.send(false).expect("response dropped"); + + context + .sleep(DEFAULT_FETCH_RETRY_TIMEOUT + Duration::from_millis(100)) + .await; + let retry = wait_for_delivery(&context, &consumer).await; + assert!( + matches!(retry.delivery.key, handler::Key::Finalized { height: h } if h == height) + ); + retry.response.send(true).expect("response dropped"); + + assert_eq!(calls.load(Ordering::Relaxed), 2); + }); + } + + #[test_traced] + fn deduplicates_identical_subscribers() { + let fixture = TestFixture::new(); + let block = fixture.create_block(1, 1); + let digest = block.digest(); + let calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let calls = calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + Some(Payload::Block(Box::new(block.clone()))) + })); + } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + let request = handler::Request::certified_block(digest, Height::new(1)); + + assert!(resolver.fetch(request).accepted()); + assert!(resolver.fetch(request).accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + assert_eq!(delivery.delivery.subscribers.len().get(), 1); + delivery.response.send(true).expect("response dropped"); + context.sleep(Duration::from_millis(100)).await; + + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(consumer.len(), 0); + }); + } + + #[test_traced] + fn failed_fetch_eventually_resolves_after_multiple_retries() { + let fixture = TestFixture::new(); + let block = fixture.create_block(1, 1); + let digest = block.digest(); + let calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let calls = calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |_| { + let attempt = calls.fetch_add(1, Ordering::Relaxed) + 1; + (attempt >= 3).then(|| Payload::Block(Box::new(block.clone()))) + })); + } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + + assert!(resolver + .fetch(handler::Request::certified_block(digest, Height::new(1))) + .accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + assert!(matches!(delivery.delivery.key, handler::Key::Block(d) if d == digest)); + delivery.response.send(true).expect("response dropped"); + + assert_eq!(calls.load(Ordering::Relaxed), 3); + }); + } + + #[test_traced] + fn fetch_during_validation_reuses_response_after_success() { + let fixture = TestFixture::new(); + let block = fixture.create_block(1, 1); + let digest = block.digest(); + let calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let calls = calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + Some(Payload::Block(Box::new(block.clone()))) + })); + } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + let height = Height::new(1); + + assert!(resolver + .fetch(handler::Request::certified_block(digest, height)) + .accepted()); + let first = wait_for_delivery(&context, &consumer).await; + + assert!(resolver + .fetch(handler::Request::finalized_block_by_height(digest, height)) + .accepted()); + context.sleep(Duration::from_millis(100)).await; + first.response.send(true).expect("response dropped"); + + let second = wait_for_delivery(&context, &consumer).await; + assert!(matches!(second.delivery.key, handler::Key::Block(d) if d == digest)); + assert!(second + .delivery + .subscribers + .contains(&handler::Annotation::Finalized( + handler::Finalized::ByHeight { height } + ))); + second.response.send(true).expect("response dropped"); + + context.sleep(Duration::from_millis(100)).await; + assert_eq!(calls.load(Ordering::Relaxed), 1); + }); + } + + #[test_traced] + fn retain_removes_unwanted_subscribers() { + let fixture = TestFixture::new(); + let digest = fixture.create_block(1, 1).digest(); + + deterministic::Runner::default().start(|context| async move { + let source = MockSource::new(); + let consumer = TestConsumer::default(); + let (mailbox_tx, mailbox_rx) = mailbox::new(context.child("mailbox"), NZUsize!(16)); + let mut actor = Actor::new( + context.child("actor"), + source, + mailbox_rx, + consumer, + DEFAULT_FETCH_RETRY_TIMEOUT, + ); + let mut resolver = Resolver { + mailbox: mailbox_tx, + }; + let keep = handler::Annotation::Certified { + height: Height::new(2), + }; + let discard = handler::Annotation::Certified { + height: Height::new(1), + }; + let key = handler::Key::Block(digest); + actor.requests.insert( + key, + RequestState { + subscribers: Arc::new(Mutex::new(vec![keep, discard])), + attempt: Attempt::Scheduled(context.current()), + }, + ); + + assert!(resolver + .retain(move |_, subscriber| *subscriber == keep) + .accepted()); + let message = actor.mailbox.recv().await.expect("missing retain"); + actor.handle_message(message); + + let subscribers = actor + .requests + .get(&key) + .expect("request should be retained") + .subscribers + .lock() + .clone(); + assert_eq!(subscribers, vec![keep]); + }); + } + + #[test_traced] + fn stale_completion_does_not_mutate_replaced_request() { + let fixture = TestFixture::new(); + let digest = fixture.create_block(1, 1).digest(); + + deterministic::Runner::default().start(|context| async move { + let source = MockSource::new(); + let consumer = TestConsumer::default(); + let (_, mailbox_rx) = mailbox::new(context.child("mailbox"), NZUsize!(16)); + let mut actor = Actor::new( + context.child("actor"), + source, + mailbox_rx, + consumer, + DEFAULT_FETCH_RETRY_TIMEOUT, + ); + + let key = handler::Key::Block(digest); + let subscriber = handler::Annotation::Certified { + height: Height::new(1), + }; + actor.requests.insert( + key, + RequestState { + subscribers: Arc::new(Mutex::new(vec![subscriber])), + attempt: Attempt::Scheduled(context.current()), + }, + ); + actor.start_fetch(key); + let first_state = actor.requests.remove(&key).expect("missing first state"); + let Attempt::Active { id: first_id, .. } = first_state.attempt else { + panic!("expected first fetch attempt to be active"); + }; + + actor.requests.insert( + key, + RequestState { + subscribers: Arc::new(Mutex::new(vec![subscriber])), + attempt: Attempt::Scheduled(context.current()), + }, + ); + actor.start_fetch(key); + let Some(RequestState { + attempt: Attempt::Active { id: second_id, .. }, + .. + }) = actor.requests.get(&key) + else { + panic!("expected second fetch attempt to be active"); + }; + let second_id = *second_id; + + actor.handle_completed(Result { + key, + id: first_id, + retry: true, + }); + + assert!(matches!( + actor.requests.get(&key), + Some(RequestState { + attempt: Attempt::Active { id, .. }, + .. + }) if *id == second_id + )); + assert!(actor.retry_schedule.is_empty()); + }); + } + + #[test_traced] + fn targeted_fetch_variants_use_same_source_path() { + let fixture = TestFixture::new(); + let block = fixture.create_block(1, 1); + let digest = block.digest(); + let calls = Arc::new(AtomicU32::new(0)); + + let source = MockSource::new(); + { + let calls = calls.clone(); + *source.block_handler.lock() = Some(Box::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + Some(Payload::Block(Box::new(block.clone()))) + })); + } + + deterministic::Runner::default().start(|context| async move { + let consumer = TestConsumer::default(); + let mut resolver = start_resolver(context.child("resolver"), source, consumer.clone()); + let target = PrivateKey::from_seed(7).public_key(); + + assert!(resolver + .fetch_targeted( + handler::Request::certified_block(digest, Height::new(1)), + NonEmptyVec::new(target) + ) + .accepted()); + let delivery = wait_for_delivery(&context, &consumer).await; + delivery.response.send(true).expect("response dropped"); + + assert_eq!(calls.load(Ordering::Relaxed), 1); + }); } } diff --git a/follower/src/test_utils.rs b/follower/src/test_utils.rs index a6bd2293..409baeab 100644 --- a/follower/src/test_utils.rs +++ b/follower/src/test_utils.rs @@ -34,7 +34,6 @@ pub type NotarizedHandler = #[derive(Clone)] pub struct MockSource { pub block_handler: BlockHandler, - #[allow(dead_code)] pub finalized_handler: FinalizedHandler, pub notarized_handler: NotarizedHandler, pub messages: Arc>>, diff --git a/validator/src/main.rs b/validator/src/main.rs index 3d93cf4a..3a276a04 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -79,6 +79,7 @@ fn main() { .worker_threads .checked_add(config.signature_threads) .expect("network buffer pool parallelism overflowed"); + // Storage I/O runs on Tokio's blocking pool. Include those threads in the // pool parallelism calculation so buffers cannot be stranded in too few // thread-local caches and surface as exhaustion under restart pressure. From 9fa0783cb2e9437f1be72cbe36093f601067e82b Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 23 May 2026 16:17:15 -0700 Subject: [PATCH 09/11] simplify --- chain/src/application.rs | 23 ++++++++++++----------- chain/src/indexer/backfiller/producer.rs | 12 +++++++++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/chain/src/application.rs b/chain/src/application.rs index 00427fc5..42e29408 100644 --- a/chain/src/application.rs +++ b/chain/src/application.rs @@ -28,7 +28,7 @@ const MAX_BLOCK_TIMESTAMP_MS: u64 = 7_258_118_400_000; pub struct Application { context: Arc, - backfiller: Option>, + backfiller: Option, } impl Clone for Application { @@ -58,7 +58,7 @@ impl Application { } pub(crate) fn with_backfiller(mut self, backfiller: indexer::Producer) -> Self { - self.backfiller = Some(Arc::new(backfiller)); + self.backfiller = Some(backfiller); self } } @@ -131,15 +131,16 @@ impl Reporter for Application { type Activity = Update; fn report(&mut self, activity: Self::Activity) -> Feedback { - if let Update::Block(block, ack_rx) = activity { - if let Some(backfiller) = self.backfiller.clone() { - let height = block.height(); - info!(height = %height, "finalized block"); - return backfiller.record(block, ack_rx); - } else { - info!(height = %block.height(), "finalized block"); - ack_rx.acknowledge(); - } + if let Update::Block(block, _) = &activity { + info!(height = %block.height(), "finalized block"); + } + + if let Some(backfiller) = &mut self.backfiller { + return backfiller.report(activity); + } + + if let Update::Block(_, ack_rx) = activity { + ack_rx.acknowledge(); } Feedback::Ok } diff --git a/chain/src/indexer/backfiller/producer.rs b/chain/src/indexer/backfiller/producer.rs index 1755e267..2636373e 100644 --- a/chain/src/indexer/backfiller/producer.rs +++ b/chain/src/indexer/backfiller/producer.rs @@ -4,6 +4,7 @@ use commonware_actor::{ mailbox::{self, Policy}, Feedback, }; +use commonware_consensus::{marshal::Update, Reporter}; use commonware_runtime::{spawn_cell, Clock, ContextCell, Handle, Metrics, Spawner, Storage}; use commonware_storage::queue; use commonware_utils::{acknowledgement::Exact, Acknowledgement}; @@ -36,9 +37,14 @@ struct Actor { receiver: mailbox::Receiver, } -impl Producer { - pub fn record(&self, block: Block, ack: Exact) -> Feedback { - self.sender.enqueue(Message { block, ack }) +impl Reporter for Producer { + type Activity = Update; + + fn report(&mut self, activity: Self::Activity) -> Feedback { + match activity { + Update::Block(block, ack) => self.sender.enqueue(Message { block, ack }), + Update::Tip(_, _, _) => Feedback::Ok, + } } } From 286fdeb6c97fd3814461d1af8aba61d4dabf9559 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 23 May 2026 16:21:30 -0700 Subject: [PATCH 10/11] nits --- chain/src/indexer/backfiller/state.rs | 14 ++++++-------- chain/src/lib.rs | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/chain/src/indexer/backfiller/state.rs b/chain/src/indexer/backfiller/state.rs index 7cd78e30..6766a8fe 100644 --- a/chain/src/indexer/backfiller/state.rs +++ b/chain/src/indexer/backfiller/state.rs @@ -83,11 +83,6 @@ impl State { } pub fn record(&mut self, block: &Block) -> Option { - // Genesis is local bootstrap state and has no certificate upload to backfill. - if block.height.get() == 0 { - return None; - } - let entry = Entry { height: block.height.get(), digest: block.digest(), @@ -201,13 +196,16 @@ mod tests { } #[test] - fn test_upload_state_ignores_genesis() { + fn test_upload_state_records_genesis() { let mut uploads = State::new(); let genesis = test_block(0, 0, b"genesis"); let digest = genesis.digest(); - assert!(uploads.record(&genesis).is_none()); - assert!(uploads.cached_block(&digest).is_none()); + let entry = uploads.record(&genesis).expect("missing genesis entry"); + + assert_eq!(entry.height, 0); + assert_eq!(entry.digest, digest); + assert_eq!(uploads.cached_block(&digest).as_ref(), Some(&genesis)); } #[test] diff --git a/chain/src/lib.rs b/chain/src/lib.rs index 1789541e..bfc4d6df 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -94,7 +94,7 @@ mod tests { }; use commonware_cryptography::{ bls12381::primitives::variant::MinSig, certificate::mocks::Fixture, ed25519::PublicKey, - Signer, + Digestible, Signer, }; use commonware_macros::{select, test_traced}; use commonware_p2p::{ @@ -775,12 +775,22 @@ mod tests { assert!(indexer .finalization_seen .load(std::sync::atomic::Ordering::Relaxed)); + let genesis_digest = + application::Application::::genesis().digest(); + let started_digests = indexer.block_upload_started_digests.lock().clone(); + let expected_genesis_uploads = n as usize; assert_eq!( indexer .block_upload_started .load(std::sync::atomic::Ordering::SeqCst), - 0, - "block uploads should stay idle when certified uploads succeed", + expected_genesis_uploads, + "only genesis should be uploaded as a bare block when certified uploads succeed", + ); + assert!( + started_digests + .iter() + .all(|digest| *digest == genesis_digest), + "non-genesis block uploads should stay idle when certified uploads succeed", ); }); } From 2b0e9e45ed81cc4dbfc4465dd0f65bec82bf3a09 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 23 May 2026 16:29:03 -0700 Subject: [PATCH 11/11] support 0 --- chain/src/lib.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/chain/src/lib.rs b/chain/src/lib.rs index bfc4d6df..cebd35e9 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -780,9 +780,7 @@ mod tests { let started_digests = indexer.block_upload_started_digests.lock().clone(); let expected_genesis_uploads = n as usize; assert_eq!( - indexer - .block_upload_started - .load(std::sync::atomic::Ordering::SeqCst), + started_digests.len(), expected_genesis_uploads, "only genesis should be uploaded as a bare block when certified uploads succeed", ); @@ -961,12 +959,18 @@ mod tests { queue_outstanding(&metrics) > 0, "expected finalized queue work while certificate uploads were blocked", ); + let genesis_digest = + application::Application::::genesis().digest(); + let expected_genesis_uploads = n as usize; + let started_digests = indexer.block_upload_started_digests.lock().clone(); assert_eq!( - indexer - .block_upload_started - .load(std::sync::atomic::Ordering::SeqCst), - 0, - "block uploads should wait while certificate uploads are still in flight", + started_digests.len(), + expected_genesis_uploads, + "only genesis should be uploaded as a bare block while certificate uploads are in flight", + ); + assert!( + started_digests.iter().all(|digest| *digest == genesis_digest), + "non-genesis block uploads should wait while certificate uploads are still in flight", ); // Release the blocked certificate uploads and confirm the @@ -986,12 +990,15 @@ mod tests { assert!(indexer .finalization_seen .load(std::sync::atomic::Ordering::Relaxed)); + let started_digests = indexer.block_upload_started_digests.lock().clone(); assert_eq!( - indexer - .block_upload_started - .load(std::sync::atomic::Ordering::SeqCst), - 0, - "block uploads should remain idle when certificate uploads eventually succeed", + started_digests.len(), + expected_genesis_uploads, + "only genesis should be uploaded as a bare block when certificate uploads eventually succeed", + ); + assert!( + started_digests.iter().all(|digest| *digest == genesis_digest), + "non-genesis block uploads should remain idle when certificate uploads eventually succeed", ); }); }