From a2df83edbb70ee687a62b788aeeea54ff1245d8c Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:34:30 +0200 Subject: [PATCH 01/19] Add modular block authentication schemes --- Cargo.lock | 170 ++++++- README.md | 22 + crates/orchestrator/README.md | 2 +- crates/orchestrator/src/benchmark.rs | 3 +- crates/orchestrator/src/main.rs | 3 +- crates/starfish-core/Cargo.toml | 1 + crates/starfish-core/src/block_handler.rs | 6 +- .../src/bls_certificate_aggregator.rs | 21 +- crates/starfish-core/src/committee.rs | 22 +- crates/starfish-core/src/config.rs | 37 +- crates/starfish-core/src/core.rs | 26 +- crates/starfish-core/src/crypto.rs | 424 ++++++++++++++---- crates/starfish-core/src/dag_state.rs | 103 ++++- crates/starfish-core/src/encoder.rs | 3 +- crates/starfish-core/src/net_sync.rs | 13 +- crates/starfish-core/src/sailfish_service.rs | 1 - crates/starfish-core/src/threshold_clock.rs | 8 +- .../src/transactions_generator.rs | 3 +- crates/starfish-core/src/types.rs | 418 +++++++++++++++-- crates/starfish-core/src/validator.rs | 38 +- crates/starfish/src/main.rs | 9 +- 21 files changed, 1123 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33c4e143..fc80648f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,6 +642,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "1.3.3" @@ -741,7 +747,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -929,6 +935,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "color-eyre" version = "0.6.5" @@ -979,6 +991,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1020,6 +1038,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 = "crc32fast" version = "1.5.0" @@ -1089,6 +1116,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -1110,6 +1146,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1158,6 +1203,16 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.6" @@ -1184,10 +1239,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "crypto-common 0.2.2", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1691,6 +1755,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "ctutils", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "0.14.32" @@ -2064,6 +2139,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2296,6 +2381,34 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8", + "shake", + "signature", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -2537,6 +2650,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2814,7 +2937,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", @@ -3297,7 +3420,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -3309,10 +3432,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3359,6 +3493,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3400,6 +3543,22 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "ssh2" version = "0.9.5" @@ -3452,6 +3611,7 @@ dependencies = [ "libc", "lz4_flex", "memmap2", + "ml-dsa", "parking_lot", "prettytable-rs", "prometheus 0.13.4", diff --git a/README.md b/README.md index 8afdc05e..e4cce090 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,28 @@ achieving 2-round optimistic commit latency. leader, data availability) in block headers, with async verification offloaded from the critical path. +### Starfish block authentication experiments + +Plain Starfish can be run with three interchangeable block-authentication +schemes: + +| CLI name | Block authentication | +|---|---| +| `starfish` | Ed25519 signature | +| `starfish-mac` | Full vector of pairwise keyed-BLAKE3 MAC tags | +| `starfish-ml-dsa-44` | ML-DSA-44 signature | + +For all three variants, `BlockReference.digest` is the BLAKE3 hash of the +canonical block content only. The authentication proof is a separate header +field and does not change the block reference. A `starfish-mac` block carries +exactly one tag for every committee member; each receiver verifies only its +own tag. Benchmark genesis deterministically generates the pairwise MAC keys, +ML-DSA seeds, and public keys in the node configuration. + +This is research/benchmark code. The RustCrypto `ml-dsa` implementation used +here states that it has not been independently audited and should not be +treated as production-ready cryptography. + ## Dissemination Modes Every protocol can run with any of three dissemination strategies diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index f441425c..ba884de6 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,7 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-speed`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index 4f8d84f4..6965d4a3 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -55,7 +55,8 @@ pub struct BenchmarkParametersGeneric { /// paying for data sent between the nodes. pub use_internal_ip_address: bool, // Consensus protocol to deploy - // (starfish | starfish-speed | sparse-starfish-speed | starfish-bls | + // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-speed | + // sparse-starfish-speed | starfish-bls | // mysticeti | mysticeti-bls | cordial-miners | bluestreak | sailfish-pp) pub consensus_protocol: String, /// number Byzantine nodes diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index eec38cac..545c63fb 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -135,7 +135,8 @@ pub enum Operation { skip_testbed_configuration: bool, /// Protocols to benchmark in order. Available options: - /// starfish | starfish-speed | sparse-starfish-speed | + /// starfish | starfish-mac | starfish-ml-dsa-44 | + /// starfish-speed | sparse-starfish-speed | /// starfish-bls | mysticeti | mysticeti-bls | /// cordial-miners | bluestreak | sailfish-pp #[clap( diff --git a/crates/starfish-core/Cargo.toml b/crates/starfish-core/Cargo.toml index fc9fd95e..0a6332fc 100644 --- a/crates/starfish-core/Cargo.toml +++ b/crates/starfish-core/Cargo.toml @@ -22,6 +22,7 @@ hex = "0.4.3" libc = "0.2.146" lz4_flex = "0.11" memmap2 = "0.7.0" +ml-dsa = { version = "0.1.1", default-features = false, features = ["alloc", "zeroize"] } parking_lot = "0.12.1" prettytable-rs = "0.10" diff --git a/crates/starfish-core/src/block_handler.rs b/crates/starfish-core/src/block_handler.rs index 001fa0a0..871a381a 100644 --- a/crates/starfish-core/src/block_handler.rs +++ b/crates/starfish-core/src/block_handler.rs @@ -33,8 +33,9 @@ const REAL_BLOCK_HANDLER_TXN_GEN_STEP: usize = 32; const _: () = assert_constants(); #[allow(dead_code)] +#[allow(clippy::manual_is_multiple_of)] const fn assert_constants() { - if !REAL_BLOCK_HANDLER_TXN_SIZE.is_multiple_of(REAL_BLOCK_HANDLER_TXN_GEN_STEP) { + if REAL_BLOCK_HANDLER_TXN_SIZE % REAL_BLOCK_HANDLER_TXN_GEN_STEP != 0 { panic!("REAL_BLOCK_HANDLER_TXN_SIZE % REAL_BLOCK_HANDLER_TXN_GEN_STEP != 0") } } @@ -264,6 +265,7 @@ impl RealCommitHandler { } impl CommitObserver for RealCommitHandler { + #[allow(clippy::manual_is_multiple_of)] fn handle_commit( &mut self, dag_state: &DagState, @@ -292,7 +294,7 @@ impl CommitObserver for RealCommitHandler { let digest_short = u16::from_le_bytes([self.commit_digest[0], self.commit_digest[1]]) & 0x3FF; self.metrics.commit_digest_latest.set(digest_short as i64); - if commit_index.is_multiple_of(100) { + if commit_index % 100 == 0 { self.metrics.commit_digest.set(digest_short as i64); } diff --git a/crates/starfish-core/src/bls_certificate_aggregator.rs b/crates/starfish-core/src/bls_certificate_aggregator.rs index 865dedcd..9f9fe847 100644 --- a/crates/starfish-core/src/bls_certificate_aggregator.rs +++ b/crates/starfish-core/src/bls_certificate_aggregator.rs @@ -297,18 +297,15 @@ impl BlsCertificateAggregator { } else if let Some(&origin_index) = seen_leader_certs.get(&(*leader_ref, *cert)) { push_task_source(&mut origins[origin_index], source); - } else { - if let Some(task) = self.aggregate_same_message_task( - crypto::bls_leader_message(leader_ref), - cert, - ) { - tasks.push(BlsVerificationTask { - block_index: origins.len(), - ..task - }); - seen_leader_certs.insert((*leader_ref, *cert), origins.len()); - origins.push(TaskOrigin::AggLeader(*leader_ref, *cert, vec![source])); - } + } else if let Some(task) = self + .aggregate_same_message_task(crypto::bls_leader_message(leader_ref), cert) + { + tasks.push(BlsVerificationTask { + block_index: origins.len(), + ..task + }); + seen_leader_certs.insert((*leader_ref, *cert), origins.len()); + origins.push(TaskOrigin::AggLeader(*leader_ref, *cert, vec![source])); } } } diff --git a/crates/starfish-core/src/committee.rs b/crates/starfish-core/src/committee.rs index fe8a3bc8..d6515928 100644 --- a/crates/starfish-core/src/committee.rs +++ b/crates/starfish-core/src/committee.rs @@ -10,7 +10,10 @@ use serde::{Deserialize, Serialize}; use crate::{ config::ImportExport, - crypto::{BlsPublicKey, BlsSigner, PublicKey, Signer, dummy_bls_public_key, dummy_public_key}, + crypto::{ + BlsPublicKey, BlsSigner, MlDsa44PublicKey, MlDsa44Signer, PublicKey, Signer, + dummy_bls_public_key, dummy_ml_dsa_44_public_key, dummy_public_key, + }, data::Data, types::{AuthorityIndex, AuthoritySet, RoundNumber, Stake, VerifiedBlock}, }; @@ -143,6 +146,12 @@ impl Committee { .map(Authority::bls_public_key) } + pub fn get_ml_dsa_44_public_key(&self, authority: AuthorityIndex) -> Option<&MlDsa44PublicKey> { + self.authorities + .get(authority as usize) + .map(Authority::ml_dsa_44_public_key) + } + pub fn known_authority(&self, authority: AuthorityIndex) -> bool { (authority as usize) < self.len() } @@ -206,14 +215,17 @@ impl Committee { pub fn new_for_benchmarks(committee_size: usize) -> Arc { let signers = Signer::new_for_test(committee_size); let bls_signers = BlsSigner::new_for_test(committee_size); + let ml_dsa_signers = MlDsa44Signer::new_for_test(committee_size); Self::new( signers .into_iter() .zip(bls_signers) - .map(|(keypair, bls_keypair)| Authority { + .zip(ml_dsa_signers) + .map(|((keypair, bls_keypair), ml_dsa_keypair)| Authority { stake: 1, public_key: keypair.public_key(), bls_public_key: bls_keypair.public_key(), + ml_dsa_44_public_key: ml_dsa_keypair.public_key(), }) .collect(), ) @@ -225,6 +237,7 @@ pub struct Authority { stake: Stake, public_key: PublicKey, bls_public_key: BlsPublicKey, + ml_dsa_44_public_key: MlDsa44PublicKey, } impl Authority { @@ -233,6 +246,7 @@ impl Authority { stake, public_key: dummy_public_key(), bls_public_key: dummy_bls_public_key(), + ml_dsa_44_public_key: dummy_ml_dsa_44_public_key(), } } @@ -247,6 +261,10 @@ impl Authority { pub fn bls_public_key(&self) -> &BlsPublicKey { &self.bls_public_key } + + pub fn ml_dsa_44_public_key(&self) -> &MlDsa44PublicKey { + &self.ml_dsa_44_public_key + } } impl ImportExport for Committee {} diff --git a/crates/starfish-core/src/config.rs b/crates/starfish-core/src/config.rs index 552bbb9c..69c0f52f 100644 --- a/crates/starfish-core/src/config.rs +++ b/crates/starfish-core/src/config.rs @@ -12,7 +12,10 @@ use std::{ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use crate::{ - crypto::{BlsPublicKey, BlsSigner, Signer, dummy_bls_signer, dummy_signer}, + crypto::{ + BlsPublicKey, BlsSigner, MacKey, MlDsa44Signer, Signer, dummy_bls_signer, + dummy_ml_dsa_44_signer, dummy_signer, mac_keyrings_for_test, + }, types::{AuthorityIndex, PublicKey, RoundNumber}, }; @@ -270,6 +273,8 @@ pub struct NodePrivateConfig { authority: AuthorityIndex, pub keypair: Signer, pub bls_keypair: BlsSigner, + pub ml_dsa_44_keypair: MlDsa44Signer, + pub mac_keys: Vec, pub storage_path: PathBuf, } @@ -279,6 +284,8 @@ impl NodePrivateConfig { authority: index, keypair: dummy_signer(), bls_keypair: dummy_bls_signer(), + ml_dsa_44_keypair: dummy_ml_dsa_44_signer(), + mac_keys: Vec::new(), storage_path: PathBuf::from("storage"), } } @@ -286,20 +293,28 @@ impl NodePrivateConfig { pub fn new_for_benchmarks(working_dir: &Path, committee_size: usize) -> Vec { let signers = Signer::new_for_test(committee_size); let bls_signers = BlsSigner::new_for_test(committee_size); + let ml_dsa_signers = MlDsa44Signer::new_for_test(committee_size); + let mac_keyrings = mac_keyrings_for_test(committee_size); signers .into_iter() .zip(bls_signers) + .zip(ml_dsa_signers) + .zip(mac_keyrings) .enumerate() - .map(|(i, (keypair, bls_keypair))| { - let authority = i as AuthorityIndex; - let path = working_dir.join(NodePrivateConfig::default_storage_path(authority)); - Self { - authority, - keypair, - bls_keypair, - storage_path: path, - } - }) + .map( + |(i, (((keypair, bls_keypair), ml_dsa_44_keypair), mac_keys))| { + let authority = i as AuthorityIndex; + let path = working_dir.join(NodePrivateConfig::default_storage_path(authority)); + Self { + authority, + keypair, + bls_keypair, + ml_dsa_44_keypair, + mac_keys, + storage_path: path, + } + }, + ) .collect() } diff --git a/crates/starfish-core/src/core.rs b/crates/starfish-core/src/core.rs index 2a0311cd..2f0d3eab 100644 --- a/crates/starfish-core/src/core.rs +++ b/crates/starfish-core/src/core.rs @@ -20,7 +20,7 @@ use crate::{ linearizer::CommittedSubDag, universal_committer::{UniversalCommitter, UniversalCommitterBuilder}, }, - crypto::{self, AsBytes, BlsSignatureBytes, BlsSigner, Signer}, + crypto::{self, AsBytes, BlsSignatureBytes, BlsSigner, MacKey, MlDsa44Signer, Signer}, dag_state::{ ByzantineStrategy, CACHED_ROUNDS, CommitData, ConsensusProtocol, DagState, DataSource, OwnBlockData, @@ -32,9 +32,10 @@ use crate::{ state::RecoveredState, store::Store, types::{ - AuthorityIndex, AuthoritySet, BaseTransaction, BlockReference, BlsAggregateCertificate, - Encoder, PartialSig, PartialSigKind, ProvableShard, ReconstructedTransactionData, - RoundNumber, SailfishFields, Shard, VerifiedBlock, + AuthorityIndex, AuthoritySet, BaseTransaction, BlockAuthenticationScheme, BlockAuthorizer, + BlockReference, BlsAggregateCertificate, Encoder, PartialSig, PartialSigKind, + ProvableShard, ReconstructedTransactionData, RoundNumber, SailfishFields, Shard, + VerifiedBlock, }, }; @@ -60,6 +61,8 @@ pub struct Core { pub(crate) metrics: Arc, signer: Signer, bls_signer: BlsSigner, + ml_dsa_44_signer: MlDsa44Signer, + mac_keys: Arc>, partial_sig_outbox: Option>, // todo - ugly, probably need to merge syncer and core recovered_committed_blocks: Option>, @@ -185,6 +188,8 @@ impl Core { metrics, signer: private_config.keypair, bls_signer: private_config.bls_keypair, + ml_dsa_44_signer: private_config.ml_dsa_44_keypair, + mac_keys: Arc::new(private_config.mac_keys), partial_sig_outbox, recovered_committed_blocks: Some(committed_blocks), recovered_committed_leaders_count: Some(committed_leaders_count), @@ -206,6 +211,10 @@ impl Core { &self.signer } + pub fn mac_keys(&self) -> Arc> { + self.mac_keys.clone() + } + pub fn get_universal_committer(&self) -> UniversalCommitter { self.committer.clone() } @@ -991,14 +1000,19 @@ impl Core { None }; - let mut block = VerifiedBlock::new_with_signer_and_unprovable( + let authorizer = match self.dag_state.block_authentication_scheme { + BlockAuthenticationScheme::Ed25519 => BlockAuthorizer::Ed25519(&self.signer), + BlockAuthenticationScheme::MacVector => BlockAuthorizer::MacVector(&self.mac_keys), + BlockAuthenticationScheme::MlDsa44 => BlockAuthorizer::MlDsa44(&self.ml_dsa_44_signer), + }; + let mut block = VerifiedBlock::new_with_authorizer_and_unprovable( self.authority, clock_round, block_references, voted_leader_ref, acknowledgment_references.to_vec(), time_ns, - &self.signer, + &authorizer, bls_signer_opt, committee_opt, aggregate_dac_sigs, diff --git a/crates/starfish-core/src/crypto.rs b/crates/starfish-core/src/crypto.rs index f34e03ca..2e97fc5d 100644 --- a/crates/starfish-core/src/crypto.rs +++ b/crates/starfish-core/src/crypto.rs @@ -5,8 +5,12 @@ use std::fmt; use blst::min_sig as bls; -use ed25519_consensus::Signature; -use rand::{SeedableRng, rngs::StdRng}; +use ml_dsa::{ + Keypair as _, MlDsa44, Signature as MlDsaSignature, Signer as MlDsaSignerTrait, + SigningKey as MlDsaSigningKey, Verifier as MlDsaVerifierTrait, + VerifyingKey as MlDsaVerifyingKey, +}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; use rs_merkle::{Hasher, MerkleProof, MerkleTree}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use zeroize::Zeroize; @@ -15,8 +19,8 @@ use crate::{ committee::Committee, crypto, types::{ - AuthorityIndex, AuthoritySet, BaseTransaction, BlockHeader, BlockReference, RoundNumber, - Shard, TimestampNs, + AuthorityIndex, AuthoritySet, BaseTransaction, BlockReference, RoundNumber, Shard, + TimestampNs, }, }; @@ -73,6 +77,11 @@ pub fn sailfish_novote_digest(round: RoundNumber, leader: AuthorityIndex) -> [u8 pub const SIGNATURE_SIZE: usize = 64; pub const BLOCK_DIGEST_SIZE: usize = 32; +pub const MAC_KEY_SIZE: usize = 32; +pub const MAC_TAG_SIZE: usize = 32; +pub const ML_DSA_44_SEED_SIZE: usize = 32; +pub const ML_DSA_44_PUBLIC_KEY_SIZE: usize = 1_312; +pub const ML_DSA_44_SIGNATURE_SIZE: usize = 2_420; pub const TRANSACTIONS_DIGEST_SIZE: usize = 32; @@ -88,6 +97,23 @@ pub struct PublicKey(ed25519_consensus::VerificationKey); #[derive(Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct SignatureBytes([u8; SIGNATURE_SIZE]); +/// A pairwise secret key shared by exactly two validators. +#[derive(Clone, Eq, PartialEq)] +pub struct MacKey([u8; MAC_KEY_SIZE]); + +#[derive(Clone, Copy, Ord, PartialOrd)] +pub struct MacTag([u8; MAC_TAG_SIZE]); + +#[derive(Clone, Eq, PartialEq)] +pub struct MlDsa44SignatureBytes(Box<[u8; ML_DSA_44_SIGNATURE_SIZE]>); + +#[derive(Clone)] +pub struct MlDsa44PublicKey(MlDsaVerifyingKey); + +/// Boxed so moving this wrapper does not copy private key material. +#[derive(Clone)] +pub struct MlDsa44Signer(Box>); + // Box ensures value is not copied in memory when Signer itself is moved around // for better security #[derive(Clone)] @@ -175,13 +201,16 @@ impl TransactionsCommitment { } } impl BlockDigest { + pub fn as_array(&self) -> &[u8; BLOCK_DIGEST_SIZE] { + &self.0 + } + pub fn new_without_transactions( authority: AuthorityIndex, round: RoundNumber, block_references: &[BlockReference], acknowledgment_references: &[BlockReference], meta_creation_time_ns: TimestampNs, - signature: &SignatureBytes, merkle_root: Option, strong_vote: Option, ) -> Self { @@ -191,7 +220,6 @@ impl BlockDigest { block_references, acknowledgment_references, meta_creation_time_ns, - signature, merkle_root, strong_vote, None, @@ -204,13 +232,12 @@ impl BlockDigest { block_references: &[BlockReference], acknowledgment_references: &[BlockReference], meta_creation_time_ns: TimestampNs, - signature: &SignatureBytes, merkle_root: Option, strong_vote: Option, unprovable_certificate: Option<&(BlockReference, bool)>, ) -> Self { let mut hasher = Blake3Hasher::new(); - Self::digest_without_signature( + Self::digest_contents( &mut hasher, authority, round, @@ -221,7 +248,6 @@ impl BlockDigest { strong_vote, ); Self::hash_unprovable_certificate(&mut hasher, unprovable_certificate); - hasher.update(signature.as_bytes()); Self(hasher.finalize().into()) } @@ -231,7 +257,6 @@ impl BlockDigest { block_references: &[BlockReference], acknowledgment_references: &[BlockReference], meta_creation_time_ns: TimestampNs, - signature: &SignatureBytes, transactions_commitment: Option, strong_vote: Option, ) -> Self { @@ -241,7 +266,6 @@ impl BlockDigest { block_references, acknowledgment_references, meta_creation_time_ns, - signature, transactions_commitment, strong_vote, None, @@ -254,13 +278,12 @@ impl BlockDigest { block_references: &[BlockReference], acknowledgment_references: &[BlockReference], meta_creation_time_ns: TimestampNs, - signature: &SignatureBytes, transactions_commitment: Option, strong_vote: Option, unprovable_certificate: Option<&(BlockReference, bool)>, ) -> Self { let mut hasher = Blake3Hasher::new(); - Self::digest_without_signature( + Self::digest_contents( &mut hasher, authority, round, @@ -271,11 +294,10 @@ impl BlockDigest { strong_vote, ); Self::hash_unprovable_certificate(&mut hasher, unprovable_certificate); - hasher.update(signature.as_bytes()); Self(hasher.finalize().into()) } - pub(crate) fn digest_without_signature( + pub(crate) fn digest_contents( hasher: &mut Blake3Hasher, authority: AuthorityIndex, round: RoundNumber, @@ -305,7 +327,7 @@ impl BlockDigest { /// Extend a block digest hasher with the generalized unprovable /// certificate reference + strong/standard flavor flag. Called after - /// `digest_without_signature` and before finalizing. No-op when `None`, + /// `digest_contents` and before finalizing. No-op when `None`, /// preserving backward compatibility. pub(crate) fn hash_unprovable_certificate( hasher: &mut Blake3Hasher, @@ -473,33 +495,234 @@ fn deserialize_fixed_bytes<'de, D: Deserializer<'de>, const N: usize>( } } -impl PublicKey { - pub fn verify_signature_in_block( +impl MacKey { + pub fn compute_tag( &self, - header: &BlockHeader, - transactions_commitment: Option, - ) -> Result<(), ed25519_consensus::Error> { - let signature = Signature::from(header.signature().0); - let acknowledgments = header.acknowledgments(); - let mut hasher = Blake3Hasher::new(); - BlockDigest::digest_without_signature( - &mut hasher, - header.authority(), - header.round(), - header.block_references(), - &acknowledgments, - header.meta_creation_time_ns(), - transactions_commitment, - header.strong_vote(), - ); - BlockDigest::hash_unprovable_certificate( - &mut hasher, - header.unprovable_certificate.as_ref(), - ); - let digest: [u8; BLOCK_DIGEST_SIZE] = hasher.finalize().into(); - self.0.verify(&signature, digest.as_ref()) + author: AuthorityIndex, + recipient: AuthorityIndex, + content_digest: &BlockDigest, + ) -> MacTag { + let mut hasher = Blake3Hasher::new_keyed(&self.0); + hasher.update(&author.to_be_bytes()); + hasher.update(&recipient.to_be_bytes()); + hasher.update(content_digest.as_ref()); + MacTag(hasher.finalize().into()) + } +} + +/// Generate deterministic, symmetric pairwise keyrings for local benchmarks +/// and tests. Entry `keyrings[a][b]` equals `keyrings[b][a]`. +#[allow(clippy::needless_range_loop)] +pub fn mac_keyrings_for_test(n: usize) -> Vec> { + let mut rng = StdRng::seed_from_u64(0x5354_4152_4649_5348); + let mut keyrings = vec![vec![MacKey([0; MAC_KEY_SIZE]); n]; n]; + for author in 0..n { + for recipient in author..n { + let mut bytes = [0; MAC_KEY_SIZE]; + rng.fill_bytes(&mut bytes); + let key = MacKey(bytes); + keyrings[author][recipient] = key.clone(); + keyrings[recipient][author] = key; + } + } + keyrings +} + +impl Drop for MacKey { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +impl fmt::Debug for MacKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("MacKey(REDACTED)") + } +} + +impl Serialize for MacKey { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(&self.0, serializer) + } +} + +impl<'de> Deserialize<'de> for MacKey { + fn deserialize>(deserializer: D) -> Result { + deserialize_fixed_bytes::(deserializer, "MAC key").map(Self) } +} + +impl AsBytes for MacTag { + fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl PartialEq for MacTag { + fn eq(&self, other: &Self) -> bool { + blake3::Hash::from_bytes(self.0) == blake3::Hash::from_bytes(other.0) + } +} +impl Eq for MacTag {} + +impl std::hash::Hash for MacTag { + fn hash(&self, state: &mut H) { + std::hash::Hash::hash(&self.0, state); + } +} + +impl AsRef<[u8]> for MacTag { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Debug for MacTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Mac({})", &hex::encode(&self.0[..4])) + } +} + +impl Serialize for MacTag { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(&self.0, serializer) + } +} + +impl<'de> Deserialize<'de> for MacTag { + fn deserialize>(deserializer: D) -> Result { + deserialize_fixed_bytes::(deserializer, "MAC tag").map(Self) + } +} + +impl MlDsa44SignatureBytes { + pub fn from_bytes(bytes: [u8; ML_DSA_44_SIGNATURE_SIZE]) -> Self { + Self(Box::new(bytes)) + } +} + +impl AsRef<[u8]> for MlDsa44SignatureBytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl fmt::Debug for MlDsa44SignatureBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MlDsa44Sig({})", &hex::encode(&self.0[..4])) + } +} + +impl Serialize for MlDsa44SignatureBytes { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(self.0.as_ref(), serializer) + } +} + +impl<'de> Deserialize<'de> for MlDsa44SignatureBytes { + fn deserialize>(deserializer: D) -> Result { + deserialize_fixed_bytes::(deserializer, "ML-DSA-44 signature") + .map(Self::from_bytes) + } +} + +impl MlDsa44PublicKey { + pub fn from_bytes(bytes: &[u8; ML_DSA_44_PUBLIC_KEY_SIZE]) -> Self { + let encoded = ml_dsa::EncodedVerifyingKey::::from(*bytes); + Self(MlDsaVerifyingKey::decode(&encoded)) + } + + pub fn to_bytes(&self) -> [u8; ML_DSA_44_PUBLIC_KEY_SIZE] { + self.0.encode().into() + } + + pub fn verify_digest_signature( + &self, + digest: &BlockDigest, + signature: &MlDsa44SignatureBytes, + ) -> Result<(), ml_dsa::signature::Error> { + let signature = MlDsaSignature::::try_from(signature.as_ref())?; + self.0.verify(digest.as_ref(), &signature) + } +} + +impl PartialEq for MlDsa44PublicKey { + fn eq(&self, other: &Self) -> bool { + self.to_bytes() == other.to_bytes() + } +} + +impl Eq for MlDsa44PublicKey {} + +impl fmt::Debug for MlDsa44PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MlDsa44Pk({})", &hex::encode(&self.to_bytes()[..4])) + } +} + +impl Serialize for MlDsa44PublicKey { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(&self.to_bytes(), serializer) + } +} + +impl<'de> Deserialize<'de> for MlDsa44PublicKey { + fn deserialize>(deserializer: D) -> Result { + let bytes = deserialize_fixed_bytes::( + deserializer, + "ML-DSA-44 public key", + )?; + Ok(Self::from_bytes(&bytes)) + } +} + +impl MlDsa44Signer { + pub fn new_for_test(n: usize) -> Vec { + let mut rng = StdRng::seed_from_u64(0x4d4c_4453_4134_3400); + (0..n) + .map(|_| { + let mut bytes = [0; ML_DSA_44_SEED_SIZE]; + rng.fill_bytes(&mut bytes); + let seed = ml_dsa::Seed::from(bytes); + Self(Box::new(MlDsaSigningKey::from_seed(&seed))) + }) + .collect() + } + + pub fn sign_digest(&self, digest: &BlockDigest) -> MlDsa44SignatureBytes { + let signature: MlDsaSignature = self.0.sign(digest.as_ref()); + MlDsa44SignatureBytes::from_bytes(signature.encode().into()) + } + + pub fn public_key(&self) -> MlDsa44PublicKey { + MlDsa44PublicKey(self.0.verifying_key()) + } +} + +impl fmt::Debug for MlDsa44Signer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MlDsa44Signer(public_key={:?})", self.public_key()) + } +} + +impl Serialize for MlDsa44Signer { + fn serialize(&self, serializer: S) -> Result { + let seed: [u8; ML_DSA_44_SEED_SIZE] = self.0.to_seed().into(); + serialize_fixed_bytes(&seed, serializer) + } +} + +impl<'de> Deserialize<'de> for MlDsa44Signer { + fn deserialize>(deserializer: D) -> Result { + let bytes = + deserialize_fixed_bytes::(deserializer, "ML-DSA-44 seed")?; + let seed = ml_dsa::Seed::from(bytes); + Ok(Self(Box::new(MlDsaSigningKey::from_seed(&seed)))) + } +} + +impl PublicKey { pub fn to_bytes(&self) -> [u8; 32] { let mut bytes = [0u8; 32]; bytes.copy_from_slice(self.0.as_ref()); @@ -530,56 +753,6 @@ impl Signer { .collect() } - pub fn sign_block( - &self, - authority: AuthorityIndex, - round: RoundNumber, - block_references: &[BlockReference], - acknowledgment_references: &[BlockReference], - meta_creation_time_ns: TimestampNs, - transactions_commitment: Option, - strong_vote: Option, - ) -> SignatureBytes { - self.sign_block_with_unprovable( - authority, - round, - block_references, - acknowledgment_references, - meta_creation_time_ns, - transactions_commitment, - strong_vote, - None, - ) - } - - pub fn sign_block_with_unprovable( - &self, - authority: AuthorityIndex, - round: RoundNumber, - block_references: &[BlockReference], - acknowledgment_references: &[BlockReference], - meta_creation_time_ns: TimestampNs, - transactions_commitment: Option, - strong_vote: Option, - unprovable_certificate: Option<&(BlockReference, bool)>, - ) -> SignatureBytes { - let mut hasher = Blake3Hasher::new(); - BlockDigest::digest_without_signature( - &mut hasher, - authority, - round, - block_references, - acknowledgment_references, - meta_creation_time_ns, - transactions_commitment, - strong_vote, - ); - BlockDigest::hash_unprovable_certificate(&mut hasher, unprovable_certificate); - let digest: [u8; BLOCK_DIGEST_SIZE] = hasher.finalize().into(); - let signature = self.0.sign(digest.as_ref()); - SignatureBytes(signature.to_bytes()) - } - /// Sign a pre-computed 32-byte digest. Used for Sailfish++ control /// messages (timeout, no-vote) that don't fit the block-signing schema. pub fn sign_digest(&self, digest: &[u8; 32]) -> SignatureBytes { @@ -744,6 +917,15 @@ pub fn dummy_public_key() -> PublicKey { dummy_signer().public_key() } +pub fn dummy_ml_dsa_44_signer() -> MlDsa44Signer { + let seed = ml_dsa::Seed::from([0; ML_DSA_44_SEED_SIZE]); + MlDsa44Signer(Box::new(MlDsaSigningKey::from_seed(&seed))) +} + +pub fn dummy_ml_dsa_44_public_key() -> MlDsa44PublicKey { + dummy_ml_dsa_44_signer().public_key() +} + // --------------------------------------------------------------------------- // BLS12-381 types (min_sig variant: 96-byte G2 public keys, 48-byte G1 // signatures). @@ -1052,6 +1234,44 @@ mod tests { use super::*; use serde::{Deserialize, Serialize}; + #[test] + fn mac_keyrings_are_symmetric_and_bind_recipient() { + let keyrings = mac_keyrings_for_test(4); + let digest = BlockDigest([7; BLOCK_DIGEST_SIZE]); + let tag = keyrings[1][3].compute_tag(1, 3, &digest); + + assert_eq!(tag, keyrings[3][1].compute_tag(1, 3, &digest)); + assert_ne!(tag, keyrings[3][1].compute_tag(1, 2, &digest)); + assert_ne!(tag, keyrings[3][1].compute_tag(2, 3, &digest)); + } + + #[test] + fn ml_dsa_44_sign_verify_and_serde_roundtrip() { + let signer = MlDsa44Signer::new_for_test(1).pop().unwrap(); + let public_key = signer.public_key(); + let digest = BlockDigest([9; BLOCK_DIGEST_SIZE]); + let signature = signer.sign_digest(&digest); + + assert!( + public_key + .verify_digest_signature(&digest, &signature) + .is_ok() + ); + assert!( + public_key + .verify_digest_signature(&BlockDigest([8; BLOCK_DIGEST_SIZE]), &signature) + .is_err() + ); + + let encoded_key = bincode::serialize(&public_key).unwrap(); + let decoded_key: MlDsa44PublicKey = bincode::deserialize(&encoded_key).unwrap(); + let encoded_signature = bincode::serialize(&signature).unwrap(); + let decoded_signature: MlDsa44SignatureBytes = + bincode::deserialize(&encoded_signature).unwrap(); + assert_eq!(public_key, decoded_key); + assert_eq!(signature, decoded_signature); + } + #[test] fn bls_sign_verify_roundtrip() { let signers = BlsSigner::new_for_test(3); @@ -1111,6 +1331,11 @@ mod tests { bls_signer: BlsSigner, bls_public_key: BlsPublicKey, bls_signature: BlsSignatureBytes, + mac_key: MacKey, + mac_tag: MacTag, + ml_dsa_44_signer: MlDsa44Signer, + ml_dsa_44_public_key: MlDsa44PublicKey, + ml_dsa_44_signature: MlDsa44SignatureBytes, } #[test] @@ -1118,15 +1343,24 @@ mod tests { let signer = Signer::new_for_test(1).pop().unwrap(); let public_key = signer.public_key(); let bls_signer = dummy_bls_signer(); + let mac_key = MacKey([10; MAC_KEY_SIZE]); + let block_digest = BlockDigest([7u8; BLOCK_DIGEST_SIZE]); + let mac_tag = mac_key.compute_tag(0, 1, &block_digest); + let ml_dsa_44_signer = dummy_ml_dsa_44_signer(); let fixture = CryptoYamlFixture { signer, public_key, - block_digest: BlockDigest([7u8; BLOCK_DIGEST_SIZE]), + block_digest, transactions_commitment: TransactionsCommitment([8u8; TRANSACTIONS_DIGEST_SIZE]), signature: SignatureBytes([9u8; SIGNATURE_SIZE]), bls_public_key: bls_signer.public_key(), bls_signature: bls_signer.sign_digest(&[5u8; 32]), bls_signer, + mac_key, + mac_tag, + ml_dsa_44_public_key: ml_dsa_44_signer.public_key(), + ml_dsa_44_signature: ml_dsa_44_signer.sign_digest(&block_digest), + ml_dsa_44_signer, }; let yaml = serde_yaml::to_string(&fixture).unwrap(); @@ -1142,6 +1376,14 @@ mod tests { assert!(fixture.signature == decoded.signature); assert_eq!(fixture.bls_public_key, decoded.bls_public_key); assert_eq!(fixture.bls_signature, decoded.bls_signature); + assert_eq!(fixture.mac_key, decoded.mac_key); + assert_eq!(fixture.mac_tag, decoded.mac_tag); + assert_eq!(fixture.ml_dsa_44_public_key, decoded.ml_dsa_44_public_key); + assert_eq!(fixture.ml_dsa_44_signature, decoded.ml_dsa_44_signature); + assert_eq!( + fixture.ml_dsa_44_signer.public_key(), + decoded.ml_dsa_44_signer.public_key() + ); assert_eq!( fixture.bls_signer.public_key(), decoded.bls_signer.public_key() diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index 4192c2d5..0c3e62b8 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -32,9 +32,9 @@ use crate::{ store::Store, threshold_clock::ThresholdClockAggregator, types::{ - AuthorityIndex, AuthoritySet, BlockDigest, BlockReference, BlsAggregateCertificate, - ProvableShard, RoundNumber, SailfishNoVoteCert, SailfishTimeoutCert, TransactionData, - VerifiedBlock, + AuthorityIndex, AuthoritySet, BlockAuthenticationScheme, BlockDigest, BlockReference, + BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishNoVoteCert, + SailfishTimeoutCert, TransactionData, VerifiedBlock, }, }; @@ -113,7 +113,7 @@ pub enum DacCertificateVerificationState { Rejected, } -#[derive(Clone, Debug, Copy, PartialEq)] +#[derive(Clone, Debug, Copy, Eq, PartialEq)] pub enum ConsensusProtocol { Mysticeti, CordialMiners, @@ -167,19 +167,25 @@ pub enum ConsensusProtocol { impl ConsensusProtocol { pub fn from_str(s: &str) -> Self { + ProtocolConfig::from_str(s) + .unwrap_or_else(|error| panic!("{error}")) + .consensus_protocol + } + + fn from_known_str(s: &str) -> Option { match s { - "mysticeti" => ConsensusProtocol::Mysticeti, - "cordial-miners" => ConsensusProtocol::CordialMiners, - "starfish" => ConsensusProtocol::Starfish, - "starfish-bls" | "starfish-l" => ConsensusProtocol::StarfishBls, - "starfish-speed" | "starfish-s" => ConsensusProtocol::StarfishSpeed, - "sailfish++" | "sailfish-pp" => ConsensusProtocol::SailfishPlusPlus, - "bluestreak" => ConsensusProtocol::Bluestreak, - "mysticeti-bls" | "mysticeti-l" => ConsensusProtocol::MysticetiBls, + "mysticeti" => Some(ConsensusProtocol::Mysticeti), + "cordial-miners" => Some(ConsensusProtocol::CordialMiners), + "starfish" => Some(ConsensusProtocol::Starfish), + "starfish-bls" | "starfish-l" => Some(ConsensusProtocol::StarfishBls), + "starfish-speed" | "starfish-s" => Some(ConsensusProtocol::StarfishSpeed), + "sailfish++" | "sailfish-pp" => Some(ConsensusProtocol::SailfishPlusPlus), + "bluestreak" => Some(ConsensusProtocol::Bluestreak), + "mysticeti-bls" | "mysticeti-l" => Some(ConsensusProtocol::MysticetiBls), "sparse-starfish-speed" | "sparse-starfish" | "ssfs" => { - ConsensusProtocol::SparseStarfishSpeed + Some(ConsensusProtocol::SparseStarfishSpeed) } - _ => ConsensusProtocol::Starfish, + _ => None, } } @@ -275,6 +281,36 @@ impl ConsensusProtocol { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProtocolConfig { + pub consensus_protocol: ConsensusProtocol, + pub block_authentication_scheme: BlockAuthenticationScheme, +} + +impl ProtocolConfig { + pub fn from_str(value: &str) -> Result { + let (consensus_protocol, block_authentication_scheme) = match value { + "starfish-mac" => ( + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + ), + "starfish-ml-dsa-44" => ( + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MlDsa44, + ), + known => ( + ConsensusProtocol::from_known_str(known) + .ok_or_else(|| format!("Unknown consensus protocol '{known}'"))?, + BlockAuthenticationScheme::Ed25519, + ), + }; + Ok(Self { + consensus_protocol, + block_authentication_scheme, + }) + } +} + const STARFISH_SPEED_HINT_WINDOW_LEADER_ROUNDS: usize = 10; #[allow(unused)] @@ -324,6 +360,7 @@ pub struct DagState { store: Arc, metrics: Arc, pub(crate) consensus_protocol: ConsensusProtocol, + pub(crate) block_authentication_scheme: BlockAuthenticationScheme, pub(crate) committee_size: usize, pub(crate) byzantine_strategy: Option, committee: Arc, @@ -528,7 +565,8 @@ impl DagState { Arc::new(RocksStore::open(&path).expect("Failed to open RocksDB")) } }; - let consensus_protocol = ConsensusProtocol::from_str(&consensus); + let protocol_config = ProtocolConfig::from_str(&consensus).expect("validated protocol"); + let consensus_protocol = protocol_config.consensus_protocol; let resolved_dissemination = consensus_protocol.resolve_dissemination_mode(dissemination_mode); let push_mode = matches!( @@ -815,6 +853,7 @@ impl DagState { dag_state_inner: Arc::new(RwLock::new(inner)), metrics, consensus_protocol, + block_authentication_scheme: protocol_config.block_authentication_scheme, round_block_cache: Arc::new(parking_lot::Mutex::new(AHashMap::new())), genesis, strong_vote_adaptive_acknowledgments, @@ -3386,7 +3425,7 @@ mod tests { use super::{ ByzantineStrategy, CACHED_ROUNDS, CertificateEvent, ConsensusProtocol, - DacCertificateVerificationState, DagState, DataSource, OwnBlockData, + DacCertificateVerificationState, DagState, DataSource, OwnBlockData, ProtocolConfig, }; use crate::{ committee::Committee, @@ -3398,9 +3437,9 @@ mod tests { data::Data, metrics::Metrics, types::{ - AuthorityIndex, AuthoritySet, BaseTransaction, BlockReference, BlsAggregateCertificate, - ProvableShard, RoundNumber, SailfishFields, SailfishNoVoteCert, Transaction, - VerifiedBlock, + AuthorityIndex, AuthoritySet, BaseTransaction, BlockAuthenticationScheme, + BlockReference, BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishFields, + SailfishNoVoteCert, Transaction, VerifiedBlock, }, }; @@ -4748,4 +4787,30 @@ mod tests { DisseminationMode::PushCausal ); } + + #[test] + fn protocol_config_selects_starfish_block_authentication() { + assert_eq!( + ProtocolConfig::from_str("starfish").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Starfish, + block_authentication_scheme: BlockAuthenticationScheme::Ed25519, + } + ); + assert_eq!( + ProtocolConfig::from_str("starfish-mac").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Starfish, + block_authentication_scheme: BlockAuthenticationScheme::MacVector, + } + ); + assert_eq!( + ProtocolConfig::from_str("starfish-ml-dsa-44").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Starfish, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, + } + ); + assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); + } } diff --git a/crates/starfish-core/src/encoder.rs b/crates/starfish-core/src/encoder.rs index 122b2e48..a1e0bc01 100644 --- a/crates/starfish-core/src/encoder.rs +++ b/crates/starfish-core/src/encoder.rs @@ -42,6 +42,7 @@ impl ShardEncoder for Encoder { data } + #[allow(clippy::manual_is_multiple_of)] fn encode_transactions( &mut self, block: &[BaseTransaction], @@ -57,7 +58,7 @@ impl ShardEncoder for Encoder { let mut shard_bytes = (bytes_length + 4).div_ceil(info_length); // Ensure shard_bytes meets alignment requirements (must be multiple of 2). - if !shard_bytes.is_multiple_of(2) { + if shard_bytes % 2 != 0 { shard_bytes += 1; } diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index cef8dd42..3b95bf62 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -34,7 +34,7 @@ use crate::{ }, core::Core, core_thread::CoreThreadDispatcher, - crypto::BlsSigner, + crypto::{BlsSigner, MacKey}, dag_state::{ConsensusProtocol, DagState, DataSource}, data::Data, metrics::{Metrics, UtilizationTimerVecExt}, @@ -435,12 +435,14 @@ fn spawn_header_worker( } let mut block: VerifiedBlock = (*data_block).clone(); tracing::debug!("Received {} from {}", block, peer); - match block.verify( + match block.verify_with_authentication( &inner.committee, own_id as usize, peer_id as usize, &mut encoder, consensus_protocol, + inner.dag_state.block_authentication_scheme, + &inner.mac_keys, ) { Ok(shard) => { debug_assert!(shard.is_none(), "shard must be None for header-only blocks") @@ -975,12 +977,14 @@ impl ConnectionHandler shard, Err(e) => { @@ -1329,6 +1333,7 @@ pub struct NetworkSyncerInner { pub block_ready_notify: Arc, pub proposal_round_notify: Arc, pub committee: Arc, + pub mac_keys: Arc>, pub dissemination_mode: DisseminationMode, pub causal_push_shard_round_lag: RoundNumber, stop: mpsc::Sender<()>, @@ -1367,6 +1372,7 @@ impl NetworkSyncer let (committed, committed_leaders_count) = core.take_recovered_committed(); commit_observer.recover_committed(committed, committed_leaders_count); let committee = core.committee().clone(); + let mac_keys = core.mac_keys(); let dag_state = core.dag_state().clone(); let dissemination_mode = dag_state .consensus_protocol @@ -1459,6 +1465,7 @@ impl NetworkSyncer syncer, proposal_round_notify, committee, + mac_keys, dissemination_mode, causal_push_shard_round_lag: node_parameters.causal_push_shard_round_lag, stop: stop_sender.clone(), diff --git a/crates/starfish-core/src/sailfish_service.rs b/crates/starfish-core/src/sailfish_service.rs index 62fb3d0e..b580fac9 100644 --- a/crates/starfish-core/src/sailfish_service.rs +++ b/crates/starfish-core/src/sailfish_service.rs @@ -551,7 +551,6 @@ mod tests { &[], &[], 1, - &crate::crypto::SignatureBytes::default(), None, None, ), diff --git a/crates/starfish-core/src/threshold_clock.rs b/crates/starfish-core/src/threshold_clock.rs index b70bf4c0..7ba883b0 100644 --- a/crates/starfish-core/src/threshold_clock.rs +++ b/crates/starfish-core/src/threshold_clock.rs @@ -75,10 +75,7 @@ impl ThresholdClockAggregator { mod tests { use super::*; - use crate::{ - crypto::SignatureBytes, - types::{AckFields, AuthorityIndex, BlockDigest, RoundNumber}, - }; + use crate::types::{AckFields, AuthorityIndex, BlockAuthentication, BlockDigest, RoundNumber}; fn make_header( authority: AuthorityIndex, @@ -100,14 +97,13 @@ mod tests { &block_references, &ack_refs, 0, - &SignatureBytes::default(), None, None, ), }, block_references, meta_creation_time_ns: 0, - signature: SignatureBytes::default(), + authentication: BlockAuthentication::None, transactions_commitment: None, ack: Some(AckFields { intersection: None, diff --git a/crates/starfish-core/src/transactions_generator.rs b/crates/starfish-core/src/transactions_generator.rs index 0ca474f9..3725fe6d 100644 --- a/crates/starfish-core/src/transactions_generator.rs +++ b/crates/starfish-core/src/transactions_generator.rs @@ -59,6 +59,7 @@ impl TransactionGenerator { ); } + #[allow(clippy::manual_is_multiple_of)] pub async fn run(mut self) { let load = self.parameters.load; let max_transactions_per_block_interval = load.div_ceil(Self::BATCHES_IN_SECOND); @@ -182,7 +183,7 @@ impl TransactionGenerator { return; } - if counter.is_multiple_of(10_000) { + if counter % 10_000 == 0 { self.metrics .submitted_transactions_bytes .inc_by(tx_to_report * tx_size as u64); diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 53cbc6e6..cb861f00 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -41,8 +41,8 @@ use crate::{ committee::Committee, crypto, crypto::{ - AsBytes, BlsSignatureBytes, BlsSigner, CryptoHash, SignatureBytes, Signer, - TransactionsCommitment, + AsBytes, BlsSignatureBytes, BlsSigner, CryptoHash, MacKey, MacTag, MlDsa44SignatureBytes, + MlDsa44Signer, SignatureBytes, Signer, TransactionsCommitment, }, dag_state::ConsensusProtocol, data::{Data, IN_MEMORY_BLOCKS, IN_MEMORY_BLOCKS_BYTES}, @@ -87,9 +87,7 @@ impl PartialOrd for BlockReference { } // --------------------------------------------------------------------------- -// BlockHeader — signed, content-addressed block identity. -// Contains exactly the fields that feed into BlockDigest::new() and -// sign_block(). +// BlockHeader — authenticated, content-addressed block identity. // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- @@ -239,7 +237,7 @@ pub struct SailfishNoVoteCert { } /// Protocol-specific fields embedded in SailfishPlusPlus block headers. -/// Part of the signed block hash. +/// Part of the authenticated block content hash. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SailfishFields { /// Timeout certificate for the previous round, if this block advances @@ -252,11 +250,56 @@ pub struct SailfishFields { } // --------------------------------------------------------------------------- -// BlockHeader — signed, content-addressed block identity. -// Contains exactly the fields that feed into BlockDigest::new() and -// sign_block(). +// BlockHeader — authenticated, content-addressed block identity. // --------------------------------------------------------------------------- +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] +pub enum BlockAuthentication { + /// Only valid for locally constructed genesis blocks. + None, + Ed25519(SignatureBytes), + MacVector(Vec), + MlDsa44(MlDsa44SignatureBytes), +} + +#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)] +pub enum BlockAuthenticationScheme { + Ed25519, + MacVector, + MlDsa44, +} + +pub enum BlockAuthorizer<'a> { + Ed25519(&'a Signer), + MacVector(&'a [MacKey]), + MlDsa44(&'a MlDsa44Signer), +} + +impl BlockAuthorizer<'_> { + fn authenticate( + &self, + author: AuthorityIndex, + content_digest: &BlockDigest, + ) -> BlockAuthentication { + match self { + Self::Ed25519(signer) => { + BlockAuthentication::Ed25519(signer.sign_digest(content_digest.as_array())) + } + Self::MacVector(keys) => BlockAuthentication::MacVector( + keys.iter() + .enumerate() + .map(|(recipient, key)| { + key.compute_tag(author, recipient as AuthorityIndex, content_digest) + }) + .collect(), + ), + Self::MlDsa44(signer) => { + BlockAuthentication::MlDsa44(signer.sign_digest(content_digest)) + } + } + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct BlockHeader { // -- Base fields (all protocols) ------------------------------------------ @@ -267,8 +310,9 @@ pub struct BlockHeader { pub(crate) block_references: Vec, /// Creation time as reported by creator (currently not enforced). pub(crate) meta_creation_time_ns: TimestampNs, - /// Signature by the block author over the header fields. - pub(crate) signature: SignatureBytes, + /// Authentication proof over `reference.digest`. This field is not part of + /// the content-addressed block identity. + pub(crate) authentication: BlockAuthentication, /// Explicit payload commitment stored in the header. /// Starfish-family protocols carry the Merkle root over encoded shards. /// Full-block protocols leave this as `None` and recompute the raw @@ -367,8 +411,8 @@ impl BlockHeader { self.reference.author_round() } - pub fn signature(&self) -> &SignatureBytes { - &self.signature + pub fn authentication(&self) -> &BlockAuthentication { + &self.authentication } pub fn meta_creation_time_ns(&self) -> TimestampNs { @@ -777,7 +821,6 @@ impl VerifiedBlock { &block_references, &acknowledgments, meta_creation_time_ns, - &signature, merkle_root, strong_vote, unprovable_certificate.as_ref(), @@ -785,7 +828,7 @@ impl VerifiedBlock { }, block_references, meta_creation_time_ns, - signature, + authentication: BlockAuthentication::Ed25519(signature), transactions_commitment: merkle_root, ack: Some(AckFields { intersection: acknowledgment_intersection, @@ -822,14 +865,13 @@ impl VerifiedBlock { &block_refs, &ack_refs, 0, - &SignatureBytes::default(), None, None, ), }, block_references: block_refs, meta_creation_time_ns: 0, - signature: SignatureBytes::default(), + authentication: BlockAuthentication::None, transactions_commitment: None, ack: None, strong_vote: None, @@ -915,6 +957,54 @@ impl VerifiedBlock { precomputed_leader_sig: Option, sailfish: Option, unprovable_certificate: Option<(BlockReference, bool)>, + ) -> Self { + let authorizer = BlockAuthorizer::Ed25519(signer); + Self::new_with_authorizer_and_unprovable( + authority, + round, + block_references, + voted_leader_ref, + acknowledgment_references, + meta_creation_time_ns, + &authorizer, + bls_signer, + committee_opt, + aggregate_dac_sigs, + transactions, + encoded_transactions, + consensus_protocol, + strong_vote, + aggregate_round_sig, + certified_leader, + precomputed_round_sig, + precomputed_leader_sig, + sailfish, + unprovable_certificate, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_with_authorizer_and_unprovable( + authority: AuthorityIndex, + round: RoundNumber, + block_references: Vec, + voted_leader_ref: Option, + acknowledgment_references: Vec, + meta_creation_time_ns: TimestampNs, + authorizer: &BlockAuthorizer<'_>, + bls_signer: Option<&BlsSigner>, + committee_opt: Option<&Committee>, + aggregate_dac_sigs: Vec, + transactions: Vec, + encoded_transactions: Option>, + consensus_protocol: ConsensusProtocol, + strong_vote: Option, + aggregate_round_sig: Option, + certified_leader: Option<(BlockReference, BlsAggregateCertificate)>, + precomputed_round_sig: Option, + precomputed_leader_sig: Option, + sailfish: Option, + unprovable_certificate: Option<(BlockReference, bool)>, ) -> Self { let supports_acknowledgments = consensus_protocol.supports_acknowledgments(); let header_transactions_commitment = if consensus_protocol.supports_acknowledgments() { @@ -950,7 +1040,7 @@ impl VerifiedBlock { &acknowledgment_references, aggregate_dac_sigs, ); - let signature = signer.sign_block_with_unprovable( + let content_digest = BlockDigest::new_without_transactions_with_unprovable( authority, round, &block_references, @@ -960,6 +1050,7 @@ impl VerifiedBlock { strong_vote, unprovable_certificate.as_ref(), ); + let authentication = authorizer.authenticate(authority, &content_digest); // Build BLS fields when the StarfishBls path is active. Partial round // and leader signatures are embedded as belt-and-suspenders alongside @@ -996,21 +1087,11 @@ impl VerifiedBlock { reference: BlockReference { authority, round, - digest: BlockDigest::new_without_transactions_with_unprovable( - authority, - round, - &block_references, - &acknowledgments, - meta_creation_time_ns, - &signature, - digest_transactions_commitment, - strong_vote, - unprovable_certificate.as_ref(), - ), + digest: content_digest, }, block_references, meta_creation_time_ns, - signature, + authentication, transactions_commitment: header_transactions_commitment, ack: supports_acknowledgments.then_some(AckFields { intersection: acknowledgment_intersection, @@ -1075,8 +1156,8 @@ impl VerifiedBlock { self.header.author_round() } - pub fn signature(&self) -> &SignatureBytes { - self.header.signature() + pub fn authentication(&self) -> &BlockAuthentication { + self.header.authentication() } pub fn meta_creation_time_ns(&self) -> TimestampNs { @@ -1208,12 +1289,36 @@ impl VerifiedBlock { _peer_id: usize, encoder: &mut Encoder, consensus_protocol: ConsensusProtocol, + ) -> eyre::Result> { + self.verify_with_authentication( + committee, + own_id, + _peer_id, + encoder, + consensus_protocol, + BlockAuthenticationScheme::Ed25519, + &[], + ) + } + + pub fn verify_with_authentication( + &mut self, + committee: &Committee, + own_id: usize, + _peer_id: usize, + encoder: &mut Encoder, + consensus_protocol: ConsensusProtocol, + authentication_scheme: BlockAuthenticationScheme, + mac_keys: &[MacKey], ) -> eyre::Result> { let (shard, digest_transactions_commitment) = self.verify_transactions(committee, own_id, encoder, consensus_protocol)?; self.verify_block_structure( committee, + own_id, consensus_protocol, + authentication_scheme, + mac_keys, digest_transactions_commitment, )?; Ok(shard) @@ -1280,11 +1385,14 @@ impl VerifiedBlock { } } - /// Verify digest, signature, includes, and threshold clock. + /// Verify content digest, authentication, includes, and threshold clock. fn verify_block_structure( &self, committee: &Committee, + own_id: usize, consensus_protocol: ConsensusProtocol, + authentication_scheme: BlockAuthenticationScheme, + mac_keys: &[MacKey], digest_transactions_commitment: Option, ) -> eyre::Result<()> { let round = self.round(); @@ -1313,7 +1421,6 @@ impl VerifiedBlock { &self.header.block_references, &acknowledgments, self.header.meta_creation_time_ns, - &self.header.signature, digest_transactions_commitment, self.header.strong_vote, self.header.unprovable_certificate.as_ref(), @@ -1324,17 +1431,57 @@ impl VerifiedBlock { digest, self.digest() ); - let pub_key = committee.get_public_key(self.authority()); - let Some(pub_key) = pub_key else { - bail!("Unknown block author {}", self.authority()) - }; if round == GENESIS_ROUND { bail!("Genesis block should not go through verification"); } - if let Err(e) = - pub_key.verify_signature_in_block(&self.header, digest_transactions_commitment) - { - bail!("Block signature verification has failed: {:?}", e); + match (authentication_scheme, &self.header.authentication) { + (BlockAuthenticationScheme::Ed25519, BlockAuthentication::Ed25519(signature)) => { + let Some(public_key) = committee.get_public_key(self.authority()) else { + bail!("Unknown block author {}", self.authority()) + }; + if let Err(error) = public_key.verify_digest_signature(digest.as_array(), signature) + { + bail!("Block Ed25519 verification has failed: {error:?}"); + } + } + (BlockAuthenticationScheme::MacVector, BlockAuthentication::MacVector(tags)) => { + ensure!( + tags.len() == committee.len(), + "MAC vector length {} does not match committee size {}", + tags.len(), + committee.len(), + ); + ensure!( + own_id < committee.len(), + "Own authority index is out of bounds" + ); + ensure!( + mac_keys.len() == committee.len(), + "MAC keyring length {} does not match committee size {}", + mac_keys.len(), + committee.len(), + ); + let author = self.authority() as usize; + let Some(key) = mac_keys.get(author) else { + bail!("Unknown block author {}", self.authority()) + }; + let expected = key.compute_tag(self.authority(), own_id as AuthorityIndex, &digest); + ensure!( + tags[own_id] == expected, + "Block MAC verification has failed" + ); + } + (BlockAuthenticationScheme::MlDsa44, BlockAuthentication::MlDsa44(signature)) => { + let Some(public_key) = committee.get_ml_dsa_44_public_key(self.authority()) else { + bail!("Unknown block author {}", self.authority()) + }; + if let Err(error) = public_key.verify_digest_signature(&digest, signature) { + bail!("Block ML-DSA-44 verification has failed: {error:?}"); + } + } + (expected, actual) => { + bail!("Expected {expected:?} block authentication, received {actual:?}") + } } for include in &self.header.block_references { ensure!( @@ -2147,6 +2294,191 @@ impl std::hash::Hash for VerifiedBlock { mod tests { use super::*; + fn make_authenticated_starfish_block( + committee: &Committee, + authorizer: &BlockAuthorizer<'_>, + ) -> VerifiedBlock { + let authority = 0; + let round = 1; + let transactions = Vec::new(); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + let encoded_transactions = encoder.encode_transactions( + &transactions, + committee.info_length(), + committee.len() - committee.info_length(), + ); + VerifiedBlock::new_with_authorizer_and_unprovable( + authority, + round, + committee + .authorities() + .map(|authority| BlockReference::new_test(authority, 0)) + .collect(), + None, + Vec::new(), + 0, + authorizer, + None, + None, + Vec::new(), + transactions, + Some(encoded_transactions), + ConsensusProtocol::Starfish, + None, + None, + None, + None, + None, + None, + None, + ) + } + + #[test] + fn block_reference_depends_only_on_content_across_authentication_schemes() { + let committee = Committee::new_for_benchmarks(4); + let ed_signers = Signer::new_for_test(committee.len()); + let ml_dsa_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let mac_keyrings = crypto::mac_keyrings_for_test(committee.len()); + let ed = BlockAuthorizer::Ed25519(&ed_signers[0]); + let mac = BlockAuthorizer::MacVector(&mac_keyrings[0]); + let ml_dsa = BlockAuthorizer::MlDsa44(&ml_dsa_signers[0]); + + let ed_block = make_authenticated_starfish_block(&committee, &ed); + let mac_block = make_authenticated_starfish_block(&committee, &mac); + let ml_dsa_block = make_authenticated_starfish_block(&committee, &ml_dsa); + + assert_eq!(ed_block.reference(), mac_block.reference()); + assert_eq!(ed_block.reference(), ml_dsa_block.reference()); + assert_ne!(ed_block.authentication(), mac_block.authentication()); + assert_ne!(ed_block.authentication(), ml_dsa_block.authentication()); + } + + #[test] + fn all_authentication_schemes_verify_for_starfish() { + let committee = Committee::new_for_benchmarks(4); + let ed_signers = Signer::new_for_test(committee.len()); + let ml_dsa_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let mac_keyrings = crypto::mac_keyrings_for_test(committee.len()); + + let cases = [ + ( + make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::Ed25519(&ed_signers[0]), + ), + BlockAuthenticationScheme::Ed25519, + ), + ( + make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&mac_keyrings[0]), + ), + BlockAuthenticationScheme::MacVector, + ), + ( + make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MlDsa44(&ml_dsa_signers[0]), + ), + BlockAuthenticationScheme::MlDsa44, + ), + ]; + + for (block, scheme) in cases { + for (receiver, receiver_keys) in mac_keyrings.iter().enumerate() { + let mut received = block.clone(); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + let mac_keys = if scheme == BlockAuthenticationScheme::MacVector { + receiver_keys.as_slice() + } else { + &[] + }; + received + .verify_with_authentication( + &committee, + receiver, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + scheme, + mac_keys, + ) + .unwrap(); + } + } + } + + #[test] + fn rejects_incomplete_mac_vector_and_wrong_authentication_scheme() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let mut mac_block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + let BlockAuthentication::MacVector(tags) = &mut mac_block.header.authentication else { + panic!("expected MAC vector") + }; + tags.pop(); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + assert!( + mac_block + .verify_with_authentication( + &committee, + 1, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[1], + ) + .is_err() + ); + + let mut wrong_tag_block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + let BlockAuthentication::MacVector(tags) = &mut wrong_tag_block.header.authentication + else { + panic!("expected MAC vector") + }; + tags.swap(1, 2); + assert!( + wrong_tag_block + .verify_with_authentication( + &committee, + 1, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[1], + ) + .is_err() + ); + + let ed_signers = Signer::new_for_test(committee.len()); + let mut ed_block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::Ed25519(&ed_signers[0]), + ); + assert!( + ed_block + .verify_with_authentication( + &committee, + 1, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MlDsa44, + &[], + ) + .is_err() + ); + } + fn single_signer_cert( digest: [u8; 32], signer: AuthorityIndex, @@ -2368,7 +2700,7 @@ mod tests { reference: BlockReference::new_test(0, 2), block_references: vec![a], meta_creation_time_ns: 0, - signature: SignatureBytes::default(), + authentication: BlockAuthentication::None, transactions_commitment: None, ack: Some(AckFields { intersection: None, diff --git a/crates/starfish-core/src/validator.rs b/crates/starfish-core/src/validator.rs index 32955dc3..05255f74 100644 --- a/crates/starfish-core/src/validator.rs +++ b/crates/starfish-core/src/validator.rs @@ -18,14 +18,14 @@ use crate::{ committee::Committee, config::{NodePrivateConfig, NodePublicConfig, Parameters}, core::Core, - dag_state::{ConsensusProtocol, DagState}, + dag_state::{DagState, ProtocolConfig}, metrics::{MetricReporter, Metrics}, net_sync::NetworkSyncer, network::Network, prometheus, runtime::{JoinError, JoinHandle}, transactions_generator::TransactionGenerator, - types::{AuthorityIndex, PartialSig}, + types::{AuthorityIndex, BlockAuthenticationScheme, PartialSig}, }; pub struct Validator { @@ -45,6 +45,35 @@ impl Validator { byzantine_strategy: String, consensus: String, ) -> Result { + let protocol_config = ProtocolConfig::from_str(&consensus).map_err(|error| eyre!(error))?; + match protocol_config.block_authentication_scheme { + BlockAuthenticationScheme::Ed25519 => { + if committee.get_public_key(authority) != Some(&private_config.keypair.public_key()) + { + return Err(eyre!( + "Ed25519 private key does not match committee authority {authority}" + )); + } + } + BlockAuthenticationScheme::MacVector => { + if private_config.mac_keys.len() != committee.len() { + return Err(eyre!( + "MAC keyring length {} does not match committee size {}", + private_config.mac_keys.len(), + committee.len(), + )); + } + } + BlockAuthenticationScheme::MlDsa44 => { + if committee.get_ml_dsa_44_public_key(authority) + != Some(&private_config.ml_dsa_44_keypair.public_key()) + { + return Err(eyre!( + "ML-DSA-44 private key does not match committee authority {authority}" + )); + } + } + } // Network and metrics setup remains the same let network_address = public_config .network_address(authority) @@ -76,7 +105,8 @@ impl Validator { .register(Box::new(pc)) .wrap_err("Failed to register ProcessCollector")?; } - let resolved_dissemination = ConsensusProtocol::from_str(&consensus) + let resolved_dissemination = protocol_config + .consensus_protocol .resolve_dissemination_mode(public_config.parameters.dissemination_mode); let dissemination_str = resolved_dissemination.to_string(); let (metrics, reporter) = Metrics::new( @@ -307,6 +337,8 @@ mod smoke_tests { #[test_case("mysticeti", 0)] #[test_case("cordial-miners", 40)] #[test_case("starfish", 60)] + #[test_case("starfish-mac", 700)] + #[test_case("starfish-ml-dsa-44", 720)] #[test_case("starfish-speed", 80)] #[test_case("starfish-bls", 100)] #[test_case("sailfish++", 120)] diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index 2c8e24e5..9c8c6954 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -65,6 +65,8 @@ enum Operation { parameters_path: String, #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, + /// Consensus/authentication variant (for example `starfish`, + /// `starfish-mac`, or `starfish-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, }, @@ -93,6 +95,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, + /// Consensus/authentication variant (for example `starfish`, + /// `starfish-mac`, or `starfish-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, /// Directory to store validator data (default: current directory) @@ -143,6 +147,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, + /// Consensus/authentication variant (for example `starfish`, + /// `starfish-mac`, or `starfish-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, #[clap(long, value_name = "INT", default_value_t = 600)] @@ -322,6 +328,7 @@ fn benchmark_genesis( Ok(()) } +#[allow(clippy::manual_is_multiple_of)] async fn local_benchmark( committee_size: usize, mut load: usize, @@ -422,7 +429,7 @@ async fn local_benchmark( )); } } - let is_byzantine = authority.is_multiple_of(3) && authority / 3 < num_byzantine_nodes; + let is_byzantine = authority % 3 == 0 && authority / 3 < num_byzantine_nodes; let validator = if is_byzantine { Validator::start( authority as AuthorityIndex, From 3b8e2269f63475d0fa62900a9214f8539808f72c Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:38:10 +0200 Subject: [PATCH 02/19] Flatten MAC vector wire encoding --- crates/starfish-core/src/crypto.rs | 6 ++ crates/starfish-core/src/types.rs | 105 ++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/crates/starfish-core/src/crypto.rs b/crates/starfish-core/src/crypto.rs index 2e97fc5d..47f55daf 100644 --- a/crates/starfish-core/src/crypto.rs +++ b/crates/starfish-core/src/crypto.rs @@ -558,6 +558,12 @@ impl AsBytes for MacTag { } } +impl MacTag { + pub(crate) fn from_bytes(bytes: [u8; MAC_TAG_SIZE]) -> Self { + Self(bytes) + } +} + impl PartialEq for MacTag { fn eq(&self, other: &Self) -> bool { blake3::Hash::from_bytes(self.0) == blake3::Hash::from_bytes(other.0) diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index cb861f00..53177a24 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -35,7 +35,7 @@ use ahash::AHashSet; use bytes::Bytes; use eyre::{bail, ensure}; use reed_solomon_simd::{ReedSolomonDecoder, ReedSolomonEncoder}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use crate::{ committee::Committee, @@ -258,10 +258,73 @@ pub enum BlockAuthentication { /// Only valid for locally constructed genesis blocks. None, Ed25519(SignatureBytes), - MacVector(Vec), + MacVector(#[serde(with = "flat_mac_vector")] Vec), MlDsa44(MlDsa44SignatureBytes), } +mod flat_mac_vector { + use super::*; + + pub fn serialize(tags: &[MacTag], serializer: S) -> Result { + if serializer.is_human_readable() { + return tags.serialize(serializer); + } + + let mut bytes = Vec::with_capacity(tags.len() * crypto::MAC_TAG_SIZE); + for tag in tags { + bytes.extend_from_slice(tag.as_ref()); + } + serializer.serialize_bytes(&bytes) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + if deserializer.is_human_readable() { + return Vec::::deserialize(deserializer); + } + + deserializer.deserialize_bytes(FlatMacVectorVisitor) + } + + struct FlatMacVectorVisitor; + + impl<'de> de::Visitor<'de> for FlatMacVectorVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "a flat byte string containing 32 bytes per MAC tag" + ) + } + + #[allow(clippy::manual_is_multiple_of)] + fn visit_bytes(self, bytes: &[u8]) -> Result { + if bytes.len() % crypto::MAC_TAG_SIZE != 0 { + return Err(E::custom(format!( + "invalid flat MAC vector length {}; expected a multiple of {}", + bytes.len(), + crypto::MAC_TAG_SIZE + ))); + } + + Ok(bytes + .chunks_exact(crypto::MAC_TAG_SIZE) + .map(|chunk| { + let mut tag = [0; crypto::MAC_TAG_SIZE]; + tag.copy_from_slice(chunk); + MacTag::from_bytes(tag) + }) + .collect()) + } + + fn visit_byte_buf(self, bytes: Vec) -> Result { + self.visit_bytes(&bytes) + } + } +} + #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)] pub enum BlockAuthenticationScheme { Ed25519, @@ -2409,6 +2472,44 @@ mod tests { } } + #[test] + fn mac_vector_uses_flat_binary_encoding() { + let committee = Committee::new_for_benchmarks(10); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + + let encoded = bincode::serialize(block.authentication()).unwrap(); + let expected_size = 4 + 8 + committee.len() * crypto::MAC_TAG_SIZE; + assert_eq!(encoded.len(), expected_size); + + let decoded: BlockAuthentication = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, *block.authentication()); + + let yaml = serde_yaml::to_string(block.authentication()).unwrap(); + let decoded_yaml: BlockAuthentication = serde_yaml::from_str(&yaml).unwrap(); + assert_eq!(decoded_yaml, *block.authentication()); + } + + #[test] + fn flat_mac_vector_rejects_partial_tags() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + + let mut encoded = bincode::serialize(block.authentication()).unwrap(); + let invalid_payload_len = committee.len() * crypto::MAC_TAG_SIZE - 1; + encoded[4..12].copy_from_slice(&(invalid_payload_len as u64).to_le_bytes()); + encoded.truncate(12 + invalid_payload_len); + + assert!(bincode::deserialize::(&encoded).is_err()); + } + #[test] fn rejects_incomplete_mac_vector_and_wrong_authentication_scheme() { let committee = Committee::new_for_benchmarks(4); From 380c3798b1998717aadc5cce54d9b29a99c0a624 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:53:55 +0200 Subject: [PATCH 03/19] Send recipient MACs on relay paths --- README.md | 11 ++- crates/starfish-core/src/broadcaster.rs | 98 ++++++++++++++++++++++- crates/starfish-core/src/types.rs | 100 +++++++++++++++++++++--- crates/starfish-core/src/validator.rs | 1 + 4 files changed, 191 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e4cce090..68f2f94e 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,13 @@ schemes: For all three variants, `BlockReference.digest` is the BLAKE3 hash of the canonical block content only. The authentication proof is a separate header -field and does not change the block reference. A `starfish-mac` block carries -exactly one tag for every committee member; each receiver verifies only its -own tag. Benchmark genesis deterministically generates the pairwise MAC keys, -ML-DSA seeds, and public keys in the node configuration. +field and does not change the block reference. A `starfish-mac` author sends +the full vector, with exactly one tag for every committee member, to its direct +recipients. A direct recipient retains that vector and, when relaying a header +or answering a missing-parent request, sends only the destination's tag. A +tag-only copy cannot be relayed a second time. Benchmark genesis +deterministically generates the pairwise MAC keys, ML-DSA seeds, and public +keys in the node configuration. This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be diff --git a/crates/starfish-core/src/broadcaster.rs b/crates/starfish-core/src/broadcaster.rs index d7770fc3..b5f7a200 100644 --- a/crates/starfish-core/src/broadcaster.rs +++ b/crates/starfish-core/src/broadcaster.rs @@ -26,11 +26,43 @@ use crate::{ runtime::{Handle, sleep}, syncer::CommitObserver, types::{ - AuthorityIndex, AuthoritySet, BlockReference, RoundNumber, VerifiedBlock, - format_authority_index, + AuthorityIndex, AuthoritySet, BlockAuthenticationScheme, BlockReference, RoundNumber, + VerifiedBlock, format_authority_index, }, }; +/// Prepare blocks sent through relay and missing-parent response paths for a +/// specific peer. In the MAC experiment, a direct recipient retains the +/// complete vector and selects only the destination's tag for these paths. A +/// tag-only copy cannot be relayed a second time and is therefore omitted. +fn prepare_relay_blocks_for_peer( + authentication_scheme: BlockAuthenticationScheme, + recipient: AuthorityIndex, + blocks: Vec>, +) -> Vec> { + if authentication_scheme != BlockAuthenticationScheme::MacVector { + return blocks; + } + + blocks + .into_iter() + .filter_map(|block| { + block + .with_recipient_mac(recipient) + .map(Data::new) + .or_else(|| { + tracing::debug!( + "Cannot relay MAC-authenticated block {} to authority {}: \ + complete MAC vector is unavailable", + block.reference(), + recipient, + ); + None + }) + }) + .collect() +} + fn peer_can_serve_missing_data( consensus_protocol: ConsensusProtocol, holders: &StakeAggregator, @@ -430,6 +462,11 @@ where .inner .dag_state .get_transmission_parts(&refs_to_send, &refs_to_send); + let headers = prepare_relay_blocks_for_peer( + self.inner.dag_state.block_authentication_scheme, + peer_id, + headers, + ); { let mut sent = self.sent_to_peer.write(); for block in headers.iter() { @@ -1308,6 +1345,7 @@ where fn materialize_push_batch( inner: &Arc>, + to_whom_authority_index: AuthorityIndex, plan: PushBatchParts, ) -> BlockBatch where @@ -1337,6 +1375,11 @@ where let (headers, shards) = inner .dag_state .get_transmission_parts(&plan.other_refs, &plan.shard_refs); + let headers = prepare_relay_blocks_for_peer( + inner.dag_state.block_authentication_scheme, + to_whom_authority_index, + headers, + ); BlockBatch { source: DataSource::BlockBundleStreaming, full_blocks: plan.own_blocks, @@ -1417,7 +1460,7 @@ where // Drop own blocks from the plan — already shipped in the fast batch. plan.own_blocks = Vec::new(); - let slow_batch = materialize_push_batch(&inner, plan); + let slow_batch = materialize_push_batch(&inner, to_whom_authority_index, plan); if slow_batch.is_empty() { return Some(()); } @@ -1526,7 +1569,11 @@ impl BlockFetcherWorker { #[cfg(test)] mod tests { use super::*; - use crate::committee::Committee; + use crate::{ + committee::Committee, + crypto::{SignatureBytes, mac_keyrings_for_test}, + types::{BaseTransaction, BlockAuthentication}, + }; fn holder_set(authorities: &[AuthorityIndex]) -> StakeAggregator { let committee = Committee::new_test(vec![1, 1, 1, 1]); @@ -1574,4 +1621,47 @@ mod tests { assert_eq!(ramp_up_chain_bomb_release_probability(180.0), 1.0); assert_eq!(ramp_up_chain_bomb_release_probability(240.0), 1.0); } + + #[test] + fn relay_preparation_selects_recipient_tag_and_stops_after_one_hop() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = mac_keyrings_for_test(committee.len()); + let mut block = VerifiedBlock::new( + 0, + 1, + Vec::new(), + Vec::new(), + 0, + SignatureBytes::default(), + Vec::::new(), + None, + None, + None, + None, + ); + let tags: Vec<_> = keyrings[0] + .iter() + .enumerate() + .map(|(recipient, key)| { + key.compute_tag(0, recipient as AuthorityIndex, &block.digest()) + }) + .collect(); + let expected = tags[2]; + block.header.authentication = BlockAuthentication::MacVector(tags); + + let relayed = prepare_relay_blocks_for_peer( + BlockAuthenticationScheme::MacVector, + 2, + vec![Data::new(block)], + ); + assert_eq!(relayed.len(), 1); + assert!(matches!( + relayed[0].authentication(), + BlockAuthentication::MacTag(tag) if *tag == expected + )); + + let second_hop = + prepare_relay_blocks_for_peer(BlockAuthenticationScheme::MacVector, 3, relayed); + assert!(second_hop.is_empty()); + } } diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 53177a24..48b8a039 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -258,7 +258,10 @@ pub enum BlockAuthentication { /// Only valid for locally constructed genesis blocks. None, Ed25519(SignatureBytes), + /// Complete author-generated authenticator retained by direct recipients. MacVector(#[serde(with = "flat_mac_vector")] Vec), + /// Recipient-specific authenticator selected from a full vector by a relay. + MacTag(MacTag), MlDsa44(MlDsa44SignatureBytes), } @@ -1297,6 +1300,21 @@ impl VerifiedBlock { } } + /// Clone a block for relaying to `recipient`, replacing its complete MAC + /// vector with only that recipient's tag. A block that was itself received + /// with a single tag cannot be relayed again. + pub fn with_recipient_mac(&self, recipient: AuthorityIndex) -> Option { + let BlockAuthentication::MacVector(tags) = &self.header.authentication else { + return None; + }; + let tag = *tags.get(recipient as usize)?; + + let mut block = self.clone(); + block.header.authentication = BlockAuthentication::MacTag(tag); + block.header.serialized = None; + Some(block) + } + // --- Decomposition --- /// Extract the header, consuming self. @@ -1507,13 +1525,23 @@ impl VerifiedBlock { bail!("Block Ed25519 verification has failed: {error:?}"); } } - (BlockAuthenticationScheme::MacVector, BlockAuthentication::MacVector(tags)) => { - ensure!( - tags.len() == committee.len(), - "MAC vector length {} does not match committee size {}", - tags.len(), - committee.len(), - ); + (BlockAuthenticationScheme::MacVector, authentication) => { + let tag = match authentication { + BlockAuthentication::MacVector(tags) => { + ensure!( + tags.len() == committee.len(), + "MAC vector length {} does not match committee size {}", + tags.len(), + committee.len(), + ); + tags.get(own_id) + .ok_or_else(|| eyre::eyre!("Own authority index is out of bounds"))? + } + BlockAuthentication::MacTag(tag) => tag, + actual => { + bail!("Expected MacVector block authentication, received {actual:?}") + } + }; ensure!( own_id < committee.len(), "Own authority index is out of bounds" @@ -1529,10 +1557,7 @@ impl VerifiedBlock { bail!("Unknown block author {}", self.authority()) }; let expected = key.compute_tag(self.authority(), own_id as AuthorityIndex, &digest); - ensure!( - tags[own_id] == expected, - "Block MAC verification has failed" - ); + ensure!(*tag == expected, "Block MAC verification has failed"); } (BlockAuthenticationScheme::MlDsa44, BlockAuthentication::MlDsa44(signature)) => { let Some(public_key) = committee.get_ml_dsa_44_public_key(self.authority()) else { @@ -2493,6 +2518,59 @@ mod tests { assert_eq!(decoded_yaml, *block.authentication()); } + #[test] + fn relay_selects_only_the_destination_mac() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + let BlockAuthentication::MacVector(full_vector) = block.authentication() else { + panic!("expected full MAC vector") + }; + + let mut relayed = block.with_recipient_mac(2).unwrap(); + let BlockAuthentication::MacTag(tag) = relayed.authentication() else { + panic!("expected recipient MAC tag") + }; + assert_eq!(*tag, full_vector[2]); + assert_eq!(relayed.reference(), block.reference()); + assert_eq!( + bincode::serialize(relayed.authentication()).unwrap().len(), + 4 + 8 + crypto::MAC_TAG_SIZE, + ); + + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + relayed + .verify_with_authentication( + &committee, + 2, + 1, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[2], + ) + .unwrap(); + + let mut wrong_recipient = block.with_recipient_mac(2).unwrap(); + assert!( + wrong_recipient + .verify_with_authentication( + &committee, + 1, + 2, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[1], + ) + .is_err() + ); + assert!(relayed.with_recipient_mac(3).is_none()); + } + #[test] fn flat_mac_vector_rejects_partial_tags() { let committee = Committee::new_for_benchmarks(4); diff --git a/crates/starfish-core/src/validator.rs b/crates/starfish-core/src/validator.rs index 05255f74..93b70276 100644 --- a/crates/starfish-core/src/validator.rs +++ b/crates/starfish-core/src/validator.rs @@ -439,6 +439,7 @@ mod smoke_tests { #[test_case("mysticeti", 100)] #[test_case("cordial-miners", 140)] #[test_case("starfish", 160)] + #[test_case("starfish-mac", 740)] #[test_case("starfish-speed", 180)] #[test_case("starfish-bls", 200)] #[test_case("sailfish++", 220)] From 8840625f43198166bd0be4910eb0587fd2d61221 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:30:40 +0200 Subject: [PATCH 04/19] Upgrade stored recipient MACs to full vectors --- README.md | 8 +- crates/starfish-core/src/block_manager.rs | 183 +++++++++++++++++++++- crates/starfish-core/src/dag_state.rs | 132 +++++++++++++++- crates/starfish-core/src/net_sync.rs | 131 ++++++++++++---- crates/starfish-core/src/types.rs | 68 ++++++++ 5 files changed, 479 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 68f2f94e..eea8e2bb 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,11 @@ field and does not change the block reference. A `starfish-mac` author sends the full vector, with exactly one tag for every committee member, to its direct recipients. A direct recipient retains that vector and, when relaying a header or answering a missing-parent request, sends only the destination's tag. A -tag-only copy cannot be relayed a second time. Benchmark genesis -deterministically generates the pairwise MAC keys, ML-DSA seeds, and public -keys in the node configuration. +tag-only copy cannot be relayed a second time. If the same node later receives +the author's full-vector copy, it upgrades the stored authentication without +adding a second DAG vertex and can then relay recipient-specific tags. +Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA +seeds, and public keys in the node configuration. This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be diff --git a/crates/starfish-core/src/block_manager.rs b/crates/starfish-core/src/block_manager.rs index f05e8d5f..4b34b864 100644 --- a/crates/starfish-core/src/block_manager.rs +++ b/crates/starfish-core/src/block_manager.rs @@ -56,6 +56,12 @@ impl BlockManager { let mut updated_existing_with_transactions: Vec> = vec![]; // Blocks to insert into the DAG in a single batched write lock. let mut blocks_to_insert: Vec> = vec![]; + // References first discovered in this batch are not visible through + // DagState until the final batched insert. Keep their positions so a + // richer duplicate later in the same batch can upgrade that pending + // insertion instead of being mistaken for an already stored block. + let mut new_blocks_in_batch: AHashMap = AHashMap::new(); + let mut updated_blocks_in_batch: AHashMap = AHashMap::new(); // missing references that we don't currently have let mut missing_references = AHashSet::new(); let mut block_exists_cache: AHashMap = AHashMap::new(); @@ -63,8 +69,33 @@ impl BlockManager { let block_reference = block.reference(); if let Some(existing_pending_block) = self.blocks_pending.get_mut(block_reference) { - if block.transactions().is_some() { - *existing_pending_block = block; + if let Some(mut merged) = existing_pending_block.merge_same_block(&block) { + merged.preserialize(); + *existing_pending_block = Data::new(merged); + } + continue; + } + + if let Some((insert_index, updated_index)) = + updated_blocks_in_batch.get(block_reference).copied() + { + if let Some(mut merged) = blocks_to_insert[insert_index].merge_same_block(&block) { + merged.preserialize(); + let merged = Data::new(merged); + blocks_to_insert[insert_index] = merged.clone(); + updated_existing_with_transactions[updated_index] = merged; + } + continue; + } + + if let Some((insert_index, processed_index)) = + new_blocks_in_batch.get(block_reference).copied() + { + if let Some(mut merged) = blocks_to_insert[insert_index].merge_same_block(&block) { + merged.preserialize(); + let merged = Data::new(merged); + blocks_to_insert[insert_index] = merged.clone(); + newly_processed[processed_index] = merged; } continue; } @@ -76,8 +107,24 @@ impl BlockManager { // Block already in store — check if this version brings new transaction data if self.dag_state.contains_new_transactions(&block) { tracing::debug!("Block has new transactions: {:?}", block_reference); + let stored_reference = *block_reference; + let mut merged = self + .dag_state + .get_storage_block(stored_reference) + .and_then(|existing| existing.merge_same_block(&block)); + let block = if let Some(ref mut merged) = merged { + merged.preserialize(); + Data::new(merged.clone()) + } else { + block + }; + let insert_index = blocks_to_insert.len(); blocks_to_insert.push(block.clone()); + let updated_index = updated_existing_with_transactions.len(); updated_existing_with_transactions.push(block); + updated_blocks_in_batch.insert(stored_reference, (insert_index, updated_index)); + } else { + self.dag_state.upgrade_mac_authentication(&block); } continue; } @@ -122,9 +169,12 @@ impl BlockManager { let block_reference = *block_reference; // Defer DAG insertion — will be done in batch after the loop. + let insert_index = blocks_to_insert.len(); blocks_to_insert.push(block.clone()); block_exists_cache.insert(block_reference, true); + let processed_index = newly_processed.len(); newly_processed.push(block); + new_blocks_in_batch.insert(block_reference, (insert_index, processed_index)); // Now unlock any pending blocks, and process them if ready. if let Some(waiting_references) = @@ -194,3 +244,132 @@ impl BlockManager { /// evicting unresolved chains from the block manager. pub fn cleanup(&mut self, _threshold_round: RoundNumber) {} } + +#[cfg(test)] +mod tests { + use prometheus::Registry; + use tempfile::TempDir; + + use super::*; + use crate::{ + config::{DisseminationMode, StorageBackend}, + crypto, + dag_state::ConsensusProtocol, + metrics::Metrics, + types::{AuthorityIndex, BlockAuthorizer}, + }; + + fn open_mac_dag_state(committee: Arc, path: &std::path::Path) -> DagState { + let registry = Registry::new(); + let (metrics, _reporter) = Metrics::new( + ®istry, + Some(committee.as_ref()), + Some("starfish-mac"), + None, + ); + DagState::open( + 0, + path, + metrics, + committee, + "honest".to_string(), + "starfish-mac".to_string(), + &StorageBackend::Rocksdb, + false, + DisseminationMode::ProtocolDefault, + ) + .dag_state + } + + fn make_mac_block( + keyrings: &[Vec], + authority: AuthorityIndex, + round: RoundNumber, + parents: Vec, + ) -> VerifiedBlock { + let mut block = VerifiedBlock::new_with_authorizer_and_unprovable( + authority, + round, + parents, + None, + Vec::new(), + 0, + &BlockAuthorizer::MacVector(&keyrings[authority as usize]), + None, + None, + Vec::new(), + Vec::new(), + None, + ConsensusProtocol::Starfish, + None, + None, + None, + None, + None, + None, + None, + ); + block.preserialize(); + block + } + + #[test] + fn block_manager_upgrades_stored_batched_and_pending_mac_copies() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let temp_dir = TempDir::new().unwrap(); + let dag_state = open_mac_dag_state(committee.clone(), temp_dir.path()); + let mut manager = BlockManager::new(dag_state.clone(), &committee); + let genesis: Vec<_> = committee + .authorities() + .map(|authority| BlockReference::new_test(authority, 0)) + .collect(); + + // A stored tag-only copy is upgraded when the author's full vector + // arrives later, without reporting another newly processed block. + let full = make_mac_block(&keyrings, 1, 1, genesis.clone()); + let reference = *full.reference(); + let mut tagged = full.with_recipient_mac(0).unwrap(); + tagged.preserialize(); + assert_eq!( + manager + .add_blocks(vec![Data::new(tagged)], DataSource::BlockBundleStreaming,) + .0 + .len(), + 1 + ); + assert!( + manager + .add_blocks(vec![Data::new(full)], DataSource::BlockBundleStreaming,) + .0 + .is_empty() + ); + assert!( + dag_state + .get_storage_block(reference) + .unwrap() + .has_full_mac_vector() + ); + + // The same upgrade also works when both copies share one receive + // batch and when the block is waiting on a missing parent. + let parent = make_mac_block(&keyrings, 2, 1, genesis); + let child = make_mac_block(&keyrings, 2, 2, vec![*parent.reference()]); + let child_reference = *child.reference(); + let mut tagged_child = child.with_recipient_mac(0).unwrap(); + tagged_child.preserialize(); + manager.add_blocks( + vec![Data::new(tagged_child), Data::new(child)], + DataSource::BlockBundleStreaming, + ); + assert_eq!(manager.pending_blocks_count(), 1); + manager.add_blocks(vec![Data::new(parent)], DataSource::BlockBundleStreaming); + assert_eq!(manager.pending_blocks_count(), 0); + assert!( + dag_state + .get_storage_block(child_reference) + .unwrap() + .has_full_mac_vector() + ); + } +} diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index 0c3e62b8..f3763881 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -32,8 +32,8 @@ use crate::{ store::Store, threshold_clock::ThresholdClockAggregator, types::{ - AuthorityIndex, AuthoritySet, BlockAuthenticationScheme, BlockDigest, BlockReference, - BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishNoVoteCert, + AuthorityIndex, AuthoritySet, BlockAuthentication, BlockAuthenticationScheme, BlockDigest, + BlockReference, BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishNoVoteCert, SailfishTimeoutCert, TransactionData, VerifiedBlock, }, }; @@ -1163,6 +1163,66 @@ impl DagState { self.dag_state_inner.read().get_storage_block(reference) } + /// Upgrade an already stored recipient-only MAC copy with a later verified + /// full-vector copy. This intentionally updates only the persisted header + /// and the matching in-memory value: the block is not re-added to the DAG, + /// so threshold clocks, votes, consensus notifications, and acceptance + /// metrics are left untouched. + pub(crate) fn upgrade_mac_authentication(&self, incoming: &VerifiedBlock) -> bool { + if !incoming.has_full_mac_vector() { + return false; + } + + let reference = *incoming.reference(); + let Some(existing) = self.get_storage_block(reference) else { + return false; + }; + if !matches!(existing.authentication(), BlockAuthentication::MacTag(_)) { + return false; + } + let Some(mut upgraded) = existing.merge_same_block(incoming) else { + return false; + }; + upgraded.preserialize(); + + let store_start = std::time::Instant::now(); + self.store + .store_header_bytes( + upgraded.reference(), + upgraded + .serialized_header_bytes() + .expect("upgraded header should be preserialized"), + ) + .expect("Failed to store upgraded MAC-vector header"); + self.metrics + .store_block_latency_us + .inc_by(store_start.elapsed().as_micros() as u64); + self.metrics.store_block_count.inc(); + + // Preserve any transaction data that may have arrived concurrently + // with the authentication upgrade. + let mut inner = self.dag_state_inner.write(); + let authority = reference.authority as usize; + let Some(blocks_at_round) = inner.index[authority].get_mut(&reference.round) else { + // The block was evicted; the persistent header update above is the + // authoritative copy and it should remain evicted from memory. + return true; + }; + let Some(current) = blocks_at_round.get_mut(&reference.digest) else { + return true; + }; + if matches!(current.authentication(), BlockAuthentication::MacTag(_)) { + let mut memory_upgrade = current + .merge_same_block(incoming) + .expect("tag-only copy should accept a full-vector upgrade"); + memory_upgrade.preserialize(); + *current = Data::new(memory_upgrade); + *inner.round_version.entry(reference.round).or_insert(0) += 1; + } + + true + } + /// Look up the `transactions_commitment` for a block in the DAG. pub fn get_transactions_commitment( &self, @@ -3431,15 +3491,16 @@ mod tests { committee::Committee, config::{DisseminationMode, StorageBackend}, crypto::{ - BLS_SIGNATURE_SIZE, BlockDigest, BlsSignatureBytes, SignatureBytes, + self, BLS_SIGNATURE_SIZE, BlockDigest, BlsSignatureBytes, SignatureBytes, TransactionsCommitment, }, data::Data, metrics::Metrics, types::{ - AuthorityIndex, AuthoritySet, BaseTransaction, BlockAuthenticationScheme, - BlockReference, BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishFields, - SailfishNoVoteCert, Transaction, VerifiedBlock, + AuthorityIndex, AuthoritySet, BaseTransaction, BlockAuthentication, + BlockAuthenticationScheme, BlockAuthorizer, BlockReference, BlsAggregateCertificate, + ProvableShard, RoundNumber, SailfishFields, SailfishNoVoteCert, Transaction, + VerifiedBlock, }, }; @@ -3696,6 +3757,65 @@ mod tests { ); } + #[test] + fn full_mac_vector_upgrades_tag_only_block_in_memory_and_storage() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let mut full = VerifiedBlock::new_with_authorizer_and_unprovable( + 1, + 1, + committee + .authorities() + .map(|authority| BlockReference::new_test(authority, 0)) + .collect(), + None, + Vec::new(), + 0, + &BlockAuthorizer::MacVector(&keyrings[1]), + None, + None, + Vec::new(), + Vec::new(), + None, + ConsensusProtocol::Starfish, + None, + None, + None, + None, + None, + None, + None, + ); + full.preserialize(); + let reference = *full.reference(); + + let mut tagged = full.with_recipient_mac(0).unwrap(); + tagged.preserialize(); + let dag_state = open_test_dag_state_for("starfish-mac", 0); + dag_state.insert_general_block(Data::new(tagged), DataSource::BlockBundleStreaming); + + assert!(matches!( + dag_state + .get_storage_block(reference) + .unwrap() + .authentication(), + BlockAuthentication::MacTag(_) + )); + assert!(matches!( + dag_state.get_blocks_by_round_cached(1)[0].authentication(), + BlockAuthentication::MacTag(_) + )); + assert!(dag_state.upgrade_mac_authentication(&full)); + + let upgraded = dag_state.get_storage_block(reference).unwrap(); + assert!(upgraded.has_full_mac_vector()); + assert!(upgraded.with_recipient_mac(2).is_some()); + assert!(dag_state.get_blocks_by_round_cached(1)[0].has_full_mac_vector()); + let persisted = dag_state.store.get_block(&reference).unwrap().unwrap(); + assert!(persisted.has_full_mac_vector()); + assert!(!dag_state.upgrade_mac_authentication(&full)); + } + #[test] fn batch_vertex_certification_waits_for_parent_closure() { let dag_state = open_test_dag_state_for("sailfish-pp", 0); diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index 3b95bf62..2ae7e943 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -161,6 +161,7 @@ fn eligible_missing_parent_refs( struct FilterForBlocks { digests: parking_lot::RwLock>, + full_mac_vectors: parking_lot::RwLock>, queue: parking_lot::RwLock>, } @@ -168,6 +169,7 @@ impl FilterForBlocks { fn new() -> Self { Self { digests: parking_lot::RwLock::new(AHashSet::new()), + full_mac_vectors: parking_lot::RwLock::new(AHashSet::new()), queue: parking_lot::RwLock::new(VecDeque::new()), } } @@ -177,58 +179,84 @@ impl FilterForBlocks { digests.iter().map(|d| set.contains(d)).collect() } - fn insert_batch(&self, new_digests: &[BlockDigest]) { + fn contains_full_mac_batch(&self, digests: &[BlockDigest]) -> Vec { + let set = self.full_mac_vectors.read(); + digests.iter().map(|d| set.contains(d)).collect() + } + + fn insert_batch(&self, blocks: &[(BlockDigest, bool)]) { let mut digests = self.digests.write(); + let mut full_mac_vectors = self.full_mac_vectors.write(); let mut queue = self.queue.write(); - for digest in new_digests { + for (digest, has_full_mac_vector) in blocks { if digests.insert(*digest) { queue.push_back(*digest); } + if *has_full_mac_vector { + full_mac_vectors.insert(*digest); + } } while queue.len() > MAX_FILTER_SIZE { if let Some(removed) = queue.pop_front() { digests.remove(&removed); + full_mac_vectors.remove(&removed); } } } - /// Inserts all digests and returns `true` for each that was genuinely new - /// (not already in the filter and not duplicated earlier in the batch). - fn insert_and_report_new(&self, digests: &[BlockDigest]) -> Vec { + /// Inserts all verified copies and returns `true` for each copy that adds + /// either a new block reference or the first full MAC vector for a + /// previously recipient-tag-only reference. + fn insert_and_report_useful(&self, blocks: &[(BlockDigest, bool)]) -> Vec { let mut set = self.digests.write(); + let mut full_mac_vectors = self.full_mac_vectors.write(); let mut queue = self.queue.write(); - let is_new: Vec = digests + let is_useful: Vec = blocks .iter() - .map(|d| { - if set.insert(*d) { - queue.push_back(*d); - true - } else { - false + .map(|(digest, has_full_mac_vector)| { + let is_new = set.insert(*digest); + if is_new { + queue.push_back(*digest); } + let is_mac_upgrade = *has_full_mac_vector && full_mac_vectors.insert(*digest); + is_new || is_mac_upgrade }) .collect(); while queue.len() > MAX_FILTER_SIZE { if let Some(removed) = queue.pop_front() { set.remove(&removed); + full_mac_vectors.remove(&removed); } } - is_new + is_useful } - /// For each header digest, returns `true` if the digest has not been seen - /// before (neither in the filter nor earlier in this batch). - fn needed_headers(&self, batch: &[BlockDigest]) -> Vec { + /// For each header, returns `true` if it is either unseen or upgrades a + /// previously seen recipient-only MAC to a full vector. + fn needed_headers(&self, batch: &[(BlockDigest, bool)]) -> Vec { let digests = self.digests.read(); - let mut seen_in_batch = AHashSet::with_capacity(batch.len()); + let full_mac_vectors = self.full_mac_vectors.read(); + let mut seen_in_batch = AHashMap::with_capacity(batch.len()); batch .iter() - .map(|digest| !digests.contains(digest) && seen_in_batch.insert(*digest)) + .map(|(digest, has_full_mac_vector)| { + let was_seen = digests.contains(digest) || seen_in_batch.contains_key(digest); + let had_full_mac_vector = seen_in_batch + .get(digest) + .copied() + .unwrap_or_else(|| full_mac_vectors.contains(digest)); + let is_needed = !was_seen || (*has_full_mac_vector && !had_full_mac_vector); + seen_in_batch + .entry(*digest) + .and_modify(|full| *full |= *has_full_mac_vector) + .or_insert(*has_full_mac_vector); + is_needed + }) .collect() } } @@ -424,8 +452,11 @@ fn spawn_header_worker( let mut encoder = ReedSolomonEncoder::new(2, 4, 2).expect("Encoder should be created"); while let Some((blocks, source)) = rx.recv().await { let connection_knowledge = inner.cordial_knowledge.connection_knowledge(peer_id); - let incoming_digests: Vec<_> = blocks.iter().map(|block| block.digest()).collect(); - let needed_before_verify = filter_for_blocks.needed_headers(&incoming_digests); + let incoming_headers: Vec<_> = blocks + .iter() + .map(|block| (block.digest(), block.has_full_mac_vector())) + .collect(); + let needed_before_verify = filter_for_blocks.needed_headers(&incoming_headers); let mut verified_blocks: Vec = Vec::new(); for (data_block, is_needed) in blocks.into_iter().zip(needed_before_verify) { @@ -466,11 +497,14 @@ fn spawn_header_worker( ck.mark_headers_useful_from_peer(&refs); } - let digests: Vec<_> = verified_blocks.iter().map(|b| b.digest()).collect(); - let is_new = filter_for_blocks.insert_and_report_new(&digests); + let filter_entries: Vec<_> = verified_blocks + .iter() + .map(|block| (block.digest(), block.has_full_mac_vector())) + .collect(); + let is_useful = filter_for_blocks.insert_and_report_useful(&filter_entries); let mut new_data_blocks = Vec::new(); - for (storage_block, is_new) in verified_blocks.into_iter().zip(is_new) { - if is_new { + for (storage_block, is_useful) in verified_blocks.into_iter().zip(is_useful) { + if is_useful { let mut storage_block = storage_block; storage_block.preserialize(); debug_assert!( @@ -962,16 +996,18 @@ impl ConnectionHandler)> = Vec::new(); - for ((data_block, _digest), (bk, sf)) in blocks - .into_iter() - .zip(incoming_digests) - .zip(block_known.into_iter().zip(shard_full)) - { - if bk && sf { + for (index, data_block) in blocks.into_iter().enumerate() { + let bk = block_known[index]; + let sf = shard_full[index]; + let incoming_has_full_mac = data_block.has_full_mac_vector(); + if bk && sf && (!incoming_has_full_mac || full_mac_known[index]) { self.metrics.filtered_blocks_total.inc(); continue; } @@ -1012,8 +1048,16 @@ impl ConnectionHandler = verified.iter().map(|(b, _)| b.digest()).collect(); - self.filter_for_blocks.insert_batch(&verified_digests); + let verified_filter_entries: Vec<_> = verified + .iter() + .map(|(block, _)| (block.digest(), block.has_full_mac_vector())) + .collect(); + let verified_digests: Vec<_> = verified_filter_entries + .iter() + .map(|(digest, _)| *digest) + .collect(); + self.filter_for_blocks + .insert_batch(&verified_filter_entries); self.filter_for_shards.mark_full_batch(&verified_digests); // --- preserialize + collect --- @@ -2388,6 +2432,29 @@ mod tests { wait.await; } + #[test] + fn block_filter_allows_exactly_one_tag_to_full_mac_upgrade() { + let filter = FilterForBlocks::new(); + let digest = BlockReference::new_test(1, 7).digest; + + assert_eq!( + filter.needed_headers(&[(digest, false), (digest, true), (digest, true)]), + vec![true, true, false] + ); + assert_eq!( + filter.insert_and_report_useful(&[(digest, false)]), + vec![true] + ); + assert_eq!(filter.needed_headers(&[(digest, false)]), vec![false]); + assert_eq!(filter.needed_headers(&[(digest, true)]), vec![true]); + assert_eq!( + filter.insert_and_report_useful(&[(digest, true), (digest, true)]), + vec![true, false] + ); + assert_eq!(filter.needed_headers(&[(digest, true)]), vec![false]); + assert_eq!(filter.contains_full_mac_batch(&[digest]), vec![true]); + } + #[test] fn acknowledgments_imply_peer_knows_shard_data() { let ack_ref = BlockReference::new_test(2, 3); diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 48b8a039..8f8830e1 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -1292,6 +1292,48 @@ impl VerifiedBlock { self.transaction_data.is_some() } + /// Returns whether this copy retains the author's complete MAC vector and + /// can therefore be specialized for another recipient. + pub fn has_full_mac_vector(&self) -> bool { + matches!( + &self.header.authentication, + BlockAuthentication::MacVector(_) + ) + } + + /// Merge two verified copies of the same content-addressed block, keeping + /// the richest independently transported components from either copy: + /// transaction data and the author's complete MAC vector. + /// + /// Returns `None` when the references differ or the merge adds nothing. + pub fn merge_same_block(&self, incoming: &Self) -> Option { + if self.reference() != incoming.reference() { + return None; + } + + let mut merged = self.clone(); + let mut changed = false; + + if merged.transaction_data.is_none() && incoming.transaction_data.is_some() { + merged.transaction_data = incoming.transaction_data.clone(); + changed = true; + } + + if matches!( + &merged.header.authentication, + BlockAuthentication::MacTag(_) + ) && matches!( + &incoming.header.authentication, + BlockAuthentication::MacVector(_) + ) { + merged.header.authentication = incoming.header.authentication.clone(); + merged.header.serialized = None; + changed = true; + } + + changed.then_some(merged) + } + /// Create a lightweight copy with only the header (no transaction data). pub fn as_header_only(&self) -> Self { Self { @@ -2571,6 +2613,32 @@ mod tests { assert!(relayed.with_recipient_mac(3).is_none()); } + #[test] + fn same_block_merge_keeps_full_mac_and_transaction_data_in_either_order() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let full = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + let mut tagged_with_transactions = full.with_recipient_mac(1).unwrap(); + tagged_with_transactions.transaction_data = + Some(TransactionData::new(vec![BaseTransaction::Share( + Transaction::new(vec![1, 2, 3]), + )])); + + let tag_then_full = tagged_with_transactions.merge_same_block(&full).unwrap(); + assert!(tag_then_full.has_full_mac_vector()); + assert!(tag_then_full.has_transaction_data()); + + let full_then_tag = full + .as_header_only() + .merge_same_block(&tagged_with_transactions) + .unwrap(); + assert!(full_then_tag.has_full_mac_vector()); + assert!(full_then_tag.has_transaction_data()); + } + #[test] fn flat_mac_vector_rejects_partial_tags() { let committee = Committee::new_for_benchmarks(4); From 2d4dff5892b8d92761f502226378510238a1f3ba Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:42:09 +0200 Subject: [PATCH 05/19] Restrict full MAC vectors to author streams --- README.md | 13 ++- crates/starfish-core/src/net_sync.rs | 168 ++++++++++++++++++++++++++- crates/starfish-core/src/types.rs | 97 +++++++++++++++- 3 files changed, 269 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eea8e2bb..fb89d150 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,14 @@ field and does not change the block reference. A `starfish-mac` author sends the full vector, with exactly one tag for every committee member, to its direct recipients. A direct recipient retains that vector and, when relaying a header or answering a missing-parent request, sends only the destination's tag. A -tag-only copy cannot be relayed a second time. If the same node later receives -the author's full-vector copy, it upgrades the stored authentication without -adding a second DAG vertex and can then relay recipient-specific tags. -Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA -seeds, and public keys in the node configuration. +tag-only copy cannot be relayed a second time. Receivers accept a full vector +only through proactive block streaming directly from the block's claimed +author; relay and synchronization traffic must contain exactly one recipient +tag. If the same node later receives the author's directly streamed +full-vector copy, it upgrades the stored authentication without adding a +second DAG vertex and can then relay recipient-specific tags. Benchmark +genesis deterministically generates the pairwise MAC keys, ML-DSA seeds, and +public keys in the node configuration. This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index 2ae7e943..5890fedf 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -46,8 +46,9 @@ use crate::{ shard_reconstructor::{DecodedBlocks, ShardMessage, start_shard_reconstructor}, syncer::{CommitObserver, Syncer, SyncerSignals}, types::{ - AuthorityIndex, AuthoritySet, BlockDigest, BlockReference, PartialSig, PartialSigKind, - ProvableShard, RoundNumber, VerifiedBlock, format_authority_index, + AuthorityIndex, AuthoritySet, BlockAuthentication, BlockAuthenticationScheme, BlockDigest, + BlockReference, PartialSig, PartialSigKind, ProvableShard, RoundNumber, VerifiedBlock, + format_authority_index, }, }; @@ -55,6 +56,48 @@ const MAX_FILTER_SIZE: usize = 100_000; const SAILFISH_CERT_BATCH_FLUSH_INTERVAL: Duration = Duration::from_millis(5); const SAILFISH_CERT_BATCH_MAX_LEN: usize = 256; +/// Enforce the MAC experiment's transport contract before cryptographic +/// verification: +/// +/// - a full vector is accepted only on proactive block streaming directly +/// from the block's claimed author; +/// - every relay and synchronization path must carry one recipient tag; +/// - a direct author stream must carry the full vector, so recipients retain +/// the material needed for one-hop relay. +fn verify_mac_transport( + block: &VerifiedBlock, + authentication_scheme: BlockAuthenticationScheme, + peer_id: AuthorityIndex, + source: DataSource, +) -> eyre::Result<()> { + if authentication_scheme != BlockAuthenticationScheme::MacVector { + return Ok(()); + } + + let direct_author_stream = peer_id == block.authority() + && matches!( + source, + DataSource::BlockBundleStreaming | DataSource::BlockBundleStreamingHeader + ); + + match block.authentication() { + BlockAuthentication::MacVector(_) if direct_author_stream => Ok(()), + BlockAuthentication::MacVector(_) => eyre::bail!( + "Full MAC vector for block {} must arrive via direct author block streaming; \ + received from authority {} with source {}", + block.reference(), + peer_id, + source, + ), + BlockAuthentication::MacTag(_) if !direct_author_stream => Ok(()), + BlockAuthentication::MacTag(_) => eyre::bail!( + "Direct author block stream for block {} must carry the full MAC vector", + block.reference(), + ), + _ => Ok(()), + } +} + async fn send_network_message_reliably( sender: &mpsc::Sender, message: NetworkMessage, @@ -466,6 +509,20 @@ fn spawn_header_worker( } let mut block: VerifiedBlock = (*data_block).clone(); tracing::debug!("Received {} from {}", block, peer); + if let Err(e) = verify_mac_transport( + &block, + inner.dag_state.block_authentication_scheme, + peer_id, + source, + ) { + tracing::warn!( + "Rejected incorrectly transported block {} from {}: {:?}", + block.reference(), + peer, + e + ); + break; + } match block.verify_with_authentication( &inner.committee, own_id as usize, @@ -1013,6 +1070,20 @@ impl ConnectionHandler, ) -> VerifiedBlock { - let authority = 0; + make_authenticated_starfish_block_for_author(committee, 0, authorizer) + } + + fn make_authenticated_starfish_block_for_author( + committee: &Committee, + authority: AuthorityIndex, + authorizer: &BlockAuthorizer<'_>, + ) -> VerifiedBlock { let round = 1; let transactions = Vec::new(); let mut encoder = Encoder::new(2, 4, 2).unwrap(); @@ -2613,6 +2620,94 @@ mod tests { assert!(relayed.with_recipient_mac(3).is_none()); } + #[test] + fn mac_verification_authenticates_the_claimed_block_author() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let mut correctly_authenticated = make_authenticated_starfish_block_for_author( + &committee, + 1, + &BlockAuthorizer::MacVector(&keyrings[1]), + ); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + correctly_authenticated + .verify_with_authentication( + &committee, + 2, + 1, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[2], + ) + .unwrap(); + + // The content claims authority 1, but authority 0's pairwise keys + // produced the vector. Recipient 2 must reject it when selecting the + // key associated with the claimed author. + let mut wrong_author_keys = make_authenticated_starfish_block_for_author( + &committee, + 1, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + assert!( + wrong_author_keys + .verify_with_authentication( + &committee, + 2, + 1, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[2], + ) + .is_err() + ); + } + + #[test] + fn mac_vector_verification_is_limited_to_the_receivers_own_tag() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let block = make_authenticated_starfish_block( + &committee, + &BlockAuthorizer::MacVector(&keyrings[0]), + ); + let mut tampered = block.clone(); + let BlockAuthentication::MacVector(tags) = &mut tampered.header.authentication else { + panic!("expected full MAC vector") + }; + tags[3] = MacTag::from_bytes([0; crypto::MAC_TAG_SIZE]); + + let mut receiver_one = tampered.clone(); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + receiver_one + .verify_with_authentication( + &committee, + 1, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[1], + ) + .unwrap(); + + assert!( + tampered + .verify_with_authentication( + &committee, + 3, + 0, + &mut encoder, + ConsensusProtocol::Starfish, + BlockAuthenticationScheme::MacVector, + &keyrings[3], + ) + .is_err() + ); + } + #[test] fn same_block_merge_keeps_full_mac_and_transaction_data_in_either_order() { let committee = Committee::new_for_benchmarks(4); From 92a3d859c10224bc9c8d94aa01d75701c4022719 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:52:11 +0200 Subject: [PATCH 06/19] Add Starfish Speed authentication variants --- README.md | 35 ++++++++++---------- crates/orchestrator/README.md | 2 +- crates/orchestrator/src/benchmark.rs | 1 + crates/orchestrator/src/main.rs | 3 +- crates/starfish-core/src/dag_state.rs | 30 +++++++++++++++++ crates/starfish-core/src/types.rs | 47 +++++++++++++++------------ crates/starfish-core/src/validator.rs | 4 +++ crates/starfish/src/main.rs | 12 +++---- 8 files changed, 87 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index fb89d150..b0b655c8 100644 --- a/README.md +++ b/README.md @@ -61,27 +61,26 @@ offloaded from the critical path. ### Starfish block authentication experiments -Plain Starfish can be run with three interchangeable block-authentication -schemes: +Plain Starfish and Starfish Speed can each be run with three interchangeable +block-authentication schemes: -| CLI name | Block authentication | -|---|---| -| `starfish` | Ed25519 signature | -| `starfish-mac` | Full vector of pairwise keyed-BLAKE3 MAC tags | -| `starfish-ml-dsa-44` | ML-DSA-44 signature | +| Protocol | Ed25519 | MAC vector | ML-DSA-44 | +|---|---|---|---| +| Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | +| Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | -For all three variants, `BlockReference.digest` is the BLAKE3 hash of the +For all six variants, `BlockReference.digest` is the BLAKE3 hash of the canonical block content only. The authentication proof is a separate header -field and does not change the block reference. A `starfish-mac` author sends -the full vector, with exactly one tag for every committee member, to its direct -recipients. A direct recipient retains that vector and, when relaying a header -or answering a missing-parent request, sends only the destination's tag. A -tag-only copy cannot be relayed a second time. Receivers accept a full vector -only through proactive block streaming directly from the block's claimed -author; relay and synchronization traffic must contain exactly one recipient -tag. If the same node later receives the author's directly streamed -full-vector copy, it upgrades the stored authentication without adding a -second DAG vertex and can then relay recipient-specific tags. Benchmark +field and does not change the block reference. An author using either MAC +variant sends the full vector, with exactly one tag for every committee member, +to its direct recipients. A direct recipient retains that vector and, when +relaying a header or answering a missing-parent request, sends only the +destination's tag. A tag-only copy cannot be relayed a second time. Receivers +accept a full vector only through proactive block streaming directly from the +block's claimed author; relay and synchronization traffic must contain exactly +one recipient tag. If the same node later receives the author's directly +streamed full-vector copy, it upgrades the stored authentication without adding +a second DAG vertex and can then relay recipient-specific tags. Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA seeds, and public keys in the node configuration. diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index ba884de6..db889f1f 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,7 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index 6965d4a3..dd5cfedd 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -56,6 +56,7 @@ pub struct BenchmarkParametersGeneric { pub use_internal_ip_address: bool, // Consensus protocol to deploy // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-speed | + // starfish-speed-mac | starfish-speed-ml-dsa-44 | // sparse-starfish-speed | starfish-bls | // mysticeti | mysticeti-bls | cordial-miners | bluestreak | sailfish-pp) pub consensus_protocol: String, diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index 545c63fb..c42b062c 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -136,7 +136,8 @@ pub enum Operation { /// Protocols to benchmark in order. Available options: /// starfish | starfish-mac | starfish-ml-dsa-44 | - /// starfish-speed | sparse-starfish-speed | + /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | + /// sparse-starfish-speed | /// starfish-bls | mysticeti | mysticeti-bls | /// cordial-miners | bluestreak | sailfish-pp #[clap( diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index f3763881..c7f16b0f 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -298,6 +298,14 @@ impl ProtocolConfig { ConsensusProtocol::Starfish, BlockAuthenticationScheme::MlDsa44, ), + "starfish-speed-mac" => ( + ConsensusProtocol::StarfishSpeed, + BlockAuthenticationScheme::MacVector, + ), + "starfish-speed-ml-dsa-44" => ( + ConsensusProtocol::StarfishSpeed, + BlockAuthenticationScheme::MlDsa44, + ), known => ( ConsensusProtocol::from_known_str(known) .ok_or_else(|| format!("Unknown consensus protocol '{known}'"))?, @@ -4931,6 +4939,28 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("starfish-speed").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::StarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::Ed25519, + } + ); + assert_eq!( + ProtocolConfig::from_str("starfish-speed-mac").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::StarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MacVector, + } + ); + assert_eq!( + ProtocolConfig::from_str("starfish-speed-ml-dsa-44").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::StarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, + } + ); assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); + assert!(ProtocolConfig::from_str("starfish-speed-unknown").is_err()); } } diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 23282285..0d43b2a4 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -2492,7 +2492,7 @@ mod tests { } #[test] - fn all_authentication_schemes_verify_for_starfish() { + fn all_authentication_schemes_verify_for_starfish_and_starfish_speed() { let committee = Committee::new_for_benchmarks(4); let ed_signers = Signer::new_for_test(committee.len()); let ml_dsa_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); @@ -2522,26 +2522,31 @@ mod tests { ), ]; - for (block, scheme) in cases { - for (receiver, receiver_keys) in mac_keyrings.iter().enumerate() { - let mut received = block.clone(); - let mut encoder = Encoder::new(2, 4, 2).unwrap(); - let mac_keys = if scheme == BlockAuthenticationScheme::MacVector { - receiver_keys.as_slice() - } else { - &[] - }; - received - .verify_with_authentication( - &committee, - receiver, - 0, - &mut encoder, - ConsensusProtocol::Starfish, - scheme, - mac_keys, - ) - .unwrap(); + for consensus_protocol in [ + ConsensusProtocol::Starfish, + ConsensusProtocol::StarfishSpeed, + ] { + for (block, scheme) in &cases { + for (receiver, receiver_keys) in mac_keyrings.iter().enumerate() { + let mut received = block.clone(); + let mut encoder = Encoder::new(2, 4, 2).unwrap(); + let mac_keys = if *scheme == BlockAuthenticationScheme::MacVector { + receiver_keys.as_slice() + } else { + &[] + }; + received + .verify_with_authentication( + &committee, + receiver, + 0, + &mut encoder, + consensus_protocol, + *scheme, + mac_keys, + ) + .unwrap(); + } } } } diff --git a/crates/starfish-core/src/validator.rs b/crates/starfish-core/src/validator.rs index 93b70276..7bd4aa92 100644 --- a/crates/starfish-core/src/validator.rs +++ b/crates/starfish-core/src/validator.rs @@ -340,6 +340,8 @@ mod smoke_tests { #[test_case("starfish-mac", 700)] #[test_case("starfish-ml-dsa-44", 720)] #[test_case("starfish-speed", 80)] + #[test_case("starfish-speed-mac", 760)] + #[test_case("starfish-speed-ml-dsa-44", 780)] #[test_case("starfish-bls", 100)] #[test_case("sailfish++", 120)] #[test_case("bluestreak", 140)] @@ -441,6 +443,8 @@ mod smoke_tests { #[test_case("starfish", 160)] #[test_case("starfish-mac", 740)] #[test_case("starfish-speed", 180)] + #[test_case("starfish-speed-mac", 800)] + #[test_case("starfish-speed-ml-dsa-44", 820)] #[test_case("starfish-bls", 200)] #[test_case("sailfish++", 220)] #[test_case("bluestreak", 260)] diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index 9c8c6954..abf422cb 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -65,8 +65,8 @@ enum Operation { parameters_path: String, #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, - /// Consensus/authentication variant (for example `starfish`, - /// `starfish-mac`, or `starfish-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac` or + /// `starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, }, @@ -95,8 +95,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish`, - /// `starfish-mac`, or `starfish-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac` or + /// `starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, /// Directory to store validator data (default: current directory) @@ -147,8 +147,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish`, - /// `starfish-mac`, or `starfish-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac` or + /// `starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, #[clap(long, value_name = "INT", default_value_t = 600)] From 8a3bdeddb3d882917c8242508c9703901db6e2eb Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:01:17 +0200 Subject: [PATCH 07/19] Fix local benchmark teardown and record auth comparison --- ...026-07-13-starfish-authentication-local.md | 84 +++++++++++++++++++ crates/starfish/src/main.rs | 30 +++---- 2 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 benchmark-results/2026-07-13-starfish-authentication-local.md diff --git a/benchmark-results/2026-07-13-starfish-authentication-local.md b/benchmark-results/2026-07-13-starfish-authentication-local.md new file mode 100644 index 00000000..2f8dbd88 --- /dev/null +++ b/benchmark-results/2026-07-13-starfish-authentication-local.md @@ -0,0 +1,84 @@ +# Starfish authentication comparison — local Apple Silicon + +Date: 2026-07-13 +Source revision: `92a3d85` plus the benchmark shutdown fix committed with this report +Host: Apple Silicon (`arm64`), macOS 15.7.4 +Build: Rust 1.86.0, release profile + +## Configuration + +- 10 honest validators in one local process +- 1,000 tx/s offered load (100 tx/s per validator) +- 20-second measurement window +- Uniform 0 ms added network latency +- Default protocol dissemination modes (`push-useful` for both families) +- One run per configuration + +Command template: + +```text +target/release/starfish local-benchmark \ + --committee-size 10 \ + --load 1000 \ + --consensus \ + --duration-secs 20 \ + --uniform-latency-ms 0 +``` + +## Results + +| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Starfish | Ed25519 | 12.1 | 13.9 | 446.50 | 4,573.30 | 14.33 | 14.30 | 65.74 | +| Starfish | MAC vector | 12.8 | 14.8 | 433.60 | 4,494.05 | 14.04 | 14.01 | 66.34 | +| Starfish | ML-DSA-44 | 15.7 | 18.0 | 432.50 | 4,125.75 | 57.11 | 57.08 | 270.44 | +| Starfish Speed | Ed25519 | 10.2 | 12.3 | 442.75 | 4,201.30 | 14.61 | 14.58 | 67.60 | +| Starfish Speed | MAC vector | 9.4 | 11.4 | 439.15 | 4,114.10 | 14.12 | 14.09 | 65.85 | +| Starfish Speed | ML-DSA-44 | 13.4 | 17.4 | 453.80 | 2,868.60 | 39.84 | 39.82 | 179.82 | + +## Relative to Ed25519 within each protocol + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish | MAC vector | +5.8% | +6.5% | -2.9% | -1.7% | -2.0% | +| Starfish | ML-DSA-44 | +29.8% | +29.5% | -3.1% | -9.8% | +298.5% | +| Starfish Speed | MAC vector | -7.8% | -7.3% | -0.8% | -2.1% | -3.4% | +| Starfish Speed | ML-DSA-44 | +31.4% | +41.5% | +2.5% | -31.7% | +172.7% | + +## Starfish Speed relative to Starfish + +| Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---:|---:|---:|---:|---:| +| Ed25519 | -15.7% | -11.5% | -0.8% | -8.1% | +2.0% | +| MAC vector | -26.6% | -23.0% | +1.3% | -8.5% | +0.6% | +| ML-DSA-44 | -14.6% | -3.3% | +4.9% | -30.5% | -30.2% | + +## Interpretation + +- The MAC variants remained close to Ed25519: TPS was within 3%, BPS within + 2.1%, and bandwidth was slightly lower in this sample. The latency changes + are small enough that repeated runs are needed before treating their sign as + meaningful. +- ML-DSA-44 materially increased latency and bandwidth. Its signature is 2,420 + bytes, versus 64 bytes for Ed25519. Outbound bandwidth increased about 4.0x + for Starfish and 2.7x for Starfish Speed. +- Starfish Speed with ML-DSA-44 produced 31.7% fewer blocks than its Ed25519 + variant while committing 2.5% more transactions. This indicates more + transactions per block in this run; it should not be read as evidence that + ML-DSA improves throughput without repeated trials. +- Against matching Starfish authentication variants, Starfish Speed had lower + latency in all three samples and essentially equal TPS for Ed25519 and MAC. + Its ML-DSA-44 run used about 30% less bandwidth, alongside about 30% fewer + blocks, than Starfish ML-DSA-44. +- All variants achieved roughly 433–454 TPS from the 1,000 tx/s offered load. + Ten validators share one laptop and therefore contend for the same CPU, + storage, and network stack. These results are useful for directional local + comparison, not distributed capacity claims. + +## Benchmark harness fix + +The previous local benchmark shutdown aborted validator tasks and immediately +deleted their RocksDB directories. On macOS this left benchmark parents stuck +in an uninterruptible exiting state. The harness now uses a `JoinSet`, aborts +all validator tasks, drains them completely, and only then removes storage. +The validation run and all six measured runs exited normally. diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index abf422cb..5e03bb0d 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -28,6 +28,7 @@ use starfish_core::{ types::AuthorityIndex, validator::Validator, }; +use tokio::task::JoinSet; use tokio::time::Instant; use tracing_subscriber::{EnvFilter, filter::LevelFilter, fmt}; @@ -386,8 +387,7 @@ async fn local_benchmark( let base_dir = PathBuf::from("local-benchmark"); fs::create_dir_all(&base_dir)?; - let mut handles = Vec::with_capacity(committee_size); - let mut abort_handles = Vec::with_capacity(committee_size); + let mut validator_tasks = JoinSet::new(); let mut metrics_of_honest_validators = Vec::new(); let mut reporters_of_honest_validators = Vec::new(); @@ -459,12 +459,10 @@ async fn local_benchmark( } // Use the same pattern as the run method - let handle = tokio::spawn(async move { + validator_tasks.spawn(async move { let (network_result, _metrics_result) = validator.await_completion().await; network_result }); - abort_handles.push(handle.abort_handle()); - handles.push(handle); } // Run for specified duration @@ -481,23 +479,25 @@ async fn local_benchmark( duration_secs, ); - // Abort all tasks - for abort_handle in abort_handles { - abort_handle.abort(); + // Abort and fully drain validator tasks before deleting their + // RocksDB directories. On macOS, removing storage while aborted + // tasks are still dropping database handles can leave the + // benchmark parent stuck in an uninterruptible exit state. + validator_tasks.abort_all(); + while validator_tasks.join_next().await.is_some() { } // Clean up fs::remove_dir_all(base_dir)?; Ok(()) } - _ = async { - for handle in handles { - if let Err(e) = handle.await { - tracing::warn!("Validator terminated with error: {}", e); - } + result = validator_tasks.join_next() => { + running.store(false, Ordering::SeqCst); + tracing::warn!("Validator terminated before benchmark timeout: {result:?}"); + validator_tasks.abort_all(); + while validator_tasks.join_next().await.is_some() { } - } => { - println!("All validators completed before timeout"); + println!("A validator completed before timeout"); Metrics::aggregate_and_display( metrics_of_honest_validators, reporters_of_honest_validators, From d3f57c2229721c2d45de44e55f20390df28dfbf7 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:17:29 +0200 Subject: [PATCH 08/19] Add Sparse Starfish authentication variants --- README.md | 11 ++-- ...026-07-13-starfish-authentication-local.md | 45 +++++++++++--- crates/orchestrator/README.md | 2 +- crates/orchestrator/src/benchmark.rs | 3 +- crates/orchestrator/src/main.rs | 3 +- crates/starfish-core/src/broadcaster.rs | 48 +++------------ crates/starfish-core/src/dag_state.rs | 30 ++++++++++ crates/starfish-core/src/net_sync.rs | 57 ++++++++++++++++++ crates/starfish-core/src/types.rs | 58 +++++++++++-------- crates/starfish-core/src/validator.rs | 4 ++ crates/starfish/src/main.rs | 6 +- 11 files changed, 182 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index b0b655c8..f683627b 100644 --- a/README.md +++ b/README.md @@ -61,18 +61,19 @@ offloaded from the critical path. ### Starfish block authentication experiments -Plain Starfish and Starfish Speed can each be run with three interchangeable -block-authentication schemes: +Starfish, Starfish Speed, and Sparse-Starfish-Speed can each be run with three +interchangeable block-authentication schemes: | Protocol | Ed25519 | MAC vector | ML-DSA-44 | |---|---|---|---| | Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | | Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | +| Sparse-Starfish-Speed | `sparse-starfish-speed` | `sparse-starfish-speed-mac` | `sparse-starfish-speed-ml-dsa-44` | -For all six variants, `BlockReference.digest` is the BLAKE3 hash of the +For all nine variants, `BlockReference.digest` is the BLAKE3 hash of the canonical block content only. The authentication proof is a separate header -field and does not change the block reference. An author using either MAC -variant sends the full vector, with exactly one tag for every committee member, +field and does not change the block reference. An author using a MAC variant +sends the full vector, with exactly one tag for every committee member, to its direct recipients. A direct recipient retains that vector and, when relaying a header or answering a missing-parent request, sends only the destination's tag. A tag-only copy cannot be relayed a second time. Receivers diff --git a/benchmark-results/2026-07-13-starfish-authentication-local.md b/benchmark-results/2026-07-13-starfish-authentication-local.md index 2f8dbd88..e51ad802 100644 --- a/benchmark-results/2026-07-13-starfish-authentication-local.md +++ b/benchmark-results/2026-07-13-starfish-authentication-local.md @@ -1,7 +1,7 @@ # Starfish authentication comparison — local Apple Silicon Date: 2026-07-13 -Source revision: `92a3d85` plus the benchmark shutdown fix committed with this report +Source revision: `8a3bded` plus the Sparse authentication changes committed with this report
Host: Apple Silicon (`arm64`), macOS 15.7.4 Build: Rust 1.86.0, release profile @@ -11,7 +11,7 @@ Build: Rust 1.86.0, release profile - 1,000 tx/s offered load (100 tx/s per validator) - 20-second measurement window - Uniform 0 ms added network latency -- Default protocol dissemination modes (`push-useful` for both families) +- Default protocol dissemination mode (`push-useful` for all three protocols) - One run per configuration Command template: @@ -35,6 +35,9 @@ target/release/starfish local-benchmark \ | Starfish Speed | Ed25519 | 10.2 | 12.3 | 442.75 | 4,201.30 | 14.61 | 14.58 | 67.60 | | Starfish Speed | MAC vector | 9.4 | 11.4 | 439.15 | 4,114.10 | 14.12 | 14.09 | 65.85 | | Starfish Speed | ML-DSA-44 | 13.4 | 17.4 | 453.80 | 2,868.60 | 39.84 | 39.82 | 179.82 | +| Sparse-Starfish-Speed | Ed25519 | 5.5 | 10.8 | 430.00 | 4,867.50 | 8.99 | 8.96 | 42.84 | +| Sparse-Starfish-Speed | MAC vector | 5.2 | 10.5 | 430.75 | 5,211.95 | 9.73 | 9.69 | 46.25 | +| Sparse-Starfish-Speed | ML-DSA-44 | 5.5 | 10.1 | 440.05 | 4,335.90 | 37.85 | 37.82 | 176.16 | ## Relative to Ed25519 within each protocol @@ -44,6 +47,8 @@ target/release/starfish local-benchmark \ | Starfish | ML-DSA-44 | +29.8% | +29.5% | -3.1% | -9.8% | +298.5% | | Starfish Speed | MAC vector | -7.8% | -7.3% | -0.8% | -2.1% | -3.4% | | Starfish Speed | ML-DSA-44 | +31.4% | +41.5% | +2.5% | -31.7% | +172.7% | +| Sparse-Starfish-Speed | MAC vector | -5.5% | -2.8% | +0.2% | +7.1% | +8.2% | +| Sparse-Starfish-Speed | ML-DSA-44 | 0.0% | -6.5% | +2.3% | -10.9% | +321.0% | ## Starfish Speed relative to Starfish @@ -53,15 +58,23 @@ target/release/starfish local-benchmark \ | MAC vector | -26.6% | -23.0% | +1.3% | -8.5% | +0.6% | | ML-DSA-44 | -14.6% | -3.3% | +4.9% | -30.5% | -30.2% | +## Sparse-Starfish-Speed relative to Starfish + +| Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---:|---:|---:|---:|---:| +| Ed25519 | -54.5% | -22.3% | -3.7% | +6.4% | -37.3% | +| MAC vector | -59.4% | -29.1% | -0.7% | +16.0% | -30.7% | +| ML-DSA-44 | -65.0% | -43.9% | +1.7% | +5.1% | -33.7% | + ## Interpretation -- The MAC variants remained close to Ed25519: TPS was within 3%, BPS within - 2.1%, and bandwidth was slightly lower in this sample. The latency changes - are small enough that repeated runs are needed before treating their sign as - meaningful. +- The Starfish and Starfish Speed MAC variants remained close to Ed25519: TPS + was within 3%, BPS within 2.1%, and bandwidth was slightly lower in this + sample. The latency changes are small enough that repeated runs are needed + before treating their sign as meaningful. - ML-DSA-44 materially increased latency and bandwidth. Its signature is 2,420 bytes, versus 64 bytes for Ed25519. Outbound bandwidth increased about 4.0x - for Starfish and 2.7x for Starfish Speed. + for Starfish, 2.7x for Starfish Speed, and 4.2x for Sparse-Starfish-Speed. - Starfish Speed with ML-DSA-44 produced 31.7% fewer blocks than its Ed25519 variant while committing 2.5% more transactions. This indicates more transactions per block in this run; it should not be read as evidence that @@ -70,7 +83,14 @@ target/release/starfish local-benchmark \ latency in all three samples and essentially equal TPS for Ed25519 and MAC. Its ML-DSA-44 run used about 30% less bandwidth, alongside about 30% fewer blocks, than Starfish ML-DSA-44. -- All variants achieved roughly 433–454 TPS from the 1,000 tx/s offered load. +- Sparse-Starfish-Speed preserved roughly the same TPS as plain Starfish while + reducing outbound bandwidth by 31-37% and block latency by 55-65% across the + three authentication schemes. Its lean headers therefore remain beneficial + with either signatures or MACs in this local sample. +- Sparse MAC remained close to Sparse Ed25519: TPS differed by 0.2%, while + outbound bandwidth was 8.2% higher. Sparse ML-DSA-44 used 4.2x the outbound + bandwidth of Sparse Ed25519 despite Sparse's lower protocol overhead. +- All variants achieved roughly 430–454 TPS from the 1,000 tx/s offered load. Ten validators share one laptop and therefore contend for the same CPU, storage, and network stack. These results are useful for directional local comparison, not distributed capacity claims. @@ -81,4 +101,11 @@ The previous local benchmark shutdown aborted validator tasks and immediately deleted their RocksDB directories. On macOS this left benchmark parents stuck in an uninterruptible exiting state. The harness now uses a `JoinSet`, aborts all validator tasks, drains them completely, and only then removes storage. -The validation run and all six measured runs exited normally. +The validation run and all nine measured protocol/authentication combinations +exited normally. + +During the first Sparse MAC run, the receiver-side transport guard exposed a +round-gap response that still carried full MAC vectors. The sender now routes +round-gap blocks through the same recipient-tag preparation used by relay and +missing-parent paths. The rejected run was discarded; the Sparse MAC row above +is the clean rerun, which emitted no transport rejections. diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index db889f1f..1ae7d267 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,7 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index dd5cfedd..374c0914 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -57,7 +57,8 @@ pub struct BenchmarkParametersGeneric { // Consensus protocol to deploy // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-speed | // starfish-speed-mac | starfish-speed-ml-dsa-44 | - // sparse-starfish-speed | starfish-bls | + // sparse-starfish-speed | sparse-starfish-speed-mac | + // sparse-starfish-speed-ml-dsa-44 | starfish-bls | // mysticeti | mysticeti-bls | cordial-miners | bluestreak | sailfish-pp) pub consensus_protocol: String, /// number Byzantine nodes diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index c42b062c..9f1bb685 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -137,7 +137,8 @@ pub enum Operation { /// Protocols to benchmark in order. Available options: /// starfish | starfish-mac | starfish-ml-dsa-44 | /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | - /// sparse-starfish-speed | + /// sparse-starfish-speed | sparse-starfish-speed-mac | + /// sparse-starfish-speed-ml-dsa-44 | /// starfish-bls | mysticeti | mysticeti-bls | /// cordial-miners | bluestreak | sailfish-pp #[clap( diff --git a/crates/starfish-core/src/broadcaster.rs b/crates/starfish-core/src/broadcaster.rs index b5f7a200..d1abc746 100644 --- a/crates/starfish-core/src/broadcaster.rs +++ b/crates/starfish-core/src/broadcaster.rs @@ -21,48 +21,16 @@ use crate::{ dag_state::{ByzantineStrategy, ConsensusProtocol, DataSource}, data::Data, metrics::{Metrics, UtilizationTimerVecExt}, - net_sync::NetworkSyncerInner, + net_sync::{NetworkSyncerInner, prepare_forwarded_blocks_for_peer}, network::{BlockBatch, NetworkMessage, ShardPayload}, runtime::{Handle, sleep}, syncer::CommitObserver, types::{ - AuthorityIndex, AuthoritySet, BlockAuthenticationScheme, BlockReference, RoundNumber, - VerifiedBlock, format_authority_index, + AuthorityIndex, AuthoritySet, BlockReference, RoundNumber, VerifiedBlock, + format_authority_index, }, }; -/// Prepare blocks sent through relay and missing-parent response paths for a -/// specific peer. In the MAC experiment, a direct recipient retains the -/// complete vector and selects only the destination's tag for these paths. A -/// tag-only copy cannot be relayed a second time and is therefore omitted. -fn prepare_relay_blocks_for_peer( - authentication_scheme: BlockAuthenticationScheme, - recipient: AuthorityIndex, - blocks: Vec>, -) -> Vec> { - if authentication_scheme != BlockAuthenticationScheme::MacVector { - return blocks; - } - - blocks - .into_iter() - .filter_map(|block| { - block - .with_recipient_mac(recipient) - .map(Data::new) - .or_else(|| { - tracing::debug!( - "Cannot relay MAC-authenticated block {} to authority {}: \ - complete MAC vector is unavailable", - block.reference(), - recipient, - ); - None - }) - }) - .collect() -} - fn peer_can_serve_missing_data( consensus_protocol: ConsensusProtocol, holders: &StakeAggregator, @@ -462,7 +430,7 @@ where .inner .dag_state .get_transmission_parts(&refs_to_send, &refs_to_send); - let headers = prepare_relay_blocks_for_peer( + let headers = prepare_forwarded_blocks_for_peer( self.inner.dag_state.block_authentication_scheme, peer_id, headers, @@ -1375,7 +1343,7 @@ where let (headers, shards) = inner .dag_state .get_transmission_parts(&plan.other_refs, &plan.shard_refs); - let headers = prepare_relay_blocks_for_peer( + let headers = prepare_forwarded_blocks_for_peer( inner.dag_state.block_authentication_scheme, to_whom_authority_index, headers, @@ -1572,7 +1540,7 @@ mod tests { use crate::{ committee::Committee, crypto::{SignatureBytes, mac_keyrings_for_test}, - types::{BaseTransaction, BlockAuthentication}, + types::{BaseTransaction, BlockAuthentication, BlockAuthenticationScheme}, }; fn holder_set(authorities: &[AuthorityIndex]) -> StakeAggregator { @@ -1649,7 +1617,7 @@ mod tests { let expected = tags[2]; block.header.authentication = BlockAuthentication::MacVector(tags); - let relayed = prepare_relay_blocks_for_peer( + let relayed = prepare_forwarded_blocks_for_peer( BlockAuthenticationScheme::MacVector, 2, vec![Data::new(block)], @@ -1661,7 +1629,7 @@ mod tests { )); let second_hop = - prepare_relay_blocks_for_peer(BlockAuthenticationScheme::MacVector, 3, relayed); + prepare_forwarded_blocks_for_peer(BlockAuthenticationScheme::MacVector, 3, relayed); assert!(second_hop.is_empty()); } } diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index c7f16b0f..3296eb88 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -306,6 +306,14 @@ impl ProtocolConfig { ConsensusProtocol::StarfishSpeed, BlockAuthenticationScheme::MlDsa44, ), + "sparse-starfish-speed-mac" => ( + ConsensusProtocol::SparseStarfishSpeed, + BlockAuthenticationScheme::MacVector, + ), + "sparse-starfish-speed-ml-dsa-44" => ( + ConsensusProtocol::SparseStarfishSpeed, + BlockAuthenticationScheme::MlDsa44, + ), known => ( ConsensusProtocol::from_known_str(known) .ok_or_else(|| format!("Unknown consensus protocol '{known}'"))?, @@ -4960,7 +4968,29 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("sparse-starfish-speed").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::Ed25519, + } + ); + assert_eq!( + ProtocolConfig::from_str("sparse-starfish-speed-mac").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MacVector, + } + ); + assert_eq!( + ProtocolConfig::from_str("sparse-starfish-speed-ml-dsa-44").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, + } + ); assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); assert!(ProtocolConfig::from_str("starfish-speed-unknown").is_err()); + assert!(ProtocolConfig::from_str("sparse-starfish-speed-unknown").is_err()); } } diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index 5890fedf..4c05f936 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -98,6 +98,38 @@ fn verify_mac_transport( } } +/// Prepare blocks forwarded through relay or synchronization paths for a +/// specific peer. MAC-authenticated blocks retain their complete vector only +/// at direct recipients; forwarding selects the destination's tag. A +/// tag-only copy cannot be forwarded again and is therefore omitted. +pub(crate) fn prepare_forwarded_blocks_for_peer( + authentication_scheme: BlockAuthenticationScheme, + recipient: AuthorityIndex, + blocks: Vec>, +) -> Vec> { + if authentication_scheme != BlockAuthenticationScheme::MacVector { + return blocks; + } + + blocks + .into_iter() + .filter_map(|block| { + block + .with_recipient_mac(recipient) + .map(Data::new) + .or_else(|| { + tracing::debug!( + "Cannot forward MAC-authenticated block {} to authority {}: \ + complete MAC vector is unavailable", + block.reference(), + recipient, + ); + None + }) + }) + .collect() +} + async fn send_network_message_reliably( sender: &mpsc::Sender, message: NetworkMessage, @@ -1400,6 +1432,11 @@ impl ConnectionHandler Date: Mon, 13 Jul 2026 14:31:24 +0200 Subject: [PATCH 09/19] Record one-minute geographic auth benchmarks --- ...6-07-13-starfish-authentication-geo-60s.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 benchmark-results/2026-07-13-starfish-authentication-geo-60s.md diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md new file mode 100644 index 00000000..a662738f --- /dev/null +++ b/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md @@ -0,0 +1,102 @@ +# Starfish authentication comparison — 60-second geographic emulation + +Date: 2026-07-13
+Source revision: `d3f57c2`
+Host: Apple Silicon (`arm64`), macOS 15.7.4
+Build: Rust 1.86.0, release profile + +## Configuration + +- 10 honest validators in one local process +- 1,000 tx/s offered load (100 tx/s per validator) +- 60-second measurement window +- Default `push-useful` dissemination for all three protocols +- One run per configuration +- Geographic latency emulation enabled; no uniform-latency override + +The ten validators map in order to `us-east-1`, `us-west-1`, +`ca-central-1`, `eu-west-1`, `eu-south-1`, `eu-north-1`, `sa-east-1`, +`ap-south-1`, `ap-southeast-1`, and `ap-northeast-1`. The harness converts +its AWS RTT table to one-way delay by dividing each cell by two, then applies +independent ±3% per-message jitter. The resulting base one-way delays range +from 0.5 ms within a region to 154.5 ms between the most distant pair. + +This is a single-machine latency emulation, not a deployment on ten remote +hosts. CPU, storage, and the physical network stack remain shared. + +Command template: + +```text +target/release/starfish local-benchmark \ + --committee-size 10 \ + --load 1000 \ + --consensus \ + --duration-secs 60 +``` + +## Results + +| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Starfish | Ed25519 | 553.4 | 603.7 | 816.25 | 100.05 | 0.49 | 0.49 | 1.23 | +| Starfish | MAC vector | 559.7 | 612.1 | 815.75 | 94.93 | 0.53 | 0.53 | 1.34 | +| Starfish | ML-DSA-44 | 554.1 | 604.2 | 814.70 | 97.00 | 0.76 | 0.76 | 1.92 | +| Starfish Speed | Ed25519 | 460.6 | 519.7 | 814.83 | 94.33 | 0.51 | 0.51 | 1.29 | +| Starfish Speed | MAC vector | 458.6 | 522.6 | 816.08 | 95.18 | 0.54 | 0.54 | 1.35 | +| Starfish Speed | ML-DSA-44 | 457.7 | 518.5 | 817.83 | 94.13 | 0.77 | 0.77 | 1.93 | +| Sparse-Starfish-Speed | Ed25519 | 417.6 | 485.1 | 818.25 | 95.42 | 0.45 | 0.45 | 1.13 | +| Sparse-Starfish-Speed | MAC vector | 418.7 | 483.5 | 816.25 | 92.70 | 0.47 | 0.47 | 1.18 | +| Sparse-Starfish-Speed | ML-DSA-44 | 420.3 | 486.9 | 817.95 | 92.85 | 0.71 | 0.71 | 1.78 | + +## Relative to Ed25519 within each protocol + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish | MAC vector | +1.1% | +1.4% | -0.1% | -5.1% | +8.2% | +| Starfish | ML-DSA-44 | +0.1% | +0.1% | -0.2% | -3.0% | +55.1% | +| Starfish Speed | MAC vector | -0.4% | +0.6% | +0.2% | +0.9% | +5.9% | +| Starfish Speed | ML-DSA-44 | -0.6% | -0.2% | +0.4% | -0.2% | +51.0% | +| Sparse-Starfish-Speed | MAC vector | +0.3% | -0.3% | -0.2% | -2.9% | +4.4% | +| Sparse-Starfish-Speed | ML-DSA-44 | +0.6% | +0.4% | 0.0% | -2.7% | +57.8% | + +## Protocol relative to matching Starfish authentication + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish Speed | Ed25519 | -16.8% | -13.9% | -0.2% | -5.7% | +4.1% | +| Starfish Speed | MAC vector | -18.1% | -14.6% | 0.0% | +0.3% | +1.9% | +| Starfish Speed | ML-DSA-44 | -17.4% | -14.2% | +0.4% | -3.0% | +1.3% | +| Sparse-Starfish-Speed | Ed25519 | -24.5% | -19.6% | +0.2% | -4.6% | -8.2% | +| Sparse-Starfish-Speed | MAC vector | -25.2% | -21.0% | +0.1% | -2.3% | -11.3% | +| Sparse-Starfish-Speed | ML-DSA-44 | -24.1% | -19.4% | +0.4% | -4.3% | -6.6% | + +## Interpretation + +- Authentication did not materially change throughput or latency under the + emulated wide-area delays. Within each protocol, TPS differed by at most + 0.4%, block latency by at most 1.1%, and end-to-end latency by at most 1.4%. + These small changes are below what should be interpreted without repeated + trials and variance estimates. +- MAC vectors increased outbound bandwidth by 4.4-8.2% relative to Ed25519, + while ML-DSA-44 increased it by 51.0-57.8%. At the geo-limited block rate, + payload and protocol traffic dominate more of the total than in the + zero-latency runs, so ML-DSA's relative bandwidth multiplier is smaller. +- Starfish Speed reduced block latency by 16.8-18.1% and end-to-end latency by + 13.9-14.6% against matching plain-Starfish authentication, with essentially + identical TPS. +- Sparse-Starfish-Speed reduced block latency by 24.1-25.2%, end-to-end + latency by 19.4-21.0%, and outbound bandwidth by 6.6-11.3% against matching + plain-Starfish authentication, again with essentially identical TPS. +- All variants committed roughly 815-818 TPS from the offered 1,000 tx/s. + This experiment measures a local machine under injected network delay; it + does not establish capacity on physically distributed hardware. + +## Caveats + +- There is one run per configuration and no warm-up exclusion, so these are + directional comparisons rather than confidence intervals. +- The reported metrics were emitted before shutdown. Validator task abortion + logs expected `JoinError::Cancelled` messages afterward. Two runs also + printed a macOS `pthread lock` teardown error after their metrics; process + and benchmark-directory checks showed no active or overlapping benchmark. + The shutdown path should still be hardened before unattended batch runs. From c90f8fb5dba2074239c2bbb8eda40b5e3a66c162 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:40:30 +0200 Subject: [PATCH 10/19] Record local validator scaling limits --- .../2026-07-13-validator-scaling-probe.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 benchmark-results/2026-07-13-validator-scaling-probe.md diff --git a/benchmark-results/2026-07-13-validator-scaling-probe.md b/benchmark-results/2026-07-13-validator-scaling-probe.md new file mode 100644 index 00000000..0f722eb4 --- /dev/null +++ b/benchmark-results/2026-07-13-validator-scaling-probe.md @@ -0,0 +1,63 @@ +# Local validator-count scaling probe + +Date: 2026-07-13
+Source revision: `dee67ee`
+Host: Apple Silicon (`arm64`), macOS 15.7.4, 64 GiB RAM, 16 logical CPU cores + +## Purpose + +Find a practical committee-size limit for geographic latency emulation on one +machine. Offered load stays at 100 tx/s per validator. The 20-80-validator +probes run for 30 seconds under the AWS RTT latency model. The 10-validator +row is the earlier 60-second baseline and is included only for orientation. + +## Sparse-Starfish-Speed MAC results + +| Validators | Offered load | Duration | Block latency (ms) | E2E latency (ms) | TPS | BPS | Outbound (MB/s) | Outcome | +|---:|---:|---:|---:|---:|---:|---:|---:|---| +| 10 | 1,000 | 60 s | 418.7 | 483.5 | 816.25 | 92.70 | 0.47 | Clean baseline | +| 20 | 2,000 | 30 s | 435.85 | 501.05 | 1,240.83 | 191.67 | 0.88 | Clean | +| 40 | 4,000 | 30 s | 461.45 | 545.75 | 2,336.33 | 364.13 | 1.96 | Clean | +| 64 | 6,400 | 30 s | 480.42 | 540.67 | 3,435.23 | 591.83 | 3.47 | Clean, near resource saturation | +| 80 | 8,000 | 30 s | 662.36 | 752.25 | 2,238.30 | 470.77 | 2.86 | Unhealthy: socket-buffer and decode errors | + +During the active 64-validator run, the process reached about 1,032% CPU +(roughly 10 cores) and 17.7 GiB resident memory. At 80 validators the network +failed to establish and sustain the full mesh reliably: the run emitted +deserialization warnings and repeated macOS `No buffer space available` +errors. Its throughput regression and latency jump therefore mark it as an +invalid benchmark configuration on this host. + +## Common-size check with plain Starfish MAC + +| Validators | Offered load | Duration | Block latency (ms) | E2E latency (ms) | TPS | BPS | Outbound (MB/s) | Cancelled reconstructions | +|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 40 | 4,000 | 30 s | 596.08 | 650.00 | 2,331.33 | 366.10 | 5.71 | 184 | + +Plain Starfish completed cleanly at 40 validators but used about 2.9x the +outbound bandwidth of Sparse-Starfish-Speed MAC at the same committee size. +It therefore provides the more conservative limit for a matrix that compares +all protocol families. + +## Limits and recommendation + +- The hard code limit is 512 validators (`MAX_COMMITTEE_SIZE`). This is a type + and data-structure bound, not a realistic single-machine target. +- The process file-descriptor limit is 1,048,575, and benchmark ports remain + well within `u16` even at 512 validators. Neither is the first constraint. +- The local network is a full mesh, so peer relationships grow as `n(n-1)`: + 1,560 at 40 validators, 4,032 at 64, and 6,320 at 80. Each validator also + owns a RocksDB instance. Socket buffers, connection tasks, and database + memory dominate before the hard committee or descriptor limits. +- Use 20 validators for quick, low-risk development comparisons. +- Use 40 validators as the recommended maximum for repeatable comparisons + across Starfish, Starfish Speed, Sparse, and all authentication schemes. +- Treat 64 as a Sparse-only local stress configuration, not a comfortable + full-matrix setting. +- Use multiple machines through the orchestrator beyond 40 validators. For + committees above ten, the local AWS table repeats the same ten regions while + all validators still share one kernel and physical host. + +The 30-second probes have different warm-up proportions from the 60-second +baseline, so their TPS values should not be used as a formal scaling curve. +The clean/error boundary and sampled resource usage are the relevant signals. From 72b43a237f42449f9165b0b36792544adf9b085d Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:04:49 +0200 Subject: [PATCH 11/19] Record 40-validator geographic auth benchmarks --- ...sh-authentication-geo-40-validators-60s.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md new file mode 100644 index 00000000..6ed5b47d --- /dev/null +++ b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md @@ -0,0 +1,126 @@ +# Starfish authentication comparison — 40-validator geographic emulation + +Date: 2026-07-13
+Source revision: `c90f8fb`
+Host: Apple Silicon (`arm64`), macOS 15.7.4
+Build: Rust 1.86.0, release profile + +## Configuration + +- 40 honest validators in one local process +- 1,000 tx/s aggregate offered load (25 tx/s per validator) +- 60-second measurement window +- Default `push-useful` dissemination for all three protocol families +- One run per configuration +- Geographic latency emulation enabled; no uniform-latency override + +The latency harness has ten AWS region profiles. At 40 validators, validator +indices are mapped modulo ten, producing four validators per modeled region. +The RTT values are divided by two to obtain one-way delays and independent +±3% per-message jitter is applied. Base one-way delays range from 0.5 ms to +154.5 ms. + +This is a single-machine latency emulation, not a deployment on 40 remote +hosts. All validators share the host's CPU, memory, storage, loopback network, +and kernel socket resources. + +In this report, **Sparse-Starfish-Speed means the sparse implementation of +Starfish Speed; it is not Bluestreak.** + +Command template: + +```text +target/release/starfish local-benchmark \ + --committee-size 40 \ + --load 1000 \ + --consensus \ + --duration-secs 60 +``` + +## Results + +| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Starfish | Ed25519 | 590.92 | 644.60 | 790.63 | 407.02 | 5.28 | 5.27 | 13.67 | +| Starfish | MAC vector | 592.98 | 646.42 | 792.77 | 393.48 | 5.63 | 5.63 | 14.54 | +| Starfish | ML-DSA-44 | 571.38 | 627.02 | 792.33 | 399.95 | 7.44 | 7.44 | 19.22 | +| Starfish Speed | Ed25519 | 712.02 | 843.95 | 794.25 | 338.55 | 4.39 | 4.39 | 11.32 | +| Starfish Speed | MAC vector | 679.67 | 788.67 | 794.10 | 344.63 | 4.68 | 4.67 | 12.06 | +| Starfish Speed | ML-DSA-44 | 648.98 | 733.05 | 794.35 | 354.83 | 6.54 | 6.53 | 16.85 | +| Sparse-Starfish-Speed | Ed25519 | 446.98 | 507.93 | 795.08 | 367.70 | 0.85 | 0.85 | 2.19 | +| Sparse-Starfish-Speed | MAC vector | 441.60 | 502.15 | 790.58 | 366.55 | 1.23 | 1.23 | 3.19 | +| Sparse-Starfish-Speed | ML-DSA-44 | 435.23 | 499.48 | 794.33 | 371.28 | 2.38 | 2.38 | 6.14 | + +All nine commands exited successfully after printing their metrics. No +deserialize, socket-buffer, or transport errors were observed during these +runs. + +## Relative to Ed25519 within each protocol + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish | MAC vector | +0.3% | +0.3% | +0.3% | -3.3% | +6.6% | +| Starfish | ML-DSA-44 | -3.3% | -2.7% | +0.2% | -1.7% | +40.9% | +| Starfish Speed | MAC vector | -4.5% | -6.6% | 0.0% | +1.8% | +6.6% | +| Starfish Speed | ML-DSA-44 | -8.9% | -13.1% | 0.0% | +4.8% | +49.0% | +| Sparse-Starfish-Speed | MAC vector | -1.2% | -1.1% | -0.6% | -0.3% | +44.7% | +| Sparse-Starfish-Speed | ML-DSA-44 | -2.6% | -1.7% | -0.1% | +1.0% | +180.0% | + +The lower latency values in some MAC and ML-DSA runs must not be interpreted +as an authentication speedup. These are single, sequential trials without a +randomized order or variance estimates. + +## Protocol relative to matching Starfish authentication + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish Speed | Ed25519 | +20.5% | +30.9% | +0.5% | -16.8% | -16.9% | +| Starfish Speed | MAC vector | +14.6% | +22.0% | +0.2% | -12.4% | -16.9% | +| Starfish Speed | ML-DSA-44 | +13.6% | +16.9% | +0.3% | -11.3% | -12.1% | +| Sparse-Starfish-Speed | Ed25519 | -24.4% | -21.2% | +0.6% | -9.7% | -83.9% | +| Sparse-Starfish-Speed | MAC vector | -25.5% | -22.3% | -0.3% | -6.8% | -78.2% | +| Sparse-Starfish-Speed | ML-DSA-44 | -23.8% | -20.3% | +0.3% | -7.2% | -68.0% | + +## Interpretation + +- The corrected workload is 1,000 tx/s total, not 4,000 tx/s. The harness + divides the total evenly, so every validator generates 25 tx/s. +- Authentication choice did not materially affect committed throughput. All + variants reported 790.58-795.08 TPS, a spread of 0.6% across the complete + matrix. +- The local harness includes the 12-second connection warm-up in its + 60-second TPS denominator: the default 10 seconds plus 2 seconds for a + 40-validator committee. Roughly 48 seconds therefore submit transactions; + the measured 790.58-795.08 TPS corresponds to about 988-994 tx/s during the + active submission window, close to the offered 1,000 tx/s. +- For plain Starfish and Starfish Speed, MAC raises outbound bandwidth by + 6.6% over Ed25519. ML-DSA-44 raises it by 40.9% and 49.0%, respectively. +- Sparse-Starfish-Speed removes so much baseline protocol traffic that + authentication bytes become a larger fraction of the remainder. Its MAC + variant rises from 0.85 to 1.23 MB/s (+44.7%), and ML-DSA-44 rises to + 2.38 MB/s (+180.0%). The absolute traffic remains below every matching + non-sparse variant. +- The MAC result is consistent with the current design: direct author block + streaming carries the full committee-sized MAC vector, while relay and + synchronization paths carry one recipient tag. Consequently, the remaining + author-stream authentication cost grows with committee size. +- Sparse-Starfish-Speed is the strongest 40-validator result in this local + emulation: 435-447 ms block latency, 499-508 ms end-to-end latency, and + 0.85-2.38 MB/s outbound across the three authentication schemes. +- Starfish Speed alone was slower than plain Starfish at 40 validators even + though it was faster in the earlier 10-validator experiment. This reversal + points to a single-host scaling or run-variance effect and needs randomized, + repeated trials before it is treated as a protocol conclusion. + +## Caveats + +- There is one run per configuration and no randomized run order, warm-up + exclusion, or confidence interval. Relative authentication bandwidth is the + clearest result; latency differences need repeated trials. +- Four validators share each synthetic region profile. This produces the AWS + delay distribution but does not model independent machines or real WAN + bandwidth constraints. +- The 40 validators form a full mesh of 1,560 directed peer relationships and + use one RocksDB instance each. Host contention is part of the measurement. +- The progress window starts before transaction generation, which explains + why displayed TPS is below the aggregate offered rate. From 35628902483713a5af565dae7d0c50d61b4570c6 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:12:40 +0200 Subject: [PATCH 12/19] Add Bluestreak authentication variants --- crates/starfish-core/src/broadcaster.rs | 22 ++++++++++++----- crates/starfish-core/src/dag_state.rs | 32 ++++++++++++++++++++++++- crates/starfish-core/src/net_sync.rs | 5 ++++ crates/starfish-core/src/validator.rs | 4 ++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/starfish-core/src/broadcaster.rs b/crates/starfish-core/src/broadcaster.rs index d1abc746..8db012cd 100644 --- a/crates/starfish-core/src/broadcaster.rs +++ b/crates/starfish-core/src/broadcaster.rs @@ -471,6 +471,11 @@ where .into_iter() .flatten() .collect(); + let all_blocks = prepare_forwarded_blocks_for_peer( + self.inner.dag_state.block_authentication_scheme, + peer_id, + all_blocks, + ); let chunk_size = batch_block_size.max(1); // MissingParentsRequest responses must serve the entire requested @@ -1323,13 +1328,18 @@ where match push_transport_format(inner.dag_state.consensus_protocol) { PushOtherBlocksFormat::FullBlocks => { let mut full_blocks = plan.own_blocks; - full_blocks.extend( - inner - .dag_state - .get_transmission_blocks(&plan.other_refs) - .into_iter() - .flatten(), + let other_blocks = inner + .dag_state + .get_transmission_blocks(&plan.other_refs) + .into_iter() + .flatten() + .collect(); + let other_blocks = prepare_forwarded_blocks_for_peer( + inner.dag_state.block_authentication_scheme, + to_whom_authority_index, + other_blocks, ); + full_blocks.extend(other_blocks); BlockBatch { source: DataSource::BlockBundleStreaming, full_blocks, diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index 3296eb88..62f380bb 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -314,6 +314,14 @@ impl ProtocolConfig { ConsensusProtocol::SparseStarfishSpeed, BlockAuthenticationScheme::MlDsa44, ), + "bluestreak-mac" => ( + ConsensusProtocol::Bluestreak, + BlockAuthenticationScheme::MacVector, + ), + "bluestreak-ml-dsa-44" => ( + ConsensusProtocol::Bluestreak, + BlockAuthenticationScheme::MlDsa44, + ), known => ( ConsensusProtocol::from_known_str(known) .ok_or_else(|| format!("Unknown consensus protocol '{known}'"))?, @@ -4925,7 +4933,7 @@ mod tests { } #[test] - fn protocol_config_selects_starfish_block_authentication() { + fn protocol_config_selects_block_authentication() { assert_eq!( ProtocolConfig::from_str("starfish").unwrap(), ProtocolConfig { @@ -4989,8 +4997,30 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("bluestreak").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Bluestreak, + block_authentication_scheme: BlockAuthenticationScheme::Ed25519, + } + ); + assert_eq!( + ProtocolConfig::from_str("bluestreak-mac").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Bluestreak, + block_authentication_scheme: BlockAuthenticationScheme::MacVector, + } + ); + assert_eq!( + ProtocolConfig::from_str("bluestreak-ml-dsa-44").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Bluestreak, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, + } + ); assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); assert!(ProtocolConfig::from_str("starfish-speed-unknown").is_err()); assert!(ProtocolConfig::from_str("sparse-starfish-speed-unknown").is_err()); + assert!(ProtocolConfig::from_str("bluestreak-unknown").is_err()); } } diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index 4c05f936..0b234c4c 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -1397,6 +1397,11 @@ impl ConnectionHandler Date: Tue, 14 Jul 2026 10:21:57 +0200 Subject: [PATCH 13/19] Document Bluestreak authentication benchmarks --- README.md | 32 +++++++++------- ...sh-authentication-geo-40-validators-60s.md | 38 ++++++++++++++----- crates/orchestrator/README.md | 2 +- crates/orchestrator/src/benchmark.rs | 5 ++- crates/orchestrator/src/main.rs | 3 +- crates/starfish/src/main.rs | 12 +++--- local-dryrun/README.md | 11 ++++-- local-dryrun/dryrun.sh | 7 ++-- 8 files changed, 71 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index f683627b..3f48750a 100644 --- a/README.md +++ b/README.md @@ -59,36 +59,42 @@ achieving 2-round optimistic commit latency. leader, data availability) in block headers, with async verification offloaded from the critical path. -### Starfish block authentication experiments +### Block authentication experiments -Starfish, Starfish Speed, and Sparse-Starfish-Speed can each be run with three -interchangeable block-authentication schemes: +Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak can each be run +with three interchangeable block-authentication schemes: | Protocol | Ed25519 | MAC vector | ML-DSA-44 | |---|---|---|---| | Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | | Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | | Sparse-Starfish-Speed | `sparse-starfish-speed` | `sparse-starfish-speed-mac` | `sparse-starfish-speed-ml-dsa-44` | +| Bluestreak | `bluestreak` | `bluestreak-mac` | `bluestreak-ml-dsa-44` | -For all nine variants, `BlockReference.digest` is the BLAKE3 hash of the +For all twelve variants, `BlockReference.digest` is the BLAKE3 hash of the canonical block content only. The authentication proof is a separate header field and does not change the block reference. An author using a MAC variant sends the full vector, with exactly one tag for every committee member, to its direct recipients. A direct recipient retains that vector and, when -relaying a header or answering a missing-parent request, sends only the -destination's tag. A tag-only copy cannot be relayed a second time. Receivers -accept a full vector only through proactive block streaming directly from the -block's claimed author; relay and synchronization traffic must contain exactly -one recipient tag. If the same node later receives the author's directly -streamed full-vector copy, it upgrades the stored authentication without adding -a second DAG vertex and can then relay recipient-specific tags. Benchmark -genesis deterministically generates the pairwise MAC keys, ML-DSA seeds, and -public keys in the node configuration. +relaying a block or header, or answering a synchronization request, sends only +the destination's tag. A tag-only copy cannot be relayed a second time. +Receivers accept a full vector only through proactive block streaming directly +from the block's claimed author; relay and synchronization traffic must contain +exactly one recipient tag. If the same node later receives the author's +directly streamed full-vector copy, it upgrades the stored authentication +without adding a second DAG vertex and can then relay recipient-specific tags. +Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA +seeds, and public keys in the node configuration. This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be treated as production-ready cryptography. +See the +[40-validator geographic authentication comparison](benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md) +for matching Ed25519, MAC-vector, and ML-DSA-44 measurements across all four +protocol families. + ## Dissemination Modes Every protocol can run with any of three dissemination strategies diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md index 6ed5b47d..697eb443 100644 --- a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md +++ b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md @@ -1,7 +1,7 @@ -# Starfish authentication comparison — 40-validator geographic emulation +# Starfish and Bluestreak authentication comparison — 40-validator geographic emulation -Date: 2026-07-13
-Source revision: `c90f8fb`
+Date: 2026-07-13–14
+Source revisions: `c90f8fb` (Starfish families), `3562890` (Bluestreak)
Host: Apple Silicon (`arm64`), macOS 15.7.4
Build: Rust 1.86.0, release profile @@ -10,7 +10,8 @@ Build: Rust 1.86.0, release profile - 40 honest validators in one local process - 1,000 tx/s aggregate offered load (25 tx/s per validator) - 60-second measurement window -- Default `push-useful` dissemination for all three protocol families +- Protocol-default dissemination: `push-useful` for the Starfish families and + `pull` for Bluestreak - One run per configuration - Geographic latency emulation enabled; no uniform-latency override @@ -50,8 +51,11 @@ target/release/starfish local-benchmark \ | Sparse-Starfish-Speed | Ed25519 | 446.98 | 507.93 | 795.08 | 367.70 | 0.85 | 0.85 | 2.19 | | Sparse-Starfish-Speed | MAC vector | 441.60 | 502.15 | 790.58 | 366.55 | 1.23 | 1.23 | 3.19 | | Sparse-Starfish-Speed | ML-DSA-44 | 435.23 | 499.48 | 794.33 | 371.28 | 2.38 | 2.38 | 6.14 | +| Bluestreak | Ed25519 | 417.65 | 477.93 | 793.48 | 389.53 | 0.59 | 0.59 | 1.53 | +| Bluestreak | MAC vector | 418.95 | 478.07 | 793.38 | 363.75 | 0.99 | 0.99 | 2.56 | +| Bluestreak | ML-DSA-44 | 420.70 | 478.43 | 795.43 | 368.57 | 1.41 | 1.41 | 3.62 | -All nine commands exited successfully after printing their metrics. No +All twelve commands exited successfully after printing their metrics. No deserialize, socket-buffer, or transport errors were observed during these runs. @@ -65,6 +69,8 @@ runs. | Starfish Speed | ML-DSA-44 | -8.9% | -13.1% | 0.0% | +4.8% | +49.0% | | Sparse-Starfish-Speed | MAC vector | -1.2% | -1.1% | -0.6% | -0.3% | +44.7% | | Sparse-Starfish-Speed | ML-DSA-44 | -2.6% | -1.7% | -0.1% | +1.0% | +180.0% | +| Bluestreak | MAC vector | +0.3% | 0.0% | 0.0% | -6.6% | +67.8% | +| Bluestreak | ML-DSA-44 | +0.7% | +0.1% | +0.2% | -5.4% | +139.0% | The lower latency values in some MAC and ML-DSA runs must not be interpreted as an authentication speedup. These are single, sequential trials without a @@ -80,21 +86,28 @@ randomized order or variance estimates. | Sparse-Starfish-Speed | Ed25519 | -24.4% | -21.2% | +0.6% | -9.7% | -83.9% | | Sparse-Starfish-Speed | MAC vector | -25.5% | -22.3% | -0.3% | -6.8% | -78.2% | | Sparse-Starfish-Speed | ML-DSA-44 | -23.8% | -20.3% | +0.3% | -7.2% | -68.0% | +| Bluestreak | Ed25519 | -29.3% | -25.9% | +0.4% | -4.3% | -88.8% | +| Bluestreak | MAC vector | -29.3% | -26.0% | +0.1% | -7.6% | -82.4% | +| Bluestreak | ML-DSA-44 | -26.4% | -23.7% | +0.4% | -7.8% | -81.0% | ## Interpretation - The corrected workload is 1,000 tx/s total, not 4,000 tx/s. The harness divides the total evenly, so every validator generates 25 tx/s. - Authentication choice did not materially affect committed throughput. All - variants reported 790.58-795.08 TPS, a spread of 0.6% across the complete + variants reported 790.58-795.43 TPS, a spread of 0.6% across the complete matrix. - The local harness includes the 12-second connection warm-up in its 60-second TPS denominator: the default 10 seconds plus 2 seconds for a 40-validator committee. Roughly 48 seconds therefore submit transactions; - the measured 790.58-795.08 TPS corresponds to about 988-994 tx/s during the + the measured 790.58-795.43 TPS corresponds to about 988-994 tx/s during the active submission window, close to the offered 1,000 tx/s. - For plain Starfish and Starfish Speed, MAC raises outbound bandwidth by 6.6% over Ed25519. ML-DSA-44 raises it by 40.9% and 49.0%, respectively. +- Bluestreak's baseline protocol traffic is even leaner. Its MAC vector raises + outbound bandwidth from 0.59 to 0.99 MB/s (+67.8%), while ML-DSA-44 raises + it to 1.41 MB/s (+139.0%). Latency and throughput remain within 0.7% of its + Ed25519 baseline. - Sparse-Starfish-Speed removes so much baseline protocol traffic that authentication bytes become a larger fraction of the remainder. Its MAC variant rises from 0.85 to 1.23 MB/s (+44.7%), and ML-DSA-44 rises to @@ -104,9 +117,12 @@ randomized order or variance estimates. streaming carries the full committee-sized MAC vector, while relay and synchronization paths carry one recipient tag. Consequently, the remaining author-stream authentication cost grows with committee size. -- Sparse-Starfish-Speed is the strongest 40-validator result in this local - emulation: 435-447 ms block latency, 499-508 ms end-to-end latency, and - 0.85-2.38 MB/s outbound across the three authentication schemes. +- Bluestreak is the strongest 40-validator result in this local emulation: + 418-421 ms block latency, about 478 ms end-to-end latency, and + 0.59-1.41 MB/s outbound across the three authentication schemes. Relative + to matching Sparse-Starfish-Speed authentication, it reduces block latency + by 3.3-6.6%, end-to-end latency by 4.2-5.9%, and outbound bandwidth by + 19.5-40.8%. - Starfish Speed alone was slower than plain Starfish at 40 validators even though it was faster in the earlier 10-validator experiment. This reversal points to a single-host scaling or run-variance effect and needs randomized, @@ -117,6 +133,8 @@ randomized order or variance estimates. - There is one run per configuration and no randomized run order, warm-up exclusion, or confidence interval. Relative authentication bandwidth is the clearest result; latency differences need repeated trials. +- The Bluestreak runs were made one day after the Starfish-family runs using + the same host and benchmark configuration but a newer source revision. - Four validators share each synthetic region profile. This produces the AWS delay distribution but does not model independent machines or real WAN bandwidth constraints. diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index 1ae7d267..02247b48 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,7 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `cordial-miners`, `sailfish-pp`. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index 374c0914..e9ebd8f0 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -58,8 +58,9 @@ pub struct BenchmarkParametersGeneric { // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-speed | // starfish-speed-mac | starfish-speed-ml-dsa-44 | // sparse-starfish-speed | sparse-starfish-speed-mac | - // sparse-starfish-speed-ml-dsa-44 | starfish-bls | - // mysticeti | mysticeti-bls | cordial-miners | bluestreak | sailfish-pp) + // sparse-starfish-speed-ml-dsa-44 | bluestreak | bluestreak-mac | + // bluestreak-ml-dsa-44 | starfish-bls | mysticeti | mysticeti-bls | + // cordial-miners | sailfish-pp) pub consensus_protocol: String, /// number Byzantine nodes pub byzantine_nodes: usize, diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index 9f1bb685..9e4811be 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -139,8 +139,9 @@ pub enum Operation { /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | /// sparse-starfish-speed | sparse-starfish-speed-mac | /// sparse-starfish-speed-ml-dsa-44 | + /// bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | /// starfish-bls | mysticeti | mysticeti-bls | - /// cordial-miners | bluestreak | sailfish-pp + /// cordial-miners | sailfish-pp #[clap( long, value_name = "STRING", diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index 0206a5ec..12b3cef9 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -66,8 +66,8 @@ enum Operation { parameters_path: String, #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, - /// Consensus/authentication variant (for example `starfish-mac` or - /// `sparse-starfish-speed-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac`, + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, }, @@ -96,8 +96,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish-mac` or - /// `sparse-starfish-speed-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac`, + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, /// Directory to store validator data (default: current directory) @@ -148,8 +148,8 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish-mac` or - /// `sparse-starfish-speed-ml-dsa-44`). + /// Consensus/authentication variant (for example `starfish-mac`, + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, #[clap(long, value_name = "INT", default_value_t = 600)] diff --git a/local-dryrun/README.md b/local-dryrun/README.md index 390d43cb..f2c54f69 100644 --- a/local-dryrun/README.md +++ b/local-dryrun/README.md @@ -32,9 +32,14 @@ NUM_NODES=10 DESIRED_TPS=1000 CONSENSUS=starfish \ | `CONSENSUS` | `bluestreak` | Consensus protocol (see below) | | `TEST_TIME` | `3000` | Experiment duration in seconds | -Supported `CONSENSUS` values: `starfish`, `starfish-speed`, -`sparse-starfish-speed`, `starfish-bls`, `cordial-miners`, -`mysticeti`, `sailfish-pp`, `bluestreak`, `mysticeti-bls`. +Supported `CONSENSUS` values include the Ed25519, MAC, and ML-DSA-44 variants +of Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak: +`starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, +`starfish-speed-mac`, `starfish-speed-ml-dsa-44`, +`sparse-starfish-speed`, `sparse-starfish-speed-mac`, +`sparse-starfish-speed-ml-dsa-44`, `bluestreak`, `bluestreak-mac`, +`bluestreak-ml-dsa-44`, `starfish-bls`, `cordial-miners`, `mysticeti`, +`sailfish-pp`, and `mysticeti-bls`. ### Protocol Tuning diff --git a/local-dryrun/dryrun.sh b/local-dryrun/dryrun.sh index b4ed06a7..37982a4c 100755 --- a/local-dryrun/dryrun.sh +++ b/local-dryrun/dryrun.sh @@ -6,9 +6,10 @@ NUM_NODES=${NUM_NODES:-10} NUM_CRASHED_NODES=${NUM_CRASHED_NODES:-0} DESIRED_TPS=${DESIRED_TPS:-100} -# Options: starfish, starfish-speed, sparse-starfish-speed, -# starfish-bls, cordial-miners, mysticeti, sailfish-pp, -# bluestreak, mysticeti-bls +# Authentication variants append -mac or -ml-dsa-44 to starfish, +# starfish-speed, sparse-starfish-speed, or bluestreak. +# Other options: starfish-bls, cordial-miners, mysticeti, +# sailfish-pp, mysticeti-bls CONSENSUS=${CONSENSUS:- sparse-starfish-speed} NUM_BYZANTINE_NODES=${NUM_BYZANTINE_NODES:-0} # Options: timeout-leader, leader-withholding, From 4b0f67d1094e73698c28577ddc487864a9a9ec22 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:56:58 +0200 Subject: [PATCH 14/19] Record 60-validator authentication benchmarks --- README.md | 4 +- ...14-authentication-geo-60-validators-60s.md | 138 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md diff --git a/README.md b/README.md index 3f48750a..572e495f 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,9 @@ treated as production-ready cryptography. See the [40-validator geographic authentication comparison](benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md) for matching Ed25519, MAC-vector, and ML-DSA-44 measurements across all four -protocol families. +protocol families. A separate +[60-validator moderate-load comparison](benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md) +records the single-machine scaling boundary at 600 tx/s aggregate load. ## Dissemination Modes diff --git a/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md b/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md new file mode 100644 index 00000000..3182c901 --- /dev/null +++ b/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md @@ -0,0 +1,138 @@ +# Authentication comparison — 60-validator geographic emulation + +Date: 2026-07-14
+Source revision: `4553e5e`
+Host: Apple Silicon (`arm64`), macOS 15.7.4
+Build: Rust 1.86.0, release profile + +## Configuration + +- 60 honest validators in one local process +- 600 tx/s aggregate offered load (exactly 10 tx/s per validator) +- 60-second measurement window +- Protocol-default dissemination: `push-useful` for the Starfish families and + `pull` for Bluestreak +- One run per configuration +- Geographic latency emulation enabled; no uniform-latency override + +The harness has ten AWS region profiles. Validator indices map to profiles +modulo ten, so this experiment models six validators per region. RTT values +are divided by two to obtain one-way delays and independent ±3% per-message +jitter is applied. Base one-way delays range from 0.5 ms to 154.5 ms. + +This is a single-machine latency emulation, not a 60-host deployment. The +validators share CPU, memory, storage, loopback networking, and kernel socket +resources. They form 3,540 directed peer relationships and use one RocksDB +instance each. + +Command template: + +```text +target/release/starfish local-benchmark \ + --committee-size 60 \ + --load 600 \ + --consensus \ + --duration-secs 60 +``` + +## Results + +| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Starfish | Ed25519 | 664.45 | 728.77 | 444.30 | 515.02 | 8.59 | 8.58 | 39.59 | +| Starfish | MAC vector | 680.68 | 753.42 | 414.87 | 449.23 | 8.35 | 8.34 | 41.21 | +| Starfish | ML-DSA-44 | 849.38 | 937.02 | 427.77 | 417.95 | 9.72 | 9.71 | 46.53 | +| Starfish Speed | Ed25519 | 592.13 | 673.87 | 460.37 | 467.12 | 6.63 | 6.62 | 29.49 | +| Starfish Speed | MAC vector | 670.80 | 783.93 | 430.95 | 439.30 | 7.30 | 7.30 | 34.70 | +| Starfish Speed | ML-DSA-44 | 641.70 | 726.03 | 433.63 | 461.57 | 11.06 | 11.05 | 52.22 | +| Sparse-Starfish-Speed | Ed25519 | 467.70 | 525.98 | 466.22 | 535.57 | 0.88 | 0.88 | 3.87 | +| Sparse-Starfish-Speed | MAC vector | 467.67 | 528.23 | 466.62 | 551.88 | 1.84 | 1.84 | 8.08 | +| Sparse-Starfish-Speed | ML-DSA-44 | 464.87 | 526.97 | 467.07 | 528.88 | 2.98 | 2.98 | 13.08 | +| Bluestreak | Ed25519 | 422.53 | 480.58 | 468.22 | 551.08 | 0.52 | 0.52 | 2.28 | +| Bluestreak | MAC vector | 421.62 | 480.37 | 467.63 | 545.72 | 1.47 | 1.46 | 6.42 | +| Bluestreak | ML-DSA-44 | 422.72 | 481.70 | 467.88 | 547.13 | 1.75 | 1.75 | 7.67 | + +All twelve commands exited successfully after emitting their metrics. No +authentication, deserialize, socket-buffer, or transport errors were observed. + +## Warm-up-adjusted offered-rate utilization + +The transaction generators wait 13 seconds before submitting: the default +10-second delay plus 3 seconds for a 60-validator committee. The displayed TPS +uses the complete 60-second window, leaving approximately 47 active submission +seconds. The following values estimate the active rate as `TPS × 60 / 47`. + +| Protocol | Authentication | Estimated active TPS | Offered rate sustained | +|---|---|---:|---:| +| Starfish | Ed25519 | 567.19 | 94.5% | +| Starfish | MAC vector | 529.62 | 88.3% | +| Starfish | ML-DSA-44 | 546.09 | 91.0% | +| Starfish Speed | Ed25519 | 587.71 | 98.0% | +| Starfish Speed | MAC vector | 550.15 | 91.7% | +| Starfish Speed | ML-DSA-44 | 553.57 | 92.3% | +| Sparse-Starfish-Speed | Ed25519 | 595.17 | 99.2% | +| Sparse-Starfish-Speed | MAC vector | 595.69 | 99.3% | +| Sparse-Starfish-Speed | ML-DSA-44 | 596.26 | 99.4% | +| Bluestreak | Ed25519 | 597.73 | 99.6% | +| Bluestreak | MAC vector | 596.97 | 99.5% | +| Bluestreak | ML-DSA-44 | 597.29 | 99.5% | + +## Relative to Ed25519 within each protocol + +| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | +|---|---|---:|---:|---:|---:|---:| +| Starfish | MAC vector | +2.4% | +3.4% | -6.6% | -12.8% | -2.8% | +| Starfish | ML-DSA-44 | +27.8% | +28.6% | -3.7% | -18.8% | +13.2% | +| Starfish Speed | MAC vector | +13.3% | +16.3% | -6.4% | -6.0% | +10.1% | +| Starfish Speed | ML-DSA-44 | +8.4% | +7.7% | -5.8% | -1.2% | +66.8% | +| Sparse-Starfish-Speed | MAC vector | 0.0% | +0.4% | +0.1% | +3.0% | +109.1% | +| Sparse-Starfish-Speed | ML-DSA-44 | -0.6% | +0.2% | +0.2% | -1.2% | +238.6% | +| Bluestreak | MAC vector | -0.2% | 0.0% | -0.1% | -1.0% | +182.7% | +| Bluestreak | ML-DSA-44 | 0.0% | +0.2% | -0.1% | -0.7% | +236.5% | + +Raw bandwidth can fall despite a larger authentication proof when a run +produces fewer blocks, as in plain Starfish MAC. The bandwidth-efficiency +metric—the ratio of bytes sent to committed transaction-payload bytes—rises +from 39.59 to 41.21 and captures the normalized increase. + +## Interpretation + +- Sixty validators are feasible on this machine at a 600 tx/s aggregate + offered load, but headroom depends strongly on the protocol family. +- Sparse-Starfish-Speed and Bluestreak sustain 99.2-99.6% of the active offered + rate for every authentication scheme. Their latency and throughput vary by + at most 0.6% and 0.2%, respectively, within each family. +- Plain Starfish sustains 88.3-94.5% of the active offered rate, while + Starfish Speed sustains 91.7-98.0%. Their authentication comparisons are + therefore partly measurements of shared-host contention. In particular, + plain Starfish ML-DSA-44 has 27.8% higher block latency than its Ed25519 run. +- Bluestreak has the lowest latency and absolute bandwidth across every + authentication scheme: 422-423 ms block latency, 480-482 ms end-to-end + latency, and 0.52-1.75 MB/s outbound. +- Relative to matching Sparse-Starfish-Speed authentication, Bluestreak lowers + block latency by 9.1-9.8%, end-to-end latency by 8.6-9.1%, and outbound + bandwidth by 20.1-41.3%, with nearly identical throughput. +- At 60 validators, a full author MAC vector contains 60 × 32 = 1,920 bytes, + compared with a 64-byte Ed25519 signature and a 2,420-byte ML-DSA-44 + signature. Relays and synchronization responses still carry only one + 32-byte recipient tag. Because Bluestreak and Sparse-Starfish-Speed remove + most other traffic, these authentication bytes produce large percentages + while their absolute bandwidth remains well below the denser protocols. +- For repeatable all-protocol authentication comparisons on this single host, + 40 validators remains the safer configuration. Sixty validators is a useful + stress configuration and a clean operating point for the sparse and + Bluestreak families; denser families should move to multiple machines for + stronger conclusions. + +## Caveats + +- There is one sequential run per configuration, with no randomized order or + confidence interval. Host scheduling and thermal state can affect results. +- The 600 tx/s load differs from the earlier 40-validator 1,000 tx/s matrix; + the two experiments should not be treated as a pure committee-size scaling + comparison. +- Six validators share each synthetic AWS region profile. The harness injects + the delay distribution but does not model independent machines or WAN + bandwidth constraints. +- Displayed TPS includes the 13-second startup delay. Warm-up-adjusted TPS is + derived rather than measured in a separately gated metrics window. From 3a82c045a6d7730097ba8ce70193835014381471 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:48:21 +0200 Subject: [PATCH 15/19] Add ML-DSA-65 authentication variants --- README.md | 20 +- crates/orchestrator/README.md | 2 +- crates/orchestrator/src/benchmark.rs | 10 +- crates/orchestrator/src/main.rs | 8 +- crates/starfish-core/src/committee.rs | 38 ++- crates/starfish-core/src/config.rs | 18 +- crates/starfish-core/src/core.rs | 7 +- crates/starfish-core/src/crypto.rs | 349 ++++++++++++++++---------- crates/starfish-core/src/dag_state.rs | 92 ++++--- crates/starfish-core/src/types.rs | 45 +++- crates/starfish-core/src/validator.rs | 18 ++ crates/starfish/src/main.rs | 6 +- local-dryrun/README.md | 12 +- local-dryrun/dryrun.sh | 2 +- 14 files changed, 410 insertions(+), 217 deletions(-) diff --git a/README.md b/README.md index 572e495f..8d844c37 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,16 @@ offloaded from the critical path. ### Block authentication experiments Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak can each be run -with three interchangeable block-authentication schemes: +with four interchangeable block-authentication schemes: -| Protocol | Ed25519 | MAC vector | ML-DSA-44 | -|---|---|---|---| -| Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | -| Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | -| Sparse-Starfish-Speed | `sparse-starfish-speed` | `sparse-starfish-speed-mac` | `sparse-starfish-speed-ml-dsa-44` | -| Bluestreak | `bluestreak` | `bluestreak-mac` | `bluestreak-ml-dsa-44` | +| Protocol | Ed25519 | MAC vector | ML-DSA-44 | ML-DSA-65 | +|---|---|---|---|---| +| Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | `starfish-ml-dsa-65` | +| Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | `starfish-speed-ml-dsa-65` | +| Sparse-Starfish-Speed | `sparse-starfish-speed` | `sparse-starfish-speed-mac` | `sparse-starfish-speed-ml-dsa-44` | `sparse-starfish-speed-ml-dsa-65` | +| Bluestreak | `bluestreak` | `bluestreak-mac` | `bluestreak-ml-dsa-44` | `bluestreak-ml-dsa-65` | -For all twelve variants, `BlockReference.digest` is the BLAKE3 hash of the +For all sixteen variants, `BlockReference.digest` is the BLAKE3 hash of the canonical block content only. The authentication proof is a separate header field and does not change the block reference. An author using a MAC variant sends the full vector, with exactly one tag for every committee member, @@ -86,6 +86,10 @@ without adding a second DAG vertex and can then relay recipient-specific tags. Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA seeds, and public keys in the node configuration. +The ML-DSA wrappers are generated from a common parameter-set definition. +ML-DSA-44 uses 1,312-byte public keys and 2,420-byte signatures; ML-DSA-65 +uses 1,952-byte public keys and 3,309-byte signatures. + This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be treated as production-ready cryptography. diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index 02247b48..2dbf4077 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,7 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-ml-dsa-65`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `starfish-speed-ml-dsa-65`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `sparse-starfish-speed-ml-dsa-65`, `bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, `bluestreak-ml-dsa-65`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `cordial-miners`, `sailfish-pp`. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index e9ebd8f0..68487b56 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -55,11 +55,13 @@ pub struct BenchmarkParametersGeneric { /// paying for data sent between the nodes. pub use_internal_ip_address: bool, // Consensus protocol to deploy - // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-speed | - // starfish-speed-mac | starfish-speed-ml-dsa-44 | + // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-ml-dsa-65 | + // starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | + // starfish-speed-ml-dsa-65 | // sparse-starfish-speed | sparse-starfish-speed-mac | - // sparse-starfish-speed-ml-dsa-44 | bluestreak | bluestreak-mac | - // bluestreak-ml-dsa-44 | starfish-bls | mysticeti | mysticeti-bls | + // sparse-starfish-speed-ml-dsa-44 | sparse-starfish-speed-ml-dsa-65 | + // bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | + // bluestreak-ml-dsa-65 | starfish-bls | mysticeti | mysticeti-bls | // cordial-miners | sailfish-pp) pub consensus_protocol: String, /// number Byzantine nodes diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index 9e4811be..fc2cf3f6 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -135,11 +135,11 @@ pub enum Operation { skip_testbed_configuration: bool, /// Protocols to benchmark in order. Available options: - /// starfish | starfish-mac | starfish-ml-dsa-44 | - /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | + /// starfish | starfish-mac | starfish-ml-dsa-44 | starfish-ml-dsa-65 | + /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | starfish-speed-ml-dsa-65 | /// sparse-starfish-speed | sparse-starfish-speed-mac | - /// sparse-starfish-speed-ml-dsa-44 | - /// bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | + /// sparse-starfish-speed-ml-dsa-44 | sparse-starfish-speed-ml-dsa-65 | + /// bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | bluestreak-ml-dsa-65 | /// starfish-bls | mysticeti | mysticeti-bls | /// cordial-miners | sailfish-pp #[clap( diff --git a/crates/starfish-core/src/committee.rs b/crates/starfish-core/src/committee.rs index d6515928..8518d758 100644 --- a/crates/starfish-core/src/committee.rs +++ b/crates/starfish-core/src/committee.rs @@ -11,8 +11,9 @@ use serde::{Deserialize, Serialize}; use crate::{ config::ImportExport, crypto::{ - BlsPublicKey, BlsSigner, MlDsa44PublicKey, MlDsa44Signer, PublicKey, Signer, - dummy_bls_public_key, dummy_ml_dsa_44_public_key, dummy_public_key, + BlsPublicKey, BlsSigner, MlDsa44PublicKey, MlDsa44Signer, MlDsa65PublicKey, MlDsa65Signer, + PublicKey, Signer, dummy_bls_public_key, dummy_ml_dsa_44_public_key, + dummy_ml_dsa_65_public_key, dummy_public_key, }, data::Data, types::{AuthorityIndex, AuthoritySet, RoundNumber, Stake, VerifiedBlock}, @@ -152,6 +153,12 @@ impl Committee { .map(Authority::ml_dsa_44_public_key) } + pub fn get_ml_dsa_65_public_key(&self, authority: AuthorityIndex) -> Option<&MlDsa65PublicKey> { + self.authorities + .get(authority as usize) + .map(Authority::ml_dsa_65_public_key) + } + pub fn known_authority(&self, authority: AuthorityIndex) -> bool { (authority as usize) < self.len() } @@ -215,18 +222,23 @@ impl Committee { pub fn new_for_benchmarks(committee_size: usize) -> Arc { let signers = Signer::new_for_test(committee_size); let bls_signers = BlsSigner::new_for_test(committee_size); - let ml_dsa_signers = MlDsa44Signer::new_for_test(committee_size); + let ml_dsa_44_signers = MlDsa44Signer::new_for_test(committee_size); + let ml_dsa_65_signers = MlDsa65Signer::new_for_test(committee_size); Self::new( signers .into_iter() .zip(bls_signers) - .zip(ml_dsa_signers) - .map(|((keypair, bls_keypair), ml_dsa_keypair)| Authority { - stake: 1, - public_key: keypair.public_key(), - bls_public_key: bls_keypair.public_key(), - ml_dsa_44_public_key: ml_dsa_keypair.public_key(), - }) + .zip(ml_dsa_44_signers) + .zip(ml_dsa_65_signers) + .map( + |(((keypair, bls_keypair), ml_dsa_44_keypair), ml_dsa_65_keypair)| Authority { + stake: 1, + public_key: keypair.public_key(), + bls_public_key: bls_keypair.public_key(), + ml_dsa_44_public_key: ml_dsa_44_keypair.public_key(), + ml_dsa_65_public_key: ml_dsa_65_keypair.public_key(), + }, + ) .collect(), ) } @@ -238,6 +250,7 @@ pub struct Authority { public_key: PublicKey, bls_public_key: BlsPublicKey, ml_dsa_44_public_key: MlDsa44PublicKey, + ml_dsa_65_public_key: MlDsa65PublicKey, } impl Authority { @@ -247,6 +260,7 @@ impl Authority { public_key: dummy_public_key(), bls_public_key: dummy_bls_public_key(), ml_dsa_44_public_key: dummy_ml_dsa_44_public_key(), + ml_dsa_65_public_key: dummy_ml_dsa_65_public_key(), } } @@ -265,6 +279,10 @@ impl Authority { pub fn ml_dsa_44_public_key(&self) -> &MlDsa44PublicKey { &self.ml_dsa_44_public_key } + + pub fn ml_dsa_65_public_key(&self) -> &MlDsa65PublicKey { + &self.ml_dsa_65_public_key + } } impl ImportExport for Committee {} diff --git a/crates/starfish-core/src/config.rs b/crates/starfish-core/src/config.rs index 69c0f52f..625d5f4b 100644 --- a/crates/starfish-core/src/config.rs +++ b/crates/starfish-core/src/config.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use crate::{ crypto::{ - BlsPublicKey, BlsSigner, MacKey, MlDsa44Signer, Signer, dummy_bls_signer, - dummy_ml_dsa_44_signer, dummy_signer, mac_keyrings_for_test, + BlsPublicKey, BlsSigner, MacKey, MlDsa44Signer, MlDsa65Signer, Signer, dummy_bls_signer, + dummy_ml_dsa_44_signer, dummy_ml_dsa_65_signer, dummy_signer, mac_keyrings_for_test, }, types::{AuthorityIndex, PublicKey, RoundNumber}, }; @@ -274,6 +274,7 @@ pub struct NodePrivateConfig { pub keypair: Signer, pub bls_keypair: BlsSigner, pub ml_dsa_44_keypair: MlDsa44Signer, + pub ml_dsa_65_keypair: MlDsa65Signer, pub mac_keys: Vec, pub storage_path: PathBuf, } @@ -285,6 +286,7 @@ impl NodePrivateConfig { keypair: dummy_signer(), bls_keypair: dummy_bls_signer(), ml_dsa_44_keypair: dummy_ml_dsa_44_signer(), + ml_dsa_65_keypair: dummy_ml_dsa_65_signer(), mac_keys: Vec::new(), storage_path: PathBuf::from("storage"), } @@ -293,16 +295,21 @@ impl NodePrivateConfig { pub fn new_for_benchmarks(working_dir: &Path, committee_size: usize) -> Vec { let signers = Signer::new_for_test(committee_size); let bls_signers = BlsSigner::new_for_test(committee_size); - let ml_dsa_signers = MlDsa44Signer::new_for_test(committee_size); + let ml_dsa_44_signers = MlDsa44Signer::new_for_test(committee_size); + let ml_dsa_65_signers = MlDsa65Signer::new_for_test(committee_size); let mac_keyrings = mac_keyrings_for_test(committee_size); signers .into_iter() .zip(bls_signers) - .zip(ml_dsa_signers) + .zip(ml_dsa_44_signers) + .zip(ml_dsa_65_signers) .zip(mac_keyrings) .enumerate() .map( - |(i, (((keypair, bls_keypair), ml_dsa_44_keypair), mac_keys))| { + |( + i, + ((((keypair, bls_keypair), ml_dsa_44_keypair), ml_dsa_65_keypair), mac_keys), + )| { let authority = i as AuthorityIndex; let path = working_dir.join(NodePrivateConfig::default_storage_path(authority)); Self { @@ -310,6 +317,7 @@ impl NodePrivateConfig { keypair, bls_keypair, ml_dsa_44_keypair, + ml_dsa_65_keypair, mac_keys, storage_path: path, } diff --git a/crates/starfish-core/src/core.rs b/crates/starfish-core/src/core.rs index 2f0d3eab..abfeeced 100644 --- a/crates/starfish-core/src/core.rs +++ b/crates/starfish-core/src/core.rs @@ -20,7 +20,9 @@ use crate::{ linearizer::CommittedSubDag, universal_committer::{UniversalCommitter, UniversalCommitterBuilder}, }, - crypto::{self, AsBytes, BlsSignatureBytes, BlsSigner, MacKey, MlDsa44Signer, Signer}, + crypto::{ + self, AsBytes, BlsSignatureBytes, BlsSigner, MacKey, MlDsa44Signer, MlDsa65Signer, Signer, + }, dag_state::{ ByzantineStrategy, CACHED_ROUNDS, CommitData, ConsensusProtocol, DagState, DataSource, OwnBlockData, @@ -62,6 +64,7 @@ pub struct Core { signer: Signer, bls_signer: BlsSigner, ml_dsa_44_signer: MlDsa44Signer, + ml_dsa_65_signer: MlDsa65Signer, mac_keys: Arc>, partial_sig_outbox: Option>, // todo - ugly, probably need to merge syncer and core @@ -189,6 +192,7 @@ impl Core { signer: private_config.keypair, bls_signer: private_config.bls_keypair, ml_dsa_44_signer: private_config.ml_dsa_44_keypair, + ml_dsa_65_signer: private_config.ml_dsa_65_keypair, mac_keys: Arc::new(private_config.mac_keys), partial_sig_outbox, recovered_committed_blocks: Some(committed_blocks), @@ -1004,6 +1008,7 @@ impl Core { BlockAuthenticationScheme::Ed25519 => BlockAuthorizer::Ed25519(&self.signer), BlockAuthenticationScheme::MacVector => BlockAuthorizer::MacVector(&self.mac_keys), BlockAuthenticationScheme::MlDsa44 => BlockAuthorizer::MlDsa44(&self.ml_dsa_44_signer), + BlockAuthenticationScheme::MlDsa65 => BlockAuthorizer::MlDsa65(&self.ml_dsa_65_signer), }; let mut block = VerifiedBlock::new_with_authorizer_and_unprovable( self.authority, diff --git a/crates/starfish-core/src/crypto.rs b/crates/starfish-core/src/crypto.rs index 47f55daf..ee74e5ee 100644 --- a/crates/starfish-core/src/crypto.rs +++ b/crates/starfish-core/src/crypto.rs @@ -6,7 +6,7 @@ use std::fmt; use blst::min_sig as bls; use ml_dsa::{ - Keypair as _, MlDsa44, Signature as MlDsaSignature, Signer as MlDsaSignerTrait, + Keypair as _, MlDsa44, MlDsa65, Signature as MlDsaSignature, Signer as MlDsaSignerTrait, SigningKey as MlDsaSigningKey, Verifier as MlDsaVerifierTrait, VerifyingKey as MlDsaVerifyingKey, }; @@ -79,9 +79,13 @@ pub const SIGNATURE_SIZE: usize = 64; pub const BLOCK_DIGEST_SIZE: usize = 32; pub const MAC_KEY_SIZE: usize = 32; pub const MAC_TAG_SIZE: usize = 32; -pub const ML_DSA_44_SEED_SIZE: usize = 32; +pub const ML_DSA_SEED_SIZE: usize = 32; +pub const ML_DSA_44_SEED_SIZE: usize = ML_DSA_SEED_SIZE; pub const ML_DSA_44_PUBLIC_KEY_SIZE: usize = 1_312; pub const ML_DSA_44_SIGNATURE_SIZE: usize = 2_420; +pub const ML_DSA_65_SEED_SIZE: usize = ML_DSA_SEED_SIZE; +pub const ML_DSA_65_PUBLIC_KEY_SIZE: usize = 1_952; +pub const ML_DSA_65_SIGNATURE_SIZE: usize = 3_309; pub const TRANSACTIONS_DIGEST_SIZE: usize = 32; @@ -104,16 +108,6 @@ pub struct MacKey([u8; MAC_KEY_SIZE]); #[derive(Clone, Copy, Ord, PartialOrd)] pub struct MacTag([u8; MAC_TAG_SIZE]); -#[derive(Clone, Eq, PartialEq)] -pub struct MlDsa44SignatureBytes(Box<[u8; ML_DSA_44_SIGNATURE_SIZE]>); - -#[derive(Clone)] -pub struct MlDsa44PublicKey(MlDsaVerifyingKey); - -/// Boxed so moving this wrapper does not copy private key material. -#[derive(Clone)] -pub struct MlDsa44Signer(Box>); - // Box ensures value is not copied in memory when Signer itself is moved around // for better security #[derive(Clone)] @@ -602,131 +596,199 @@ impl<'de> Deserialize<'de> for MacTag { } } -impl MlDsa44SignatureBytes { - pub fn from_bytes(bytes: [u8; ML_DSA_44_SIGNATURE_SIZE]) -> Self { - Self(Box::new(bytes)) - } -} - -impl AsRef<[u8]> for MlDsa44SignatureBytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} +macro_rules! define_ml_dsa_variant { + ( + parameter_set = $parameter_set:ty, + signature = $signature:ident, + public_key = $public_key:ident, + signer = $signer:ident, + seed_size = $seed_size:ident, + public_key_size = $public_key_size:ident, + signature_size = $signature_size:ident, + test_rng_seed = $test_rng_seed:expr, + label = $label:literal, + dummy_signer = $dummy_signer:ident, + dummy_public_key = $dummy_public_key:ident + ) => { + #[derive(Clone, Eq, PartialEq)] + pub struct $signature(Box<[u8; $signature_size]>); + + #[derive(Clone)] + pub struct $public_key(MlDsaVerifyingKey<$parameter_set>); + + /// Boxed so moving this wrapper does not copy private key material. + #[derive(Clone)] + pub struct $signer(Box>); + + impl $signature { + pub fn from_bytes(bytes: [u8; $signature_size]) -> Self { + Self(Box::new(bytes)) + } + } -impl fmt::Debug for MlDsa44SignatureBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MlDsa44Sig({})", &hex::encode(&self.0[..4])) - } -} + impl AsRef<[u8]> for $signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } + } -impl Serialize for MlDsa44SignatureBytes { - fn serialize(&self, serializer: S) -> Result { - serialize_fixed_bytes(self.0.as_ref(), serializer) - } -} + impl fmt::Debug for $signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}Sig({})", $label, &hex::encode(&self.0[..4])) + } + } -impl<'de> Deserialize<'de> for MlDsa44SignatureBytes { - fn deserialize>(deserializer: D) -> Result { - deserialize_fixed_bytes::(deserializer, "ML-DSA-44 signature") - .map(Self::from_bytes) - } -} + impl Serialize for $signature { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(self.0.as_ref(), serializer) + } + } -impl MlDsa44PublicKey { - pub fn from_bytes(bytes: &[u8; ML_DSA_44_PUBLIC_KEY_SIZE]) -> Self { - let encoded = ml_dsa::EncodedVerifyingKey::::from(*bytes); - Self(MlDsaVerifyingKey::decode(&encoded)) - } + impl<'de> Deserialize<'de> for $signature { + fn deserialize>(deserializer: D) -> Result { + deserialize_fixed_bytes::( + deserializer, + concat!($label, " signature"), + ) + .map(Self::from_bytes) + } + } - pub fn to_bytes(&self) -> [u8; ML_DSA_44_PUBLIC_KEY_SIZE] { - self.0.encode().into() - } + impl $public_key { + pub fn from_bytes(bytes: &[u8; $public_key_size]) -> Self { + let encoded = ml_dsa::EncodedVerifyingKey::<$parameter_set>::from(*bytes); + Self(MlDsaVerifyingKey::decode(&encoded)) + } + + pub fn to_bytes(&self) -> [u8; $public_key_size] { + self.0.encode().into() + } + + pub fn verify_digest_signature( + &self, + digest: &BlockDigest, + signature: &$signature, + ) -> Result<(), ml_dsa::signature::Error> { + let signature = MlDsaSignature::<$parameter_set>::try_from(signature.as_ref())?; + self.0.verify(digest.as_ref(), &signature) + } + } - pub fn verify_digest_signature( - &self, - digest: &BlockDigest, - signature: &MlDsa44SignatureBytes, - ) -> Result<(), ml_dsa::signature::Error> { - let signature = MlDsaSignature::::try_from(signature.as_ref())?; - self.0.verify(digest.as_ref(), &signature) - } -} + impl PartialEq for $public_key { + fn eq(&self, other: &Self) -> bool { + self.to_bytes() == other.to_bytes() + } + } -impl PartialEq for MlDsa44PublicKey { - fn eq(&self, other: &Self) -> bool { - self.to_bytes() == other.to_bytes() - } -} + impl Eq for $public_key {} -impl Eq for MlDsa44PublicKey {} + impl fmt::Debug for $public_key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}Pk({})", $label, &hex::encode(&self.to_bytes()[..4])) + } + } -impl fmt::Debug for MlDsa44PublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MlDsa44Pk({})", &hex::encode(&self.to_bytes()[..4])) - } -} + impl Serialize for $public_key { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(&self.to_bytes(), serializer) + } + } -impl Serialize for MlDsa44PublicKey { - fn serialize(&self, serializer: S) -> Result { - serialize_fixed_bytes(&self.to_bytes(), serializer) - } -} + impl<'de> Deserialize<'de> for $public_key { + fn deserialize>(deserializer: D) -> Result { + let bytes = deserialize_fixed_bytes::( + deserializer, + concat!($label, " public key"), + )?; + Ok(Self::from_bytes(&bytes)) + } + } -impl<'de> Deserialize<'de> for MlDsa44PublicKey { - fn deserialize>(deserializer: D) -> Result { - let bytes = deserialize_fixed_bytes::( - deserializer, - "ML-DSA-44 public key", - )?; - Ok(Self::from_bytes(&bytes)) - } -} + impl $signer { + pub fn new_for_test(n: usize) -> Vec { + let mut rng = StdRng::seed_from_u64($test_rng_seed); + (0..n) + .map(|_| { + let mut bytes = [0; $seed_size]; + rng.fill_bytes(&mut bytes); + let seed = ml_dsa::Seed::from(bytes); + Self(Box::new(MlDsaSigningKey::from_seed(&seed))) + }) + .collect() + } + + pub fn sign_digest(&self, digest: &BlockDigest) -> $signature { + let signature: MlDsaSignature<$parameter_set> = self.0.sign(digest.as_ref()); + $signature::from_bytes(signature.encode().into()) + } + + pub fn public_key(&self) -> $public_key { + $public_key(self.0.verifying_key()) + } + } -impl MlDsa44Signer { - pub fn new_for_test(n: usize) -> Vec { - let mut rng = StdRng::seed_from_u64(0x4d4c_4453_4134_3400); - (0..n) - .map(|_| { - let mut bytes = [0; ML_DSA_44_SEED_SIZE]; - rng.fill_bytes(&mut bytes); - let seed = ml_dsa::Seed::from(bytes); - Self(Box::new(MlDsaSigningKey::from_seed(&seed))) - }) - .collect() - } + impl fmt::Debug for $signer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}Signer(public_key={:?})", $label, self.public_key()) + } + } - pub fn sign_digest(&self, digest: &BlockDigest) -> MlDsa44SignatureBytes { - let signature: MlDsaSignature = self.0.sign(digest.as_ref()); - MlDsa44SignatureBytes::from_bytes(signature.encode().into()) - } + impl Serialize for $signer { + fn serialize(&self, serializer: S) -> Result { + let seed: [u8; $seed_size] = self.0.to_seed().into(); + serialize_fixed_bytes(&seed, serializer) + } + } - pub fn public_key(&self) -> MlDsa44PublicKey { - MlDsa44PublicKey(self.0.verifying_key()) - } -} + impl<'de> Deserialize<'de> for $signer { + fn deserialize>(deserializer: D) -> Result { + let bytes = deserialize_fixed_bytes::( + deserializer, + concat!($label, " seed"), + )?; + let seed = ml_dsa::Seed::from(bytes); + Ok(Self(Box::new(MlDsaSigningKey::from_seed(&seed)))) + } + } -impl fmt::Debug for MlDsa44Signer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MlDsa44Signer(public_key={:?})", self.public_key()) - } -} + pub fn $dummy_signer() -> $signer { + let seed = ml_dsa::Seed::from([0; $seed_size]); + $signer(Box::new(MlDsaSigningKey::from_seed(&seed))) + } -impl Serialize for MlDsa44Signer { - fn serialize(&self, serializer: S) -> Result { - let seed: [u8; ML_DSA_44_SEED_SIZE] = self.0.to_seed().into(); - serialize_fixed_bytes(&seed, serializer) - } + pub fn $dummy_public_key() -> $public_key { + $dummy_signer().public_key() + } + }; } -impl<'de> Deserialize<'de> for MlDsa44Signer { - fn deserialize>(deserializer: D) -> Result { - let bytes = - deserialize_fixed_bytes::(deserializer, "ML-DSA-44 seed")?; - let seed = ml_dsa::Seed::from(bytes); - Ok(Self(Box::new(MlDsaSigningKey::from_seed(&seed)))) - } -} +define_ml_dsa_variant!( + parameter_set = MlDsa44, + signature = MlDsa44SignatureBytes, + public_key = MlDsa44PublicKey, + signer = MlDsa44Signer, + seed_size = ML_DSA_44_SEED_SIZE, + public_key_size = ML_DSA_44_PUBLIC_KEY_SIZE, + signature_size = ML_DSA_44_SIGNATURE_SIZE, + test_rng_seed = 0x4d4c_4453_4134_3400, + label = "ML-DSA-44", + dummy_signer = dummy_ml_dsa_44_signer, + dummy_public_key = dummy_ml_dsa_44_public_key +); + +define_ml_dsa_variant!( + parameter_set = MlDsa65, + signature = MlDsa65SignatureBytes, + public_key = MlDsa65PublicKey, + signer = MlDsa65Signer, + seed_size = ML_DSA_65_SEED_SIZE, + public_key_size = ML_DSA_65_PUBLIC_KEY_SIZE, + signature_size = ML_DSA_65_SIGNATURE_SIZE, + test_rng_seed = 0x4d4c_4453_4136_3500, + label = "ML-DSA-65", + dummy_signer = dummy_ml_dsa_65_signer, + dummy_public_key = dummy_ml_dsa_65_public_key +); impl PublicKey { pub fn to_bytes(&self) -> [u8; 32] { @@ -923,15 +985,6 @@ pub fn dummy_public_key() -> PublicKey { dummy_signer().public_key() } -pub fn dummy_ml_dsa_44_signer() -> MlDsa44Signer { - let seed = ml_dsa::Seed::from([0; ML_DSA_44_SEED_SIZE]); - MlDsa44Signer(Box::new(MlDsaSigningKey::from_seed(&seed))) -} - -pub fn dummy_ml_dsa_44_public_key() -> MlDsa44PublicKey { - dummy_ml_dsa_44_signer().public_key() -} - // --------------------------------------------------------------------------- // BLS12-381 types (min_sig variant: 96-byte G2 public keys, 48-byte G1 // signatures). @@ -1278,6 +1331,33 @@ mod tests { assert_eq!(signature, decoded_signature); } + #[test] + fn ml_dsa_65_sign_verify_and_serde_roundtrip() { + let signer = MlDsa65Signer::new_for_test(1).pop().unwrap(); + let public_key = signer.public_key(); + let digest = BlockDigest([9; BLOCK_DIGEST_SIZE]); + let signature = signer.sign_digest(&digest); + + assert!( + public_key + .verify_digest_signature(&digest, &signature) + .is_ok() + ); + assert!( + public_key + .verify_digest_signature(&BlockDigest([8; BLOCK_DIGEST_SIZE]), &signature) + .is_err() + ); + + let encoded_key = bincode::serialize(&public_key).unwrap(); + let decoded_key: MlDsa65PublicKey = bincode::deserialize(&encoded_key).unwrap(); + let encoded_signature = bincode::serialize(&signature).unwrap(); + let decoded_signature: MlDsa65SignatureBytes = + bincode::deserialize(&encoded_signature).unwrap(); + assert_eq!(public_key, decoded_key); + assert_eq!(signature, decoded_signature); + } + #[test] fn bls_sign_verify_roundtrip() { let signers = BlsSigner::new_for_test(3); @@ -1342,6 +1422,9 @@ mod tests { ml_dsa_44_signer: MlDsa44Signer, ml_dsa_44_public_key: MlDsa44PublicKey, ml_dsa_44_signature: MlDsa44SignatureBytes, + ml_dsa_65_signer: MlDsa65Signer, + ml_dsa_65_public_key: MlDsa65PublicKey, + ml_dsa_65_signature: MlDsa65SignatureBytes, } #[test] @@ -1353,6 +1436,7 @@ mod tests { let block_digest = BlockDigest([7u8; BLOCK_DIGEST_SIZE]); let mac_tag = mac_key.compute_tag(0, 1, &block_digest); let ml_dsa_44_signer = dummy_ml_dsa_44_signer(); + let ml_dsa_65_signer = dummy_ml_dsa_65_signer(); let fixture = CryptoYamlFixture { signer, public_key, @@ -1367,6 +1451,9 @@ mod tests { ml_dsa_44_public_key: ml_dsa_44_signer.public_key(), ml_dsa_44_signature: ml_dsa_44_signer.sign_digest(&block_digest), ml_dsa_44_signer, + ml_dsa_65_public_key: ml_dsa_65_signer.public_key(), + ml_dsa_65_signature: ml_dsa_65_signer.sign_digest(&block_digest), + ml_dsa_65_signer, }; let yaml = serde_yaml::to_string(&fixture).unwrap(); @@ -1390,6 +1477,12 @@ mod tests { fixture.ml_dsa_44_signer.public_key(), decoded.ml_dsa_44_signer.public_key() ); + assert_eq!(fixture.ml_dsa_65_public_key, decoded.ml_dsa_65_public_key); + assert_eq!(fixture.ml_dsa_65_signature, decoded.ml_dsa_65_signature); + assert_eq!( + fixture.ml_dsa_65_signer.public_key(), + decoded.ml_dsa_65_signer.public_key() + ); assert_eq!( fixture.bls_signer.public_key(), decoded.bls_signer.public_key() diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index 62f380bb..39b04008 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -289,45 +289,30 @@ pub struct ProtocolConfig { impl ProtocolConfig { pub fn from_str(value: &str) -> Result { - let (consensus_protocol, block_authentication_scheme) = match value { - "starfish-mac" => ( - ConsensusProtocol::Starfish, - BlockAuthenticationScheme::MacVector, - ), - "starfish-ml-dsa-44" => ( - ConsensusProtocol::Starfish, - BlockAuthenticationScheme::MlDsa44, - ), - "starfish-speed-mac" => ( - ConsensusProtocol::StarfishSpeed, - BlockAuthenticationScheme::MacVector, - ), - "starfish-speed-ml-dsa-44" => ( - ConsensusProtocol::StarfishSpeed, - BlockAuthenticationScheme::MlDsa44, - ), - "sparse-starfish-speed-mac" => ( - ConsensusProtocol::SparseStarfishSpeed, - BlockAuthenticationScheme::MacVector, - ), - "sparse-starfish-speed-ml-dsa-44" => ( - ConsensusProtocol::SparseStarfishSpeed, - BlockAuthenticationScheme::MlDsa44, - ), - "bluestreak-mac" => ( - ConsensusProtocol::Bluestreak, - BlockAuthenticationScheme::MacVector, - ), - "bluestreak-ml-dsa-44" => ( - ConsensusProtocol::Bluestreak, - BlockAuthenticationScheme::MlDsa44, - ), - known => ( - ConsensusProtocol::from_known_str(known) - .ok_or_else(|| format!("Unknown consensus protocol '{known}'"))?, - BlockAuthenticationScheme::Ed25519, - ), - }; + let (protocol_name, block_authentication_scheme) = [ + ("-ml-dsa-65", BlockAuthenticationScheme::MlDsa65), + ("-ml-dsa-44", BlockAuthenticationScheme::MlDsa44), + ("-mac", BlockAuthenticationScheme::MacVector), + ] + .into_iter() + .find_map(|(suffix, scheme)| value.strip_suffix(suffix).map(|base| (base, scheme))) + .unwrap_or((value, BlockAuthenticationScheme::Ed25519)); + + let consensus_protocol = ConsensusProtocol::from_known_str(protocol_name) + .ok_or_else(|| format!("Unknown consensus protocol '{value}'"))?; + if block_authentication_scheme != BlockAuthenticationScheme::Ed25519 + && !matches!( + consensus_protocol, + ConsensusProtocol::Starfish + | ConsensusProtocol::StarfishSpeed + | ConsensusProtocol::SparseStarfishSpeed + | ConsensusProtocol::Bluestreak + ) + { + return Err(format!( + "Block authentication variants are not supported for '{protocol_name}'" + )); + } Ok(Self { consensus_protocol, block_authentication_scheme, @@ -4955,6 +4940,13 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("starfish-ml-dsa-65").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Starfish, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, + } + ); assert_eq!( ProtocolConfig::from_str("starfish-speed").unwrap(), ProtocolConfig { @@ -4976,6 +4968,13 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("starfish-speed-ml-dsa-65").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::StarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, + } + ); assert_eq!( ProtocolConfig::from_str("sparse-starfish-speed").unwrap(), ProtocolConfig { @@ -4997,6 +4996,13 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("sparse-starfish-speed-ml-dsa-65").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, + } + ); assert_eq!( ProtocolConfig::from_str("bluestreak").unwrap(), ProtocolConfig { @@ -5018,6 +5024,14 @@ mod tests { block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, } ); + assert_eq!( + ProtocolConfig::from_str("bluestreak-ml-dsa-65").unwrap(), + ProtocolConfig { + consensus_protocol: ConsensusProtocol::Bluestreak, + block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, + } + ); + assert!(ProtocolConfig::from_str("mysticeti-ml-dsa-65").is_err()); assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); assert!(ProtocolConfig::from_str("starfish-speed-unknown").is_err()); assert!(ProtocolConfig::from_str("sparse-starfish-speed-unknown").is_err()); diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 72ce9e46..39773db1 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -42,7 +42,8 @@ use crate::{ crypto, crypto::{ AsBytes, BlsSignatureBytes, BlsSigner, CryptoHash, MacKey, MacTag, MlDsa44SignatureBytes, - MlDsa44Signer, SignatureBytes, Signer, TransactionsCommitment, + MlDsa44Signer, MlDsa65SignatureBytes, MlDsa65Signer, SignatureBytes, Signer, + TransactionsCommitment, }, dag_state::ConsensusProtocol, data::{Data, IN_MEMORY_BLOCKS, IN_MEMORY_BLOCKS_BYTES}, @@ -263,6 +264,7 @@ pub enum BlockAuthentication { /// Recipient-specific authenticator selected from a full vector by a relay. MacTag(MacTag), MlDsa44(MlDsa44SignatureBytes), + MlDsa65(MlDsa65SignatureBytes), } mod flat_mac_vector { @@ -333,12 +335,14 @@ pub enum BlockAuthenticationScheme { Ed25519, MacVector, MlDsa44, + MlDsa65, } pub enum BlockAuthorizer<'a> { Ed25519(&'a Signer), MacVector(&'a [MacKey]), MlDsa44(&'a MlDsa44Signer), + MlDsa65(&'a MlDsa65Signer), } impl BlockAuthorizer<'_> { @@ -362,6 +366,9 @@ impl BlockAuthorizer<'_> { Self::MlDsa44(signer) => { BlockAuthentication::MlDsa44(signer.sign_digest(content_digest)) } + Self::MlDsa65(signer) => { + BlockAuthentication::MlDsa65(signer.sign_digest(content_digest)) + } } } } @@ -1609,6 +1616,14 @@ impl VerifiedBlock { bail!("Block ML-DSA-44 verification has failed: {error:?}"); } } + (BlockAuthenticationScheme::MlDsa65, BlockAuthentication::MlDsa65(signature)) => { + let Some(public_key) = committee.get_ml_dsa_65_public_key(self.authority()) else { + bail!("Unknown block author {}", self.authority()) + }; + if let Err(error) = public_key.verify_digest_signature(&digest, signature) { + bail!("Block ML-DSA-65 verification has failed: {error:?}"); + } + } (expected, actual) => { bail!("Expected {expected:?} block authentication, received {actual:?}") } @@ -2475,27 +2490,33 @@ mod tests { fn block_reference_depends_only_on_content_across_authentication_schemes() { let committee = Committee::new_for_benchmarks(4); let ed_signers = Signer::new_for_test(committee.len()); - let ml_dsa_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let ml_dsa_44_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let ml_dsa_65_signers = crypto::MlDsa65Signer::new_for_test(committee.len()); let mac_keyrings = crypto::mac_keyrings_for_test(committee.len()); let ed = BlockAuthorizer::Ed25519(&ed_signers[0]); let mac = BlockAuthorizer::MacVector(&mac_keyrings[0]); - let ml_dsa = BlockAuthorizer::MlDsa44(&ml_dsa_signers[0]); + let ml_dsa_44 = BlockAuthorizer::MlDsa44(&ml_dsa_44_signers[0]); + let ml_dsa_65 = BlockAuthorizer::MlDsa65(&ml_dsa_65_signers[0]); let ed_block = make_authenticated_starfish_block(&committee, &ed); let mac_block = make_authenticated_starfish_block(&committee, &mac); - let ml_dsa_block = make_authenticated_starfish_block(&committee, &ml_dsa); + let ml_dsa_44_block = make_authenticated_starfish_block(&committee, &ml_dsa_44); + let ml_dsa_65_block = make_authenticated_starfish_block(&committee, &ml_dsa_65); assert_eq!(ed_block.reference(), mac_block.reference()); - assert_eq!(ed_block.reference(), ml_dsa_block.reference()); + assert_eq!(ed_block.reference(), ml_dsa_44_block.reference()); + assert_eq!(ed_block.reference(), ml_dsa_65_block.reference()); assert_ne!(ed_block.authentication(), mac_block.authentication()); - assert_ne!(ed_block.authentication(), ml_dsa_block.authentication()); + assert_ne!(ed_block.authentication(), ml_dsa_44_block.authentication()); + assert_ne!(ed_block.authentication(), ml_dsa_65_block.authentication()); } #[test] fn all_authentication_schemes_verify_for_starfish_protocols() { let committee = Committee::new_for_benchmarks(4); let ed_signers = Signer::new_for_test(committee.len()); - let ml_dsa_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let ml_dsa_44_signers = crypto::MlDsa44Signer::new_for_test(committee.len()); + let ml_dsa_65_signers = crypto::MlDsa65Signer::new_for_test(committee.len()); let mac_keyrings = crypto::mac_keyrings_for_test(committee.len()); for consensus_protocol in [ @@ -2529,10 +2550,18 @@ mod tests { make_authenticated_starfish_block_for_author( &committee, author as AuthorityIndex, - &BlockAuthorizer::MlDsa44(&ml_dsa_signers[author]), + &BlockAuthorizer::MlDsa44(&ml_dsa_44_signers[author]), ), BlockAuthenticationScheme::MlDsa44, ), + ( + make_authenticated_starfish_block_for_author( + &committee, + author as AuthorityIndex, + &BlockAuthorizer::MlDsa65(&ml_dsa_65_signers[author]), + ), + BlockAuthenticationScheme::MlDsa65, + ), ]; for (block, scheme) in &cases { for (receiver, receiver_keys) in mac_keyrings.iter().enumerate() { diff --git a/crates/starfish-core/src/validator.rs b/crates/starfish-core/src/validator.rs index 2ef9825d..a7d3a9f2 100644 --- a/crates/starfish-core/src/validator.rs +++ b/crates/starfish-core/src/validator.rs @@ -73,6 +73,15 @@ impl Validator { )); } } + BlockAuthenticationScheme::MlDsa65 => { + if committee.get_ml_dsa_65_public_key(authority) + != Some(&private_config.ml_dsa_65_keypair.public_key()) + { + return Err(eyre!( + "ML-DSA-65 private key does not match committee authority {authority}" + )); + } + } } // Network and metrics setup remains the same let network_address = public_config @@ -339,9 +348,11 @@ mod smoke_tests { #[test_case("starfish", 60)] #[test_case("starfish-mac", 700)] #[test_case("starfish-ml-dsa-44", 720)] + #[test_case("starfish-ml-dsa-65", 1000)] #[test_case("starfish-speed", 80)] #[test_case("starfish-speed-mac", 760)] #[test_case("starfish-speed-ml-dsa-44", 780)] + #[test_case("starfish-speed-ml-dsa-65", 1040)] #[test_case("starfish-bls", 100)] #[test_case("sailfish++", 120)] #[test_case("bluestreak", 140)] @@ -349,8 +360,10 @@ mod smoke_tests { #[test_case("sparse-starfish-speed", 180)] #[test_case("sparse-starfish-speed-mac", 840)] #[test_case("sparse-starfish-speed-ml-dsa-44", 860)] + #[test_case("sparse-starfish-speed-ml-dsa-65", 1080)] #[test_case("bluestreak-mac", 920)] #[test_case("bluestreak-ml-dsa-44", 940)] + #[test_case("bluestreak-ml-dsa-65", 1120)] #[tokio::test] async fn validator_commit(consensus: &str, port_offset: u16) { run_commit_test(consensus, port_offset).await; @@ -446,9 +459,12 @@ mod smoke_tests { #[test_case("cordial-miners", 140)] #[test_case("starfish", 160)] #[test_case("starfish-mac", 740)] + #[test_case("starfish-ml-dsa-44", 1020)] + #[test_case("starfish-ml-dsa-65", 1200)] #[test_case("starfish-speed", 180)] #[test_case("starfish-speed-mac", 800)] #[test_case("starfish-speed-ml-dsa-44", 820)] + #[test_case("starfish-speed-ml-dsa-65", 1220)] #[test_case("starfish-bls", 200)] #[test_case("sailfish++", 220)] #[test_case("bluestreak", 260)] @@ -456,8 +472,10 @@ mod smoke_tests { #[test_case("sparse-starfish-speed", 320)] #[test_case("sparse-starfish-speed-mac", 880)] #[test_case("sparse-starfish-speed-ml-dsa-44", 900)] + #[test_case("sparse-starfish-speed-ml-dsa-65", 1240)] #[test_case("bluestreak-mac", 960)] #[test_case("bluestreak-ml-dsa-44", 980)] + #[test_case("bluestreak-ml-dsa-65", 1260)] #[tokio::test] async fn validator_sync(consensus: &str, port_offset: u16) { run_sync_test(consensus, port_offset).await; diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index 12b3cef9..11a5f588 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -67,7 +67,7 @@ enum Operation { #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, }, @@ -97,7 +97,7 @@ enum Operation { #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, /// Directory to store validator data (default: current directory) @@ -149,7 +149,7 @@ enum Operation { #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-44`). + /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, #[clap(long, value_name = "INT", default_value_t = 600)] diff --git a/local-dryrun/README.md b/local-dryrun/README.md index f2c54f69..8b4e0b7b 100644 --- a/local-dryrun/README.md +++ b/local-dryrun/README.md @@ -32,13 +32,15 @@ NUM_NODES=10 DESIRED_TPS=1000 CONSENSUS=starfish \ | `CONSENSUS` | `bluestreak` | Consensus protocol (see below) | | `TEST_TIME` | `3000` | Experiment duration in seconds | -Supported `CONSENSUS` values include the Ed25519, MAC, and ML-DSA-44 variants +Supported `CONSENSUS` values include the Ed25519, MAC, ML-DSA-44, and ML-DSA-65 variants of Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak: -`starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-speed`, -`starfish-speed-mac`, `starfish-speed-ml-dsa-44`, +`starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-ml-dsa-65`, +`starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, +`starfish-speed-ml-dsa-65`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, -`sparse-starfish-speed-ml-dsa-44`, `bluestreak`, `bluestreak-mac`, -`bluestreak-ml-dsa-44`, `starfish-bls`, `cordial-miners`, `mysticeti`, +`sparse-starfish-speed-ml-dsa-44`, `sparse-starfish-speed-ml-dsa-65`, +`bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, +`bluestreak-ml-dsa-65`, `starfish-bls`, `cordial-miners`, `mysticeti`, `sailfish-pp`, and `mysticeti-bls`. ### Protocol Tuning diff --git a/local-dryrun/dryrun.sh b/local-dryrun/dryrun.sh index 37982a4c..117404d1 100755 --- a/local-dryrun/dryrun.sh +++ b/local-dryrun/dryrun.sh @@ -6,7 +6,7 @@ NUM_NODES=${NUM_NODES:-10} NUM_CRASHED_NODES=${NUM_CRASHED_NODES:-0} DESIRED_TPS=${DESIRED_TPS:-100} -# Authentication variants append -mac or -ml-dsa-44 to starfish, +# Authentication variants append -mac, -ml-dsa-44, or -ml-dsa-65 to starfish, # starfish-speed, sparse-starfish-speed, or bluestreak. # Other options: starfish-bls, cordial-miners, mysticeti, # sailfish-pp, mysticeti-bls From ce7b46e49d9b92a298da4b05ad0564639cc30d51 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:04:51 +0200 Subject: [PATCH 16/19] Record ML-DSA-65 authentication benchmarks --- README.md | 4 +- ...sh-authentication-geo-40-validators-60s.md | 46 +++++++++---- ...14-authentication-geo-60-validators-60s.md | 65 +++++++++++++------ 3 files changed, 81 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8d844c37..bab9266b 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ treated as production-ready cryptography. See the [40-validator geographic authentication comparison](benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md) -for matching Ed25519, MAC-vector, and ML-DSA-44 measurements across all four -protocol families. A separate +for matching Ed25519, MAC-vector, ML-DSA-44, and ML-DSA-65 measurements across +all four protocol families. A separate [60-validator moderate-load comparison](benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md) records the single-machine scaling boundary at 600 tx/s aggregate load. diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md index 697eb443..e52471f3 100644 --- a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md +++ b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md @@ -1,7 +1,8 @@ # Starfish and Bluestreak authentication comparison — 40-validator geographic emulation Date: 2026-07-13–14
-Source revisions: `c90f8fb` (Starfish families), `3562890` (Bluestreak)
+Source revisions: `c90f8fb` (original Starfish families), `3562890` +(original Bluestreak), `3a82c04` (ML-DSA-65 extension)
Host: Apple Silicon (`arm64`), macOS 15.7.4
Build: Rust 1.86.0, release profile @@ -45,17 +46,21 @@ target/release/starfish local-benchmark \ | Starfish | Ed25519 | 590.92 | 644.60 | 790.63 | 407.02 | 5.28 | 5.27 | 13.67 | | Starfish | MAC vector | 592.98 | 646.42 | 792.77 | 393.48 | 5.63 | 5.63 | 14.54 | | Starfish | ML-DSA-44 | 571.38 | 627.02 | 792.33 | 399.95 | 7.44 | 7.44 | 19.22 | +| Starfish | ML-DSA-65 | 567.80 | 624.50 | 794.25 | 406.15 | 7.81 | 7.81 | 20.15 | | Starfish Speed | Ed25519 | 712.02 | 843.95 | 794.25 | 338.55 | 4.39 | 4.39 | 11.32 | | Starfish Speed | MAC vector | 679.67 | 788.67 | 794.10 | 344.63 | 4.68 | 4.67 | 12.06 | | Starfish Speed | ML-DSA-44 | 648.98 | 733.05 | 794.35 | 354.83 | 6.54 | 6.53 | 16.85 | +| Starfish Speed | ML-DSA-65 | 520.70 | 601.35 | 795.93 | 367.73 | 7.61 | 7.60 | 19.57 | | Sparse-Starfish-Speed | Ed25519 | 446.98 | 507.93 | 795.08 | 367.70 | 0.85 | 0.85 | 2.19 | | Sparse-Starfish-Speed | MAC vector | 441.60 | 502.15 | 790.58 | 366.55 | 1.23 | 1.23 | 3.19 | | Sparse-Starfish-Speed | ML-DSA-44 | 435.23 | 499.48 | 794.33 | 371.28 | 2.38 | 2.38 | 6.14 | +| Sparse-Starfish-Speed | ML-DSA-65 | 434.75 | 495.95 | 788.70 | 377.72 | 2.92 | 2.92 | 7.59 | | Bluestreak | Ed25519 | 417.65 | 477.93 | 793.48 | 389.53 | 0.59 | 0.59 | 1.53 | | Bluestreak | MAC vector | 418.95 | 478.07 | 793.38 | 363.75 | 0.99 | 0.99 | 2.56 | | Bluestreak | ML-DSA-44 | 420.70 | 478.43 | 795.43 | 368.57 | 1.41 | 1.41 | 3.62 | +| Bluestreak | ML-DSA-65 | 421.02 | 479.50 | 796.03 | 364.83 | 1.71 | 1.70 | 4.39 | -All twelve commands exited successfully after printing their metrics. No +All sixteen commands exited successfully after printing their metrics. No deserialize, socket-buffer, or transport errors were observed during these runs. @@ -65,12 +70,16 @@ runs. |---|---|---:|---:|---:|---:|---:| | Starfish | MAC vector | +0.3% | +0.3% | +0.3% | -3.3% | +6.6% | | Starfish | ML-DSA-44 | -3.3% | -2.7% | +0.2% | -1.7% | +40.9% | +| Starfish | ML-DSA-65 | -3.9% | -3.1% | +0.5% | -0.2% | +47.9% | | Starfish Speed | MAC vector | -4.5% | -6.6% | 0.0% | +1.8% | +6.6% | | Starfish Speed | ML-DSA-44 | -8.9% | -13.1% | 0.0% | +4.8% | +49.0% | +| Starfish Speed | ML-DSA-65 | -26.9% | -28.7% | +0.2% | +8.6% | +73.3% | | Sparse-Starfish-Speed | MAC vector | -1.2% | -1.1% | -0.6% | -0.3% | +44.7% | | Sparse-Starfish-Speed | ML-DSA-44 | -2.6% | -1.7% | -0.1% | +1.0% | +180.0% | +| Sparse-Starfish-Speed | ML-DSA-65 | -2.7% | -2.4% | -0.8% | +2.7% | +243.5% | | Bluestreak | MAC vector | +0.3% | 0.0% | 0.0% | -6.6% | +67.8% | | Bluestreak | ML-DSA-44 | +0.7% | +0.1% | +0.2% | -5.4% | +139.0% | +| Bluestreak | ML-DSA-65 | +0.8% | +0.3% | +0.3% | -6.3% | +189.8% | The lower latency values in some MAC and ML-DSA runs must not be interpreted as an authentication speedup. These are single, sequential trials without a @@ -83,46 +92,54 @@ randomized order or variance estimates. | Starfish Speed | Ed25519 | +20.5% | +30.9% | +0.5% | -16.8% | -16.9% | | Starfish Speed | MAC vector | +14.6% | +22.0% | +0.2% | -12.4% | -16.9% | | Starfish Speed | ML-DSA-44 | +13.6% | +16.9% | +0.3% | -11.3% | -12.1% | +| Starfish Speed | ML-DSA-65 | -8.3% | -3.7% | +0.2% | -9.5% | -2.6% | | Sparse-Starfish-Speed | Ed25519 | -24.4% | -21.2% | +0.6% | -9.7% | -83.9% | | Sparse-Starfish-Speed | MAC vector | -25.5% | -22.3% | -0.3% | -6.8% | -78.2% | | Sparse-Starfish-Speed | ML-DSA-44 | -23.8% | -20.3% | +0.3% | -7.2% | -68.0% | +| Sparse-Starfish-Speed | ML-DSA-65 | -23.4% | -20.6% | -0.7% | -7.0% | -62.6% | | Bluestreak | Ed25519 | -29.3% | -25.9% | +0.4% | -4.3% | -88.8% | | Bluestreak | MAC vector | -29.3% | -26.0% | +0.1% | -7.6% | -82.4% | | Bluestreak | ML-DSA-44 | -26.4% | -23.7% | +0.4% | -7.8% | -81.0% | +| Bluestreak | ML-DSA-65 | -25.9% | -23.2% | +0.2% | -10.2% | -78.1% | ## Interpretation - The corrected workload is 1,000 tx/s total, not 4,000 tx/s. The harness divides the total evenly, so every validator generates 25 tx/s. - Authentication choice did not materially affect committed throughput. All - variants reported 790.58-795.43 TPS, a spread of 0.6% across the complete + variants reported 788.70-796.03 TPS, a spread of 0.9% across the complete matrix. - The local harness includes the 12-second connection warm-up in its 60-second TPS denominator: the default 10 seconds plus 2 seconds for a 40-validator committee. Roughly 48 seconds therefore submit transactions; - the measured 790.58-795.43 TPS corresponds to about 988-994 tx/s during the + the measured 788.70-796.03 TPS corresponds to about 986-995 tx/s during the active submission window, close to the offered 1,000 tx/s. - For plain Starfish and Starfish Speed, MAC raises outbound bandwidth by - 6.6% over Ed25519. ML-DSA-44 raises it by 40.9% and 49.0%, respectively. + 6.6% over Ed25519. ML-DSA-44 raises it by 40.9% and 49.0%, while ML-DSA-65 + raises it by 47.9% and 73.3%, respectively. - Bluestreak's baseline protocol traffic is even leaner. Its MAC vector raises outbound bandwidth from 0.59 to 0.99 MB/s (+67.8%), while ML-DSA-44 raises - it to 1.41 MB/s (+139.0%). Latency and throughput remain within 0.7% of its - Ed25519 baseline. + it to 1.41 MB/s (+139.0%) and ML-DSA-65 to 1.71 MB/s (+189.8%). Latency and + throughput remain within 0.8% of its Ed25519 baseline. - Sparse-Starfish-Speed removes so much baseline protocol traffic that authentication bytes become a larger fraction of the remainder. Its MAC variant rises from 0.85 to 1.23 MB/s (+44.7%), and ML-DSA-44 rises to - 2.38 MB/s (+180.0%). The absolute traffic remains below every matching - non-sparse variant. + 2.38 MB/s (+180.0%); ML-DSA-65 reaches 2.92 MB/s (+243.5%). The absolute + traffic remains below every matching non-sparse variant. +- The signature sizes explain the incremental ML-DSA bandwidth: Ed25519 is + 64 bytes, ML-DSA-44 is 2,420 bytes, and ML-DSA-65 is 3,309 bytes. ML-DSA-65 + public keys are 1,952 bytes but are provisioned in the committee config, + not sent in every block. - The MAC result is consistent with the current design: direct author block streaming carries the full committee-sized MAC vector, while relay and synchronization paths carry one recipient tag. Consequently, the remaining author-stream authentication cost grows with committee size. - Bluestreak is the strongest 40-validator result in this local emulation: - 418-421 ms block latency, about 478 ms end-to-end latency, and - 0.59-1.41 MB/s outbound across the three authentication schemes. Relative + 418-421 ms block latency, 478-480 ms end-to-end latency, and + 0.59-1.71 MB/s outbound across the four authentication schemes. Relative to matching Sparse-Starfish-Speed authentication, it reduces block latency - by 3.3-6.6%, end-to-end latency by 4.2-5.9%, and outbound bandwidth by - 19.5-40.8%. + by 3.2-6.6%, end-to-end latency by 3.3-5.9%, and outbound bandwidth by + 19.5-41.4%. - Starfish Speed alone was slower than plain Starfish at 40 validators even though it was faster in the earlier 10-validator experiment. This reversal points to a single-host scaling or run-variance effect and needs randomized, @@ -135,6 +152,9 @@ randomized order or variance estimates. clearest result; latency differences need repeated trials. - The Bluestreak runs were made one day after the Starfish-family runs using the same host and benchmark configuration but a newer source revision. +- The ML-DSA-65 rows were appended after the original matrix on source + revision `3a82c04`; they use the same host, release profile, and command + template. - Four validators share each synthetic region profile. This produces the AWS delay distribution but does not model independent machines or real WAN bandwidth constraints. diff --git a/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md b/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md index 3182c901..bdc746ae 100644 --- a/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md +++ b/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md @@ -1,7 +1,8 @@ # Authentication comparison — 60-validator geographic emulation Date: 2026-07-14
-Source revision: `4553e5e`
+Source revisions: `4553e5e` (original matrix), `3a82c04` (ML-DSA-65 +extension)
Host: Apple Silicon (`arm64`), macOS 15.7.4
Build: Rust 1.86.0, release profile @@ -42,18 +43,27 @@ target/release/starfish local-benchmark \ | Starfish | Ed25519 | 664.45 | 728.77 | 444.30 | 515.02 | 8.59 | 8.58 | 39.59 | | Starfish | MAC vector | 680.68 | 753.42 | 414.87 | 449.23 | 8.35 | 8.34 | 41.21 | | Starfish | ML-DSA-44 | 849.38 | 937.02 | 427.77 | 417.95 | 9.72 | 9.71 | 46.53 | +| Starfish | ML-DSA-65† | 749.38 | 837.48 | 413.53 | 443.48 | 13.98 | 13.98 | 69.25 | | Starfish Speed | Ed25519 | 592.13 | 673.87 | 460.37 | 467.12 | 6.63 | 6.62 | 29.49 | | Starfish Speed | MAC vector | 670.80 | 783.93 | 430.95 | 439.30 | 7.30 | 7.30 | 34.70 | | Starfish Speed | ML-DSA-44 | 641.70 | 726.03 | 433.63 | 461.57 | 11.06 | 11.05 | 52.22 | +| Starfish Speed | ML-DSA-65 | 641.40 | 727.28 | 421.98 | 415.88 | 12.10 | 12.09 | 58.71 | | Sparse-Starfish-Speed | Ed25519 | 467.70 | 525.98 | 466.22 | 535.57 | 0.88 | 0.88 | 3.87 | | Sparse-Starfish-Speed | MAC vector | 467.67 | 528.23 | 466.62 | 551.88 | 1.84 | 1.84 | 8.08 | | Sparse-Starfish-Speed | ML-DSA-44 | 464.87 | 526.97 | 467.07 | 528.88 | 2.98 | 2.98 | 13.08 | +| Sparse-Starfish-Speed | ML-DSA-65 | 461.05 | 516.02 | 469.62 | 560.13 | 3.95 | 3.95 | 17.24 | | Bluestreak | Ed25519 | 422.53 | 480.58 | 468.22 | 551.08 | 0.52 | 0.52 | 2.28 | | Bluestreak | MAC vector | 421.62 | 480.37 | 467.63 | 545.72 | 1.47 | 1.46 | 6.42 | | Bluestreak | ML-DSA-44 | 422.72 | 481.70 | 467.88 | 547.13 | 1.75 | 1.75 | 7.67 | +| Bluestreak | ML-DSA-65 | 423.98 | 482.82 | 470.25 | 548.02 | 2.22 | 2.22 | 9.66 | -All twelve commands exited successfully after emitting their metrics. No -authentication, deserialize, socket-buffer, or transport errors were observed. +All sixteen commands exited successfully after emitting their metrics. The +Starfish ML-DSA-65 run marked † crossed this host's socket-buffer ceiling at +second 20: three writes reported `No buffer space available`, followed by +three decompress/deserialize warnings. Its metrics are retained as a +host-contended stress result, not a clean protocol comparison. The other +three ML-DSA-65 runs had no authentication, deserialize, socket-buffer, or +transport errors. ## Warm-up-adjusted offered-rate utilization @@ -67,15 +77,19 @@ seconds. The following values estimate the active rate as `TPS × 60 / 47`. | Starfish | Ed25519 | 567.19 | 94.5% | | Starfish | MAC vector | 529.62 | 88.3% | | Starfish | ML-DSA-44 | 546.09 | 91.0% | +| Starfish | ML-DSA-65† | 527.91 | 88.0% | | Starfish Speed | Ed25519 | 587.71 | 98.0% | | Starfish Speed | MAC vector | 550.15 | 91.7% | | Starfish Speed | ML-DSA-44 | 553.57 | 92.3% | +| Starfish Speed | ML-DSA-65 | 538.70 | 89.8% | | Sparse-Starfish-Speed | Ed25519 | 595.17 | 99.2% | | Sparse-Starfish-Speed | MAC vector | 595.69 | 99.3% | | Sparse-Starfish-Speed | ML-DSA-44 | 596.26 | 99.4% | +| Sparse-Starfish-Speed | ML-DSA-65 | 599.51 | 99.9% | | Bluestreak | Ed25519 | 597.73 | 99.6% | | Bluestreak | MAC vector | 596.97 | 99.5% | | Bluestreak | ML-DSA-44 | 597.29 | 99.5% | +| Bluestreak | ML-DSA-65 | 600.32 | 100.1% | ## Relative to Ed25519 within each protocol @@ -83,41 +97,50 @@ seconds. The following values estimate the active rate as `TPS × 60 / 47`. |---|---|---:|---:|---:|---:|---:| | Starfish | MAC vector | +2.4% | +3.4% | -6.6% | -12.8% | -2.8% | | Starfish | ML-DSA-44 | +27.8% | +28.6% | -3.7% | -18.8% | +13.2% | +| Starfish | ML-DSA-65† | +12.8% | +14.9% | -6.9% | -13.9% | +62.7% | | Starfish Speed | MAC vector | +13.3% | +16.3% | -6.4% | -6.0% | +10.1% | | Starfish Speed | ML-DSA-44 | +8.4% | +7.7% | -5.8% | -1.2% | +66.8% | +| Starfish Speed | ML-DSA-65 | +8.3% | +7.9% | -8.3% | -11.0% | +82.5% | | Sparse-Starfish-Speed | MAC vector | 0.0% | +0.4% | +0.1% | +3.0% | +109.1% | | Sparse-Starfish-Speed | ML-DSA-44 | -0.6% | +0.2% | +0.2% | -1.2% | +238.6% | +| Sparse-Starfish-Speed | ML-DSA-65 | -1.4% | -1.9% | +0.7% | +4.6% | +348.9% | | Bluestreak | MAC vector | -0.2% | 0.0% | -0.1% | -1.0% | +182.7% | | Bluestreak | ML-DSA-44 | 0.0% | +0.2% | -0.1% | -0.7% | +236.5% | +| Bluestreak | ML-DSA-65 | +0.3% | +0.5% | +0.4% | -0.6% | +326.9% | Raw bandwidth can fall despite a larger authentication proof when a run produces fewer blocks, as in plain Starfish MAC. The bandwidth-efficiency metric—the ratio of bytes sent to committed transaction-payload bytes—rises -from 39.59 to 41.21 and captures the normalized increase. +from 39.59 to 41.21 for MAC and 69.25 for ML-DSA-65, capturing the normalized +increase even when raw block production varies. ## Interpretation - Sixty validators are feasible on this machine at a 600 tx/s aggregate offered load, but headroom depends strongly on the protocol family. -- Sparse-Starfish-Speed and Bluestreak sustain 99.2-99.6% of the active offered - rate for every authentication scheme. Their latency and throughput vary by - at most 0.6% and 0.2%, respectively, within each family. -- Plain Starfish sustains 88.3-94.5% of the active offered rate, while - Starfish Speed sustains 91.7-98.0%. Their authentication comparisons are - therefore partly measurements of shared-host contention. In particular, - plain Starfish ML-DSA-44 has 27.8% higher block latency than its Ed25519 run. +- Sparse-Starfish-Speed and Bluestreak sustain 99.2-100.1% of the active + offered rate across all four authentication schemes. Within each family, + block latency varies by at most 1.4% and throughput by at most 0.7%. +- Plain Starfish sustains 88.0-94.5% of the active offered rate, while + Starfish Speed sustains 89.8-98.0%. Their authentication comparisons are + therefore partly measurements of shared-host contention. Plain Starfish + ML-DSA-65 is the clearest limit: its 13.98 MB/s full-mesh traffic triggered + the host's socket-buffer errors, so that row is not a clean protocol result. - Bluestreak has the lowest latency and absolute bandwidth across every - authentication scheme: 422-423 ms block latency, 480-482 ms end-to-end - latency, and 0.52-1.75 MB/s outbound. + authentication scheme: 422-424 ms block latency, 480-483 ms end-to-end + latency, and 0.52-2.22 MB/s outbound. - Relative to matching Sparse-Starfish-Speed authentication, Bluestreak lowers - block latency by 9.1-9.8%, end-to-end latency by 8.6-9.1%, and outbound - bandwidth by 20.1-41.3%, with nearly identical throughput. + block latency by 8.0-9.8%, end-to-end latency by 6.4-9.1%, and outbound + bandwidth by 20.1-43.8%, with nearly identical throughput. - At 60 validators, a full author MAC vector contains 60 × 32 = 1,920 bytes, compared with a 64-byte Ed25519 signature and a 2,420-byte ML-DSA-44 - signature. Relays and synchronization responses still carry only one - 32-byte recipient tag. Because Bluestreak and Sparse-Starfish-Speed remove - most other traffic, these authentication bytes produce large percentages - while their absolute bandwidth remains well below the denser protocols. + signature; an ML-DSA-65 signature is 3,309 bytes. ML-DSA-65 public keys are + 1,952 bytes and are provisioned in committee configuration rather than sent + in each block. Relays and synchronization responses for MAC blocks still + carry only one 32-byte recipient tag. Because Bluestreak and + Sparse-Starfish-Speed remove most other traffic, authentication bytes + produce large percentages while absolute bandwidth remains well below the + denser protocols. - For repeatable all-protocol authentication comparisons on this single host, 40 validators remains the safer configuration. Sixty validators is a useful stress configuration and a clean operating point for the sparse and @@ -128,6 +151,10 @@ from 39.59 to 41.21 and captures the normalized increase. - There is one sequential run per configuration, with no randomized order or confidence interval. Host scheduling and thermal state can affect results. +- The ML-DSA-65 rows were appended after the original matrix on source + revision `3a82c04`. The marked plain-Starfish row encountered host transport + errors and should be treated only as evidence that this configuration + exceeds the local machine's clean operating point. - The 600 tx/s load differs from the earlier 40-validator 1,000 tx/s matrix; the two experiments should not be treated as a pure committee-size scaling comparison. From cd598f03c2cf7c69a7f181a646726a489df6b6f5 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:24:07 +0200 Subject: [PATCH 17/19] Remove unrelated changes from authentication PR --- ...026-07-13-starfish-authentication-local.md | 13 +--- .../2026-07-13-validator-scaling-probe.md | 63 ------------------- crates/starfish-core/src/block_handler.rs | 6 +- .../src/bls_certificate_aggregator.rs | 21 ++++--- crates/starfish-core/src/encoder.rs | 3 +- .../src/transactions_generator.rs | 3 +- crates/starfish-core/src/types.rs | 7 +-- crates/starfish/src/main.rs | 33 +++++----- 8 files changed, 38 insertions(+), 111 deletions(-) delete mode 100644 benchmark-results/2026-07-13-validator-scaling-probe.md diff --git a/benchmark-results/2026-07-13-starfish-authentication-local.md b/benchmark-results/2026-07-13-starfish-authentication-local.md index e51ad802..b4d05f11 100644 --- a/benchmark-results/2026-07-13-starfish-authentication-local.md +++ b/benchmark-results/2026-07-13-starfish-authentication-local.md @@ -1,8 +1,8 @@ # Starfish authentication comparison — local Apple Silicon -Date: 2026-07-13 +Date: 2026-07-13
Source revision: `8a3bded` plus the Sparse authentication changes committed with this report
-Host: Apple Silicon (`arm64`), macOS 15.7.4 +Host: Apple Silicon (`arm64`), macOS 15.7.4
Build: Rust 1.86.0, release profile ## Configuration @@ -95,14 +95,7 @@ target/release/starfish local-benchmark \ storage, and network stack. These results are useful for directional local comparison, not distributed capacity claims. -## Benchmark harness fix - -The previous local benchmark shutdown aborted validator tasks and immediately -deleted their RocksDB directories. On macOS this left benchmark parents stuck -in an uninterruptible exiting state. The harness now uses a `JoinSet`, aborts -all validator tasks, drains them completely, and only then removes storage. -The validation run and all nine measured protocol/authentication combinations -exited normally. +## Benchmark validation note During the first Sparse MAC run, the receiver-side transport guard exposed a round-gap response that still carried full MAC vectors. The sender now routes diff --git a/benchmark-results/2026-07-13-validator-scaling-probe.md b/benchmark-results/2026-07-13-validator-scaling-probe.md deleted file mode 100644 index 0f722eb4..00000000 --- a/benchmark-results/2026-07-13-validator-scaling-probe.md +++ /dev/null @@ -1,63 +0,0 @@ -# Local validator-count scaling probe - -Date: 2026-07-13
-Source revision: `dee67ee`
-Host: Apple Silicon (`arm64`), macOS 15.7.4, 64 GiB RAM, 16 logical CPU cores - -## Purpose - -Find a practical committee-size limit for geographic latency emulation on one -machine. Offered load stays at 100 tx/s per validator. The 20-80-validator -probes run for 30 seconds under the AWS RTT latency model. The 10-validator -row is the earlier 60-second baseline and is included only for orientation. - -## Sparse-Starfish-Speed MAC results - -| Validators | Offered load | Duration | Block latency (ms) | E2E latency (ms) | TPS | BPS | Outbound (MB/s) | Outcome | -|---:|---:|---:|---:|---:|---:|---:|---:|---| -| 10 | 1,000 | 60 s | 418.7 | 483.5 | 816.25 | 92.70 | 0.47 | Clean baseline | -| 20 | 2,000 | 30 s | 435.85 | 501.05 | 1,240.83 | 191.67 | 0.88 | Clean | -| 40 | 4,000 | 30 s | 461.45 | 545.75 | 2,336.33 | 364.13 | 1.96 | Clean | -| 64 | 6,400 | 30 s | 480.42 | 540.67 | 3,435.23 | 591.83 | 3.47 | Clean, near resource saturation | -| 80 | 8,000 | 30 s | 662.36 | 752.25 | 2,238.30 | 470.77 | 2.86 | Unhealthy: socket-buffer and decode errors | - -During the active 64-validator run, the process reached about 1,032% CPU -(roughly 10 cores) and 17.7 GiB resident memory. At 80 validators the network -failed to establish and sustain the full mesh reliably: the run emitted -deserialization warnings and repeated macOS `No buffer space available` -errors. Its throughput regression and latency jump therefore mark it as an -invalid benchmark configuration on this host. - -## Common-size check with plain Starfish MAC - -| Validators | Offered load | Duration | Block latency (ms) | E2E latency (ms) | TPS | BPS | Outbound (MB/s) | Cancelled reconstructions | -|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| 40 | 4,000 | 30 s | 596.08 | 650.00 | 2,331.33 | 366.10 | 5.71 | 184 | - -Plain Starfish completed cleanly at 40 validators but used about 2.9x the -outbound bandwidth of Sparse-Starfish-Speed MAC at the same committee size. -It therefore provides the more conservative limit for a matrix that compares -all protocol families. - -## Limits and recommendation - -- The hard code limit is 512 validators (`MAX_COMMITTEE_SIZE`). This is a type - and data-structure bound, not a realistic single-machine target. -- The process file-descriptor limit is 1,048,575, and benchmark ports remain - well within `u16` even at 512 validators. Neither is the first constraint. -- The local network is a full mesh, so peer relationships grow as `n(n-1)`: - 1,560 at 40 validators, 4,032 at 64, and 6,320 at 80. Each validator also - owns a RocksDB instance. Socket buffers, connection tasks, and database - memory dominate before the hard committee or descriptor limits. -- Use 20 validators for quick, low-risk development comparisons. -- Use 40 validators as the recommended maximum for repeatable comparisons - across Starfish, Starfish Speed, Sparse, and all authentication schemes. -- Treat 64 as a Sparse-only local stress configuration, not a comfortable - full-matrix setting. -- Use multiple machines through the orchestrator beyond 40 validators. For - committees above ten, the local AWS table repeats the same ten regions while - all validators still share one kernel and physical host. - -The 30-second probes have different warm-up proportions from the 60-second -baseline, so their TPS values should not be used as a formal scaling curve. -The clean/error boundary and sampled resource usage are the relevant signals. diff --git a/crates/starfish-core/src/block_handler.rs b/crates/starfish-core/src/block_handler.rs index 871a381a..001fa0a0 100644 --- a/crates/starfish-core/src/block_handler.rs +++ b/crates/starfish-core/src/block_handler.rs @@ -33,9 +33,8 @@ const REAL_BLOCK_HANDLER_TXN_GEN_STEP: usize = 32; const _: () = assert_constants(); #[allow(dead_code)] -#[allow(clippy::manual_is_multiple_of)] const fn assert_constants() { - if REAL_BLOCK_HANDLER_TXN_SIZE % REAL_BLOCK_HANDLER_TXN_GEN_STEP != 0 { + if !REAL_BLOCK_HANDLER_TXN_SIZE.is_multiple_of(REAL_BLOCK_HANDLER_TXN_GEN_STEP) { panic!("REAL_BLOCK_HANDLER_TXN_SIZE % REAL_BLOCK_HANDLER_TXN_GEN_STEP != 0") } } @@ -265,7 +264,6 @@ impl RealCommitHandler { } impl CommitObserver for RealCommitHandler { - #[allow(clippy::manual_is_multiple_of)] fn handle_commit( &mut self, dag_state: &DagState, @@ -294,7 +292,7 @@ impl CommitObserver for RealCommitHandler { let digest_short = u16::from_le_bytes([self.commit_digest[0], self.commit_digest[1]]) & 0x3FF; self.metrics.commit_digest_latest.set(digest_short as i64); - if commit_index % 100 == 0 { + if commit_index.is_multiple_of(100) { self.metrics.commit_digest.set(digest_short as i64); } diff --git a/crates/starfish-core/src/bls_certificate_aggregator.rs b/crates/starfish-core/src/bls_certificate_aggregator.rs index 9f9fe847..865dedcd 100644 --- a/crates/starfish-core/src/bls_certificate_aggregator.rs +++ b/crates/starfish-core/src/bls_certificate_aggregator.rs @@ -297,15 +297,18 @@ impl BlsCertificateAggregator { } else if let Some(&origin_index) = seen_leader_certs.get(&(*leader_ref, *cert)) { push_task_source(&mut origins[origin_index], source); - } else if let Some(task) = self - .aggregate_same_message_task(crypto::bls_leader_message(leader_ref), cert) - { - tasks.push(BlsVerificationTask { - block_index: origins.len(), - ..task - }); - seen_leader_certs.insert((*leader_ref, *cert), origins.len()); - origins.push(TaskOrigin::AggLeader(*leader_ref, *cert, vec![source])); + } else { + if let Some(task) = self.aggregate_same_message_task( + crypto::bls_leader_message(leader_ref), + cert, + ) { + tasks.push(BlsVerificationTask { + block_index: origins.len(), + ..task + }); + seen_leader_certs.insert((*leader_ref, *cert), origins.len()); + origins.push(TaskOrigin::AggLeader(*leader_ref, *cert, vec![source])); + } } } } diff --git a/crates/starfish-core/src/encoder.rs b/crates/starfish-core/src/encoder.rs index a1e0bc01..122b2e48 100644 --- a/crates/starfish-core/src/encoder.rs +++ b/crates/starfish-core/src/encoder.rs @@ -42,7 +42,6 @@ impl ShardEncoder for Encoder { data } - #[allow(clippy::manual_is_multiple_of)] fn encode_transactions( &mut self, block: &[BaseTransaction], @@ -58,7 +57,7 @@ impl ShardEncoder for Encoder { let mut shard_bytes = (bytes_length + 4).div_ceil(info_length); // Ensure shard_bytes meets alignment requirements (must be multiple of 2). - if shard_bytes % 2 != 0 { + if !shard_bytes.is_multiple_of(2) { shard_bytes += 1; } diff --git a/crates/starfish-core/src/transactions_generator.rs b/crates/starfish-core/src/transactions_generator.rs index 3725fe6d..0ca474f9 100644 --- a/crates/starfish-core/src/transactions_generator.rs +++ b/crates/starfish-core/src/transactions_generator.rs @@ -59,7 +59,6 @@ impl TransactionGenerator { ); } - #[allow(clippy::manual_is_multiple_of)] pub async fn run(mut self) { let load = self.parameters.load; let max_transactions_per_block_interval = load.div_ceil(Self::BATCHES_IN_SECOND); @@ -183,7 +182,7 @@ impl TransactionGenerator { return; } - if counter % 10_000 == 0 { + if counter.is_multiple_of(10_000) { self.metrics .submitted_transactions_bytes .inc_by(tx_to_report * tx_size as u64); diff --git a/crates/starfish-core/src/types.rs b/crates/starfish-core/src/types.rs index 39773db1..df6a36ec 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -304,9 +304,9 @@ mod flat_mac_vector { ) } - #[allow(clippy::manual_is_multiple_of)] fn visit_bytes(self, bytes: &[u8]) -> Result { - if bytes.len() % crypto::MAC_TAG_SIZE != 0 { + let chunks = bytes.chunks_exact(crypto::MAC_TAG_SIZE); + if !chunks.remainder().is_empty() { return Err(E::custom(format!( "invalid flat MAC vector length {}; expected a multiple of {}", bytes.len(), @@ -314,8 +314,7 @@ mod flat_mac_vector { ))); } - Ok(bytes - .chunks_exact(crypto::MAC_TAG_SIZE) + Ok(chunks .map(|chunk| { let mut tag = [0; crypto::MAC_TAG_SIZE]; tag.copy_from_slice(chunk); diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index 11a5f588..ee02e1c5 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -28,7 +28,6 @@ use starfish_core::{ types::AuthorityIndex, validator::Validator, }; -use tokio::task::JoinSet; use tokio::time::Instant; use tracing_subscriber::{EnvFilter, filter::LevelFilter, fmt}; @@ -329,7 +328,6 @@ fn benchmark_genesis( Ok(()) } -#[allow(clippy::manual_is_multiple_of)] async fn local_benchmark( committee_size: usize, mut load: usize, @@ -387,7 +385,8 @@ async fn local_benchmark( let base_dir = PathBuf::from("local-benchmark"); fs::create_dir_all(&base_dir)?; - let mut validator_tasks = JoinSet::new(); + let mut handles = Vec::with_capacity(committee_size); + let mut abort_handles = Vec::with_capacity(committee_size); let mut metrics_of_honest_validators = Vec::new(); let mut reporters_of_honest_validators = Vec::new(); @@ -429,7 +428,7 @@ async fn local_benchmark( )); } } - let is_byzantine = authority % 3 == 0 && authority / 3 < num_byzantine_nodes; + let is_byzantine = authority.is_multiple_of(3) && authority / 3 < num_byzantine_nodes; let validator = if is_byzantine { Validator::start( authority as AuthorityIndex, @@ -459,10 +458,12 @@ async fn local_benchmark( } // Use the same pattern as the run method - validator_tasks.spawn(async move { + let handle = tokio::spawn(async move { let (network_result, _metrics_result) = validator.await_completion().await; network_result }); + abort_handles.push(handle.abort_handle()); + handles.push(handle); } // Run for specified duration @@ -479,25 +480,23 @@ async fn local_benchmark( duration_secs, ); - // Abort and fully drain validator tasks before deleting their - // RocksDB directories. On macOS, removing storage while aborted - // tasks are still dropping database handles can leave the - // benchmark parent stuck in an uninterruptible exit state. - validator_tasks.abort_all(); - while validator_tasks.join_next().await.is_some() { + // Abort all tasks + for abort_handle in abort_handles { + abort_handle.abort(); } // Clean up fs::remove_dir_all(base_dir)?; Ok(()) } - result = validator_tasks.join_next() => { - running.store(false, Ordering::SeqCst); - tracing::warn!("Validator terminated before benchmark timeout: {result:?}"); - validator_tasks.abort_all(); - while validator_tasks.join_next().await.is_some() { + _ = async { + for handle in handles { + if let Err(e) = handle.await { + tracing::warn!("Validator terminated with error: {}", e); + } } - println!("A validator completed before timeout"); + } => { + println!("All validators completed before timeout"); Metrics::aggregate_and_display( metrics_of_honest_validators, reporters_of_honest_validators, From 2e7c86b89c8db9caf05aeb33e28bd178f6f86be3 Mon Sep 17 00:00:00 2001 From: NaitsabesMue <51112618+NaitsabesMue@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:48:44 +0200 Subject: [PATCH 18/19] Separate signature selection from MAC experiments --- README.md | 71 ++--- ...sh-authentication-geo-40-validators-60s.md | 164 ------------ ...6-07-13-starfish-authentication-geo-60s.md | 102 ------- ...026-07-13-starfish-authentication-local.md | 104 -------- ...14-authentication-geo-60-validators-60s.md | 165 ------------ crates/orchestrator/README.md | 8 +- crates/orchestrator/src/benchmark.rs | 11 +- crates/orchestrator/src/main.rs | 30 ++- crates/starfish-core/src/config.rs | 5 + crates/starfish-core/src/dag_state.rs | 252 +++++++++--------- crates/starfish-core/src/net_sync.rs | 4 +- crates/starfish-core/src/validator.rs | 127 +++++---- crates/starfish/src/main.rs | 78 +++++- local-dryrun/README.md | 18 +- local-dryrun/dryrun.sh | 11 +- 15 files changed, 354 insertions(+), 796 deletions(-) delete mode 100644 benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md delete mode 100644 benchmark-results/2026-07-13-starfish-authentication-geo-60s.md delete mode 100644 benchmark-results/2026-07-13-starfish-authentication-local.md delete mode 100644 benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md diff --git a/README.md b/README.md index bab9266b..5b2db961 100644 --- a/README.md +++ b/README.md @@ -59,32 +59,29 @@ achieving 2-round optimistic commit latency. leader, data availability) in block headers, with async verification offloaded from the critical path. -### Block authentication experiments - -Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak can each be run -with four interchangeable block-authentication schemes: - -| Protocol | Ed25519 | MAC vector | ML-DSA-44 | ML-DSA-65 | -|---|---|---|---|---| -| Starfish | `starfish` | `starfish-mac` | `starfish-ml-dsa-44` | `starfish-ml-dsa-65` | -| Starfish Speed | `starfish-speed` | `starfish-speed-mac` | `starfish-speed-ml-dsa-44` | `starfish-speed-ml-dsa-65` | -| Sparse-Starfish-Speed | `sparse-starfish-speed` | `sparse-starfish-speed-mac` | `sparse-starfish-speed-ml-dsa-44` | `sparse-starfish-speed-ml-dsa-65` | -| Bluestreak | `bluestreak` | `bluestreak-mac` | `bluestreak-ml-dsa-44` | `bluestreak-ml-dsa-65` | - -For all sixteen variants, `BlockReference.digest` is the BLAKE3 hash of the -canonical block content only. The authentication proof is a separate header -field and does not change the block reference. An author using a MAC variant -sends the full vector, with exactly one tag for every committee member, -to its direct recipients. A direct recipient retains that vector and, when -relaying a block or header, or answering a synchronization request, sends only -the destination's tag. A tag-only copy cannot be relayed a second time. -Receivers accept a full vector only through proactive block streaming directly -from the block's claimed author; relay and synchronization traffic must contain -exactly one recipient tag. If the same node later receives the author's -directly streamed full-vector copy, it upgrades the stored authentication -without adding a second DAG vertex and can then relay recipient-specific tags. -Benchmark genesis deterministically generates the pairwise MAC keys, ML-DSA -seeds, and public keys in the node configuration. +### Block authentication + +Every consensus protocol can select its block signature independently: + +| Scheme | CLI option | +|---|---| +| Ed25519 (default) | `--block-authentication ed25519` or omit the option | +| ML-DSA-44 | `--block-authentication ml-dsa-44` | +| ML-DSA-65 | `--block-authentication ml-dsa-65` | + +For example, `--consensus mysticeti --block-authentication ml-dsa-65` changes +Mysticeti's block signature without creating another consensus protocol. This +selection is also available through the orchestrator. Protocol-specific BLS +certificates are unaffected. These digital-signature selections retain the +transferable public verification assumed by the protocols and do not change +their message flow or proof structure. The same value can be set as +`block_authentication` in the node-parameters YAML; the CLI option overrides +that setting. + +`BlockReference.digest` is the BLAKE3 hash of the canonical block content only. +The modular authentication proof is a separate header field and does not change +the block reference. Benchmark genesis generates all Ed25519 and ML-DSA key +material regardless of the selected signature scheme. The ML-DSA wrappers are generated from a common parameter-set definition. ML-DSA-44 uses 1,312-byte public keys and 2,420-byte signatures; ML-DSA-65 @@ -94,12 +91,22 @@ This is research/benchmark code. The RustCrypto `ml-dsa` implementation used here states that it has not been independently audited and should not be treated as production-ready cryptography. -See the -[40-validator geographic authentication comparison](benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md) -for matching Ed25519, MAC-vector, ML-DSA-44, and ML-DSA-65 measurements across -all four protocol families. A separate -[60-validator moderate-load comparison](benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md) -records the single-machine scaling boundary at 600 tx/s aggregate load. +#### Experimental MAC protocols + +`starfish-mac`, `starfish-speed-mac`, `sparse-starfish-speed-mac`, and +`bluestreak-mac` remain separate work-in-progress benchmark protocols. They are +not interchangeable signature selections and cannot be combined with +`--block-authentication`. + +These variants measure a lower bound for pairwise-MAC authentication. Direct +author streaming carries the full committee-sized MAC vector; relays and +synchronization responses carry only the destination's tag. Pairwise MACs do +not provide transferable authorship, and a Byzantine author can give different +recipients valid and invalid tags for the same block reference. The current +prototype does not add the quorum-authentication/RBC exchange needed to bind +the author to an available authenticator. It therefore makes no safety or +liveness claim and must not be treated as a proven variant of the underlying +protocol. ## Dissemination Modes diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md deleted file mode 100644 index e52471f3..00000000 --- a/benchmark-results/2026-07-13-starfish-authentication-geo-40-validators-60s.md +++ /dev/null @@ -1,164 +0,0 @@ -# Starfish and Bluestreak authentication comparison — 40-validator geographic emulation - -Date: 2026-07-13–14
-Source revisions: `c90f8fb` (original Starfish families), `3562890` -(original Bluestreak), `3a82c04` (ML-DSA-65 extension)
-Host: Apple Silicon (`arm64`), macOS 15.7.4
-Build: Rust 1.86.0, release profile - -## Configuration - -- 40 honest validators in one local process -- 1,000 tx/s aggregate offered load (25 tx/s per validator) -- 60-second measurement window -- Protocol-default dissemination: `push-useful` for the Starfish families and - `pull` for Bluestreak -- One run per configuration -- Geographic latency emulation enabled; no uniform-latency override - -The latency harness has ten AWS region profiles. At 40 validators, validator -indices are mapped modulo ten, producing four validators per modeled region. -The RTT values are divided by two to obtain one-way delays and independent -±3% per-message jitter is applied. Base one-way delays range from 0.5 ms to -154.5 ms. - -This is a single-machine latency emulation, not a deployment on 40 remote -hosts. All validators share the host's CPU, memory, storage, loopback network, -and kernel socket resources. - -In this report, **Sparse-Starfish-Speed means the sparse implementation of -Starfish Speed; it is not Bluestreak.** - -Command template: - -```text -target/release/starfish local-benchmark \ - --committee-size 40 \ - --load 1000 \ - --consensus \ - --duration-secs 60 -``` - -## Results - -| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| Starfish | Ed25519 | 590.92 | 644.60 | 790.63 | 407.02 | 5.28 | 5.27 | 13.67 | -| Starfish | MAC vector | 592.98 | 646.42 | 792.77 | 393.48 | 5.63 | 5.63 | 14.54 | -| Starfish | ML-DSA-44 | 571.38 | 627.02 | 792.33 | 399.95 | 7.44 | 7.44 | 19.22 | -| Starfish | ML-DSA-65 | 567.80 | 624.50 | 794.25 | 406.15 | 7.81 | 7.81 | 20.15 | -| Starfish Speed | Ed25519 | 712.02 | 843.95 | 794.25 | 338.55 | 4.39 | 4.39 | 11.32 | -| Starfish Speed | MAC vector | 679.67 | 788.67 | 794.10 | 344.63 | 4.68 | 4.67 | 12.06 | -| Starfish Speed | ML-DSA-44 | 648.98 | 733.05 | 794.35 | 354.83 | 6.54 | 6.53 | 16.85 | -| Starfish Speed | ML-DSA-65 | 520.70 | 601.35 | 795.93 | 367.73 | 7.61 | 7.60 | 19.57 | -| Sparse-Starfish-Speed | Ed25519 | 446.98 | 507.93 | 795.08 | 367.70 | 0.85 | 0.85 | 2.19 | -| Sparse-Starfish-Speed | MAC vector | 441.60 | 502.15 | 790.58 | 366.55 | 1.23 | 1.23 | 3.19 | -| Sparse-Starfish-Speed | ML-DSA-44 | 435.23 | 499.48 | 794.33 | 371.28 | 2.38 | 2.38 | 6.14 | -| Sparse-Starfish-Speed | ML-DSA-65 | 434.75 | 495.95 | 788.70 | 377.72 | 2.92 | 2.92 | 7.59 | -| Bluestreak | Ed25519 | 417.65 | 477.93 | 793.48 | 389.53 | 0.59 | 0.59 | 1.53 | -| Bluestreak | MAC vector | 418.95 | 478.07 | 793.38 | 363.75 | 0.99 | 0.99 | 2.56 | -| Bluestreak | ML-DSA-44 | 420.70 | 478.43 | 795.43 | 368.57 | 1.41 | 1.41 | 3.62 | -| Bluestreak | ML-DSA-65 | 421.02 | 479.50 | 796.03 | 364.83 | 1.71 | 1.70 | 4.39 | - -All sixteen commands exited successfully after printing their metrics. No -deserialize, socket-buffer, or transport errors were observed during these -runs. - -## Relative to Ed25519 within each protocol - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish | MAC vector | +0.3% | +0.3% | +0.3% | -3.3% | +6.6% | -| Starfish | ML-DSA-44 | -3.3% | -2.7% | +0.2% | -1.7% | +40.9% | -| Starfish | ML-DSA-65 | -3.9% | -3.1% | +0.5% | -0.2% | +47.9% | -| Starfish Speed | MAC vector | -4.5% | -6.6% | 0.0% | +1.8% | +6.6% | -| Starfish Speed | ML-DSA-44 | -8.9% | -13.1% | 0.0% | +4.8% | +49.0% | -| Starfish Speed | ML-DSA-65 | -26.9% | -28.7% | +0.2% | +8.6% | +73.3% | -| Sparse-Starfish-Speed | MAC vector | -1.2% | -1.1% | -0.6% | -0.3% | +44.7% | -| Sparse-Starfish-Speed | ML-DSA-44 | -2.6% | -1.7% | -0.1% | +1.0% | +180.0% | -| Sparse-Starfish-Speed | ML-DSA-65 | -2.7% | -2.4% | -0.8% | +2.7% | +243.5% | -| Bluestreak | MAC vector | +0.3% | 0.0% | 0.0% | -6.6% | +67.8% | -| Bluestreak | ML-DSA-44 | +0.7% | +0.1% | +0.2% | -5.4% | +139.0% | -| Bluestreak | ML-DSA-65 | +0.8% | +0.3% | +0.3% | -6.3% | +189.8% | - -The lower latency values in some MAC and ML-DSA runs must not be interpreted -as an authentication speedup. These are single, sequential trials without a -randomized order or variance estimates. - -## Protocol relative to matching Starfish authentication - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish Speed | Ed25519 | +20.5% | +30.9% | +0.5% | -16.8% | -16.9% | -| Starfish Speed | MAC vector | +14.6% | +22.0% | +0.2% | -12.4% | -16.9% | -| Starfish Speed | ML-DSA-44 | +13.6% | +16.9% | +0.3% | -11.3% | -12.1% | -| Starfish Speed | ML-DSA-65 | -8.3% | -3.7% | +0.2% | -9.5% | -2.6% | -| Sparse-Starfish-Speed | Ed25519 | -24.4% | -21.2% | +0.6% | -9.7% | -83.9% | -| Sparse-Starfish-Speed | MAC vector | -25.5% | -22.3% | -0.3% | -6.8% | -78.2% | -| Sparse-Starfish-Speed | ML-DSA-44 | -23.8% | -20.3% | +0.3% | -7.2% | -68.0% | -| Sparse-Starfish-Speed | ML-DSA-65 | -23.4% | -20.6% | -0.7% | -7.0% | -62.6% | -| Bluestreak | Ed25519 | -29.3% | -25.9% | +0.4% | -4.3% | -88.8% | -| Bluestreak | MAC vector | -29.3% | -26.0% | +0.1% | -7.6% | -82.4% | -| Bluestreak | ML-DSA-44 | -26.4% | -23.7% | +0.4% | -7.8% | -81.0% | -| Bluestreak | ML-DSA-65 | -25.9% | -23.2% | +0.2% | -10.2% | -78.1% | - -## Interpretation - -- The corrected workload is 1,000 tx/s total, not 4,000 tx/s. The harness - divides the total evenly, so every validator generates 25 tx/s. -- Authentication choice did not materially affect committed throughput. All - variants reported 788.70-796.03 TPS, a spread of 0.9% across the complete - matrix. -- The local harness includes the 12-second connection warm-up in its - 60-second TPS denominator: the default 10 seconds plus 2 seconds for a - 40-validator committee. Roughly 48 seconds therefore submit transactions; - the measured 788.70-796.03 TPS corresponds to about 986-995 tx/s during the - active submission window, close to the offered 1,000 tx/s. -- For plain Starfish and Starfish Speed, MAC raises outbound bandwidth by - 6.6% over Ed25519. ML-DSA-44 raises it by 40.9% and 49.0%, while ML-DSA-65 - raises it by 47.9% and 73.3%, respectively. -- Bluestreak's baseline protocol traffic is even leaner. Its MAC vector raises - outbound bandwidth from 0.59 to 0.99 MB/s (+67.8%), while ML-DSA-44 raises - it to 1.41 MB/s (+139.0%) and ML-DSA-65 to 1.71 MB/s (+189.8%). Latency and - throughput remain within 0.8% of its Ed25519 baseline. -- Sparse-Starfish-Speed removes so much baseline protocol traffic that - authentication bytes become a larger fraction of the remainder. Its MAC - variant rises from 0.85 to 1.23 MB/s (+44.7%), and ML-DSA-44 rises to - 2.38 MB/s (+180.0%); ML-DSA-65 reaches 2.92 MB/s (+243.5%). The absolute - traffic remains below every matching non-sparse variant. -- The signature sizes explain the incremental ML-DSA bandwidth: Ed25519 is - 64 bytes, ML-DSA-44 is 2,420 bytes, and ML-DSA-65 is 3,309 bytes. ML-DSA-65 - public keys are 1,952 bytes but are provisioned in the committee config, - not sent in every block. -- The MAC result is consistent with the current design: direct author block - streaming carries the full committee-sized MAC vector, while relay and - synchronization paths carry one recipient tag. Consequently, the remaining - author-stream authentication cost grows with committee size. -- Bluestreak is the strongest 40-validator result in this local emulation: - 418-421 ms block latency, 478-480 ms end-to-end latency, and - 0.59-1.71 MB/s outbound across the four authentication schemes. Relative - to matching Sparse-Starfish-Speed authentication, it reduces block latency - by 3.2-6.6%, end-to-end latency by 3.3-5.9%, and outbound bandwidth by - 19.5-41.4%. -- Starfish Speed alone was slower than plain Starfish at 40 validators even - though it was faster in the earlier 10-validator experiment. This reversal - points to a single-host scaling or run-variance effect and needs randomized, - repeated trials before it is treated as a protocol conclusion. - -## Caveats - -- There is one run per configuration and no randomized run order, warm-up - exclusion, or confidence interval. Relative authentication bandwidth is the - clearest result; latency differences need repeated trials. -- The Bluestreak runs were made one day after the Starfish-family runs using - the same host and benchmark configuration but a newer source revision. -- The ML-DSA-65 rows were appended after the original matrix on source - revision `3a82c04`; they use the same host, release profile, and command - template. -- Four validators share each synthetic region profile. This produces the AWS - delay distribution but does not model independent machines or real WAN - bandwidth constraints. -- The 40 validators form a full mesh of 1,560 directed peer relationships and - use one RocksDB instance each. Host contention is part of the measurement. -- The progress window starts before transaction generation, which explains - why displayed TPS is below the aggregate offered rate. diff --git a/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md b/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md deleted file mode 100644 index a662738f..00000000 --- a/benchmark-results/2026-07-13-starfish-authentication-geo-60s.md +++ /dev/null @@ -1,102 +0,0 @@ -# Starfish authentication comparison — 60-second geographic emulation - -Date: 2026-07-13
-Source revision: `d3f57c2`
-Host: Apple Silicon (`arm64`), macOS 15.7.4
-Build: Rust 1.86.0, release profile - -## Configuration - -- 10 honest validators in one local process -- 1,000 tx/s offered load (100 tx/s per validator) -- 60-second measurement window -- Default `push-useful` dissemination for all three protocols -- One run per configuration -- Geographic latency emulation enabled; no uniform-latency override - -The ten validators map in order to `us-east-1`, `us-west-1`, -`ca-central-1`, `eu-west-1`, `eu-south-1`, `eu-north-1`, `sa-east-1`, -`ap-south-1`, `ap-southeast-1`, and `ap-northeast-1`. The harness converts -its AWS RTT table to one-way delay by dividing each cell by two, then applies -independent ±3% per-message jitter. The resulting base one-way delays range -from 0.5 ms within a region to 154.5 ms between the most distant pair. - -This is a single-machine latency emulation, not a deployment on ten remote -hosts. CPU, storage, and the physical network stack remain shared. - -Command template: - -```text -target/release/starfish local-benchmark \ - --committee-size 10 \ - --load 1000 \ - --consensus \ - --duration-secs 60 -``` - -## Results - -| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| Starfish | Ed25519 | 553.4 | 603.7 | 816.25 | 100.05 | 0.49 | 0.49 | 1.23 | -| Starfish | MAC vector | 559.7 | 612.1 | 815.75 | 94.93 | 0.53 | 0.53 | 1.34 | -| Starfish | ML-DSA-44 | 554.1 | 604.2 | 814.70 | 97.00 | 0.76 | 0.76 | 1.92 | -| Starfish Speed | Ed25519 | 460.6 | 519.7 | 814.83 | 94.33 | 0.51 | 0.51 | 1.29 | -| Starfish Speed | MAC vector | 458.6 | 522.6 | 816.08 | 95.18 | 0.54 | 0.54 | 1.35 | -| Starfish Speed | ML-DSA-44 | 457.7 | 518.5 | 817.83 | 94.13 | 0.77 | 0.77 | 1.93 | -| Sparse-Starfish-Speed | Ed25519 | 417.6 | 485.1 | 818.25 | 95.42 | 0.45 | 0.45 | 1.13 | -| Sparse-Starfish-Speed | MAC vector | 418.7 | 483.5 | 816.25 | 92.70 | 0.47 | 0.47 | 1.18 | -| Sparse-Starfish-Speed | ML-DSA-44 | 420.3 | 486.9 | 817.95 | 92.85 | 0.71 | 0.71 | 1.78 | - -## Relative to Ed25519 within each protocol - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish | MAC vector | +1.1% | +1.4% | -0.1% | -5.1% | +8.2% | -| Starfish | ML-DSA-44 | +0.1% | +0.1% | -0.2% | -3.0% | +55.1% | -| Starfish Speed | MAC vector | -0.4% | +0.6% | +0.2% | +0.9% | +5.9% | -| Starfish Speed | ML-DSA-44 | -0.6% | -0.2% | +0.4% | -0.2% | +51.0% | -| Sparse-Starfish-Speed | MAC vector | +0.3% | -0.3% | -0.2% | -2.9% | +4.4% | -| Sparse-Starfish-Speed | ML-DSA-44 | +0.6% | +0.4% | 0.0% | -2.7% | +57.8% | - -## Protocol relative to matching Starfish authentication - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish Speed | Ed25519 | -16.8% | -13.9% | -0.2% | -5.7% | +4.1% | -| Starfish Speed | MAC vector | -18.1% | -14.6% | 0.0% | +0.3% | +1.9% | -| Starfish Speed | ML-DSA-44 | -17.4% | -14.2% | +0.4% | -3.0% | +1.3% | -| Sparse-Starfish-Speed | Ed25519 | -24.5% | -19.6% | +0.2% | -4.6% | -8.2% | -| Sparse-Starfish-Speed | MAC vector | -25.2% | -21.0% | +0.1% | -2.3% | -11.3% | -| Sparse-Starfish-Speed | ML-DSA-44 | -24.1% | -19.4% | +0.4% | -4.3% | -6.6% | - -## Interpretation - -- Authentication did not materially change throughput or latency under the - emulated wide-area delays. Within each protocol, TPS differed by at most - 0.4%, block latency by at most 1.1%, and end-to-end latency by at most 1.4%. - These small changes are below what should be interpreted without repeated - trials and variance estimates. -- MAC vectors increased outbound bandwidth by 4.4-8.2% relative to Ed25519, - while ML-DSA-44 increased it by 51.0-57.8%. At the geo-limited block rate, - payload and protocol traffic dominate more of the total than in the - zero-latency runs, so ML-DSA's relative bandwidth multiplier is smaller. -- Starfish Speed reduced block latency by 16.8-18.1% and end-to-end latency by - 13.9-14.6% against matching plain-Starfish authentication, with essentially - identical TPS. -- Sparse-Starfish-Speed reduced block latency by 24.1-25.2%, end-to-end - latency by 19.4-21.0%, and outbound bandwidth by 6.6-11.3% against matching - plain-Starfish authentication, again with essentially identical TPS. -- All variants committed roughly 815-818 TPS from the offered 1,000 tx/s. - This experiment measures a local machine under injected network delay; it - does not establish capacity on physically distributed hardware. - -## Caveats - -- There is one run per configuration and no warm-up exclusion, so these are - directional comparisons rather than confidence intervals. -- The reported metrics were emitted before shutdown. Validator task abortion - logs expected `JoinError::Cancelled` messages afterward. Two runs also - printed a macOS `pthread lock` teardown error after their metrics; process - and benchmark-directory checks showed no active or overlapping benchmark. - The shutdown path should still be hardened before unattended batch runs. diff --git a/benchmark-results/2026-07-13-starfish-authentication-local.md b/benchmark-results/2026-07-13-starfish-authentication-local.md deleted file mode 100644 index b4d05f11..00000000 --- a/benchmark-results/2026-07-13-starfish-authentication-local.md +++ /dev/null @@ -1,104 +0,0 @@ -# Starfish authentication comparison — local Apple Silicon - -Date: 2026-07-13
-Source revision: `8a3bded` plus the Sparse authentication changes committed with this report
-Host: Apple Silicon (`arm64`), macOS 15.7.4
-Build: Rust 1.86.0, release profile - -## Configuration - -- 10 honest validators in one local process -- 1,000 tx/s offered load (100 tx/s per validator) -- 20-second measurement window -- Uniform 0 ms added network latency -- Default protocol dissemination mode (`push-useful` for all three protocols) -- One run per configuration - -Command template: - -```text -target/release/starfish local-benchmark \ - --committee-size 10 \ - --load 1000 \ - --consensus \ - --duration-secs 20 \ - --uniform-latency-ms 0 -``` - -## Results - -| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| Starfish | Ed25519 | 12.1 | 13.9 | 446.50 | 4,573.30 | 14.33 | 14.30 | 65.74 | -| Starfish | MAC vector | 12.8 | 14.8 | 433.60 | 4,494.05 | 14.04 | 14.01 | 66.34 | -| Starfish | ML-DSA-44 | 15.7 | 18.0 | 432.50 | 4,125.75 | 57.11 | 57.08 | 270.44 | -| Starfish Speed | Ed25519 | 10.2 | 12.3 | 442.75 | 4,201.30 | 14.61 | 14.58 | 67.60 | -| Starfish Speed | MAC vector | 9.4 | 11.4 | 439.15 | 4,114.10 | 14.12 | 14.09 | 65.85 | -| Starfish Speed | ML-DSA-44 | 13.4 | 17.4 | 453.80 | 2,868.60 | 39.84 | 39.82 | 179.82 | -| Sparse-Starfish-Speed | Ed25519 | 5.5 | 10.8 | 430.00 | 4,867.50 | 8.99 | 8.96 | 42.84 | -| Sparse-Starfish-Speed | MAC vector | 5.2 | 10.5 | 430.75 | 5,211.95 | 9.73 | 9.69 | 46.25 | -| Sparse-Starfish-Speed | ML-DSA-44 | 5.5 | 10.1 | 440.05 | 4,335.90 | 37.85 | 37.82 | 176.16 | - -## Relative to Ed25519 within each protocol - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish | MAC vector | +5.8% | +6.5% | -2.9% | -1.7% | -2.0% | -| Starfish | ML-DSA-44 | +29.8% | +29.5% | -3.1% | -9.8% | +298.5% | -| Starfish Speed | MAC vector | -7.8% | -7.3% | -0.8% | -2.1% | -3.4% | -| Starfish Speed | ML-DSA-44 | +31.4% | +41.5% | +2.5% | -31.7% | +172.7% | -| Sparse-Starfish-Speed | MAC vector | -5.5% | -2.8% | +0.2% | +7.1% | +8.2% | -| Sparse-Starfish-Speed | ML-DSA-44 | 0.0% | -6.5% | +2.3% | -10.9% | +321.0% | - -## Starfish Speed relative to Starfish - -| Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---:|---:|---:|---:|---:| -| Ed25519 | -15.7% | -11.5% | -0.8% | -8.1% | +2.0% | -| MAC vector | -26.6% | -23.0% | +1.3% | -8.5% | +0.6% | -| ML-DSA-44 | -14.6% | -3.3% | +4.9% | -30.5% | -30.2% | - -## Sparse-Starfish-Speed relative to Starfish - -| Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---:|---:|---:|---:|---:| -| Ed25519 | -54.5% | -22.3% | -3.7% | +6.4% | -37.3% | -| MAC vector | -59.4% | -29.1% | -0.7% | +16.0% | -30.7% | -| ML-DSA-44 | -65.0% | -43.9% | +1.7% | +5.1% | -33.7% | - -## Interpretation - -- The Starfish and Starfish Speed MAC variants remained close to Ed25519: TPS - was within 3%, BPS within 2.1%, and bandwidth was slightly lower in this - sample. The latency changes are small enough that repeated runs are needed - before treating their sign as meaningful. -- ML-DSA-44 materially increased latency and bandwidth. Its signature is 2,420 - bytes, versus 64 bytes for Ed25519. Outbound bandwidth increased about 4.0x - for Starfish, 2.7x for Starfish Speed, and 4.2x for Sparse-Starfish-Speed. -- Starfish Speed with ML-DSA-44 produced 31.7% fewer blocks than its Ed25519 - variant while committing 2.5% more transactions. This indicates more - transactions per block in this run; it should not be read as evidence that - ML-DSA improves throughput without repeated trials. -- Against matching Starfish authentication variants, Starfish Speed had lower - latency in all three samples and essentially equal TPS for Ed25519 and MAC. - Its ML-DSA-44 run used about 30% less bandwidth, alongside about 30% fewer - blocks, than Starfish ML-DSA-44. -- Sparse-Starfish-Speed preserved roughly the same TPS as plain Starfish while - reducing outbound bandwidth by 31-37% and block latency by 55-65% across the - three authentication schemes. Its lean headers therefore remain beneficial - with either signatures or MACs in this local sample. -- Sparse MAC remained close to Sparse Ed25519: TPS differed by 0.2%, while - outbound bandwidth was 8.2% higher. Sparse ML-DSA-44 used 4.2x the outbound - bandwidth of Sparse Ed25519 despite Sparse's lower protocol overhead. -- All variants achieved roughly 430–454 TPS from the 1,000 tx/s offered load. - Ten validators share one laptop and therefore contend for the same CPU, - storage, and network stack. These results are useful for directional local - comparison, not distributed capacity claims. - -## Benchmark validation note - -During the first Sparse MAC run, the receiver-side transport guard exposed a -round-gap response that still carried full MAC vectors. The sender now routes -round-gap blocks through the same recipient-tag preparation used by relay and -missing-parent paths. The rejected run was discarded; the Sparse MAC row above -is the clean rerun, which emitted no transport rejections. diff --git a/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md b/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md deleted file mode 100644 index bdc746ae..00000000 --- a/benchmark-results/2026-07-14-authentication-geo-60-validators-60s.md +++ /dev/null @@ -1,165 +0,0 @@ -# Authentication comparison — 60-validator geographic emulation - -Date: 2026-07-14
-Source revisions: `4553e5e` (original matrix), `3a82c04` (ML-DSA-65 -extension)
-Host: Apple Silicon (`arm64`), macOS 15.7.4
-Build: Rust 1.86.0, release profile - -## Configuration - -- 60 honest validators in one local process -- 600 tx/s aggregate offered load (exactly 10 tx/s per validator) -- 60-second measurement window -- Protocol-default dissemination: `push-useful` for the Starfish families and - `pull` for Bluestreak -- One run per configuration -- Geographic latency emulation enabled; no uniform-latency override - -The harness has ten AWS region profiles. Validator indices map to profiles -modulo ten, so this experiment models six validators per region. RTT values -are divided by two to obtain one-way delays and independent ±3% per-message -jitter is applied. Base one-way delays range from 0.5 ms to 154.5 ms. - -This is a single-machine latency emulation, not a 60-host deployment. The -validators share CPU, memory, storage, loopback networking, and kernel socket -resources. They form 3,540 directed peer relationships and use one RocksDB -instance each. - -Command template: - -```text -target/release/starfish local-benchmark \ - --committee-size 60 \ - --load 600 \ - --consensus \ - --duration-secs 60 -``` - -## Results - -| Protocol | Authentication | Block latency (ms) | E2E latency (ms) | TPS | BPS | Bandwidth out (MB/s) | Bandwidth in (MB/s) | Bandwidth efficiency | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| Starfish | Ed25519 | 664.45 | 728.77 | 444.30 | 515.02 | 8.59 | 8.58 | 39.59 | -| Starfish | MAC vector | 680.68 | 753.42 | 414.87 | 449.23 | 8.35 | 8.34 | 41.21 | -| Starfish | ML-DSA-44 | 849.38 | 937.02 | 427.77 | 417.95 | 9.72 | 9.71 | 46.53 | -| Starfish | ML-DSA-65† | 749.38 | 837.48 | 413.53 | 443.48 | 13.98 | 13.98 | 69.25 | -| Starfish Speed | Ed25519 | 592.13 | 673.87 | 460.37 | 467.12 | 6.63 | 6.62 | 29.49 | -| Starfish Speed | MAC vector | 670.80 | 783.93 | 430.95 | 439.30 | 7.30 | 7.30 | 34.70 | -| Starfish Speed | ML-DSA-44 | 641.70 | 726.03 | 433.63 | 461.57 | 11.06 | 11.05 | 52.22 | -| Starfish Speed | ML-DSA-65 | 641.40 | 727.28 | 421.98 | 415.88 | 12.10 | 12.09 | 58.71 | -| Sparse-Starfish-Speed | Ed25519 | 467.70 | 525.98 | 466.22 | 535.57 | 0.88 | 0.88 | 3.87 | -| Sparse-Starfish-Speed | MAC vector | 467.67 | 528.23 | 466.62 | 551.88 | 1.84 | 1.84 | 8.08 | -| Sparse-Starfish-Speed | ML-DSA-44 | 464.87 | 526.97 | 467.07 | 528.88 | 2.98 | 2.98 | 13.08 | -| Sparse-Starfish-Speed | ML-DSA-65 | 461.05 | 516.02 | 469.62 | 560.13 | 3.95 | 3.95 | 17.24 | -| Bluestreak | Ed25519 | 422.53 | 480.58 | 468.22 | 551.08 | 0.52 | 0.52 | 2.28 | -| Bluestreak | MAC vector | 421.62 | 480.37 | 467.63 | 545.72 | 1.47 | 1.46 | 6.42 | -| Bluestreak | ML-DSA-44 | 422.72 | 481.70 | 467.88 | 547.13 | 1.75 | 1.75 | 7.67 | -| Bluestreak | ML-DSA-65 | 423.98 | 482.82 | 470.25 | 548.02 | 2.22 | 2.22 | 9.66 | - -All sixteen commands exited successfully after emitting their metrics. The -Starfish ML-DSA-65 run marked † crossed this host's socket-buffer ceiling at -second 20: three writes reported `No buffer space available`, followed by -three decompress/deserialize warnings. Its metrics are retained as a -host-contended stress result, not a clean protocol comparison. The other -three ML-DSA-65 runs had no authentication, deserialize, socket-buffer, or -transport errors. - -## Warm-up-adjusted offered-rate utilization - -The transaction generators wait 13 seconds before submitting: the default -10-second delay plus 3 seconds for a 60-validator committee. The displayed TPS -uses the complete 60-second window, leaving approximately 47 active submission -seconds. The following values estimate the active rate as `TPS × 60 / 47`. - -| Protocol | Authentication | Estimated active TPS | Offered rate sustained | -|---|---|---:|---:| -| Starfish | Ed25519 | 567.19 | 94.5% | -| Starfish | MAC vector | 529.62 | 88.3% | -| Starfish | ML-DSA-44 | 546.09 | 91.0% | -| Starfish | ML-DSA-65† | 527.91 | 88.0% | -| Starfish Speed | Ed25519 | 587.71 | 98.0% | -| Starfish Speed | MAC vector | 550.15 | 91.7% | -| Starfish Speed | ML-DSA-44 | 553.57 | 92.3% | -| Starfish Speed | ML-DSA-65 | 538.70 | 89.8% | -| Sparse-Starfish-Speed | Ed25519 | 595.17 | 99.2% | -| Sparse-Starfish-Speed | MAC vector | 595.69 | 99.3% | -| Sparse-Starfish-Speed | ML-DSA-44 | 596.26 | 99.4% | -| Sparse-Starfish-Speed | ML-DSA-65 | 599.51 | 99.9% | -| Bluestreak | Ed25519 | 597.73 | 99.6% | -| Bluestreak | MAC vector | 596.97 | 99.5% | -| Bluestreak | ML-DSA-44 | 597.29 | 99.5% | -| Bluestreak | ML-DSA-65 | 600.32 | 100.1% | - -## Relative to Ed25519 within each protocol - -| Protocol | Authentication | Block latency | E2E latency | TPS | BPS | Bandwidth out | -|---|---|---:|---:|---:|---:|---:| -| Starfish | MAC vector | +2.4% | +3.4% | -6.6% | -12.8% | -2.8% | -| Starfish | ML-DSA-44 | +27.8% | +28.6% | -3.7% | -18.8% | +13.2% | -| Starfish | ML-DSA-65† | +12.8% | +14.9% | -6.9% | -13.9% | +62.7% | -| Starfish Speed | MAC vector | +13.3% | +16.3% | -6.4% | -6.0% | +10.1% | -| Starfish Speed | ML-DSA-44 | +8.4% | +7.7% | -5.8% | -1.2% | +66.8% | -| Starfish Speed | ML-DSA-65 | +8.3% | +7.9% | -8.3% | -11.0% | +82.5% | -| Sparse-Starfish-Speed | MAC vector | 0.0% | +0.4% | +0.1% | +3.0% | +109.1% | -| Sparse-Starfish-Speed | ML-DSA-44 | -0.6% | +0.2% | +0.2% | -1.2% | +238.6% | -| Sparse-Starfish-Speed | ML-DSA-65 | -1.4% | -1.9% | +0.7% | +4.6% | +348.9% | -| Bluestreak | MAC vector | -0.2% | 0.0% | -0.1% | -1.0% | +182.7% | -| Bluestreak | ML-DSA-44 | 0.0% | +0.2% | -0.1% | -0.7% | +236.5% | -| Bluestreak | ML-DSA-65 | +0.3% | +0.5% | +0.4% | -0.6% | +326.9% | - -Raw bandwidth can fall despite a larger authentication proof when a run -produces fewer blocks, as in plain Starfish MAC. The bandwidth-efficiency -metric—the ratio of bytes sent to committed transaction-payload bytes—rises -from 39.59 to 41.21 for MAC and 69.25 for ML-DSA-65, capturing the normalized -increase even when raw block production varies. - -## Interpretation - -- Sixty validators are feasible on this machine at a 600 tx/s aggregate - offered load, but headroom depends strongly on the protocol family. -- Sparse-Starfish-Speed and Bluestreak sustain 99.2-100.1% of the active - offered rate across all four authentication schemes. Within each family, - block latency varies by at most 1.4% and throughput by at most 0.7%. -- Plain Starfish sustains 88.0-94.5% of the active offered rate, while - Starfish Speed sustains 89.8-98.0%. Their authentication comparisons are - therefore partly measurements of shared-host contention. Plain Starfish - ML-DSA-65 is the clearest limit: its 13.98 MB/s full-mesh traffic triggered - the host's socket-buffer errors, so that row is not a clean protocol result. -- Bluestreak has the lowest latency and absolute bandwidth across every - authentication scheme: 422-424 ms block latency, 480-483 ms end-to-end - latency, and 0.52-2.22 MB/s outbound. -- Relative to matching Sparse-Starfish-Speed authentication, Bluestreak lowers - block latency by 8.0-9.8%, end-to-end latency by 6.4-9.1%, and outbound - bandwidth by 20.1-43.8%, with nearly identical throughput. -- At 60 validators, a full author MAC vector contains 60 × 32 = 1,920 bytes, - compared with a 64-byte Ed25519 signature and a 2,420-byte ML-DSA-44 - signature; an ML-DSA-65 signature is 3,309 bytes. ML-DSA-65 public keys are - 1,952 bytes and are provisioned in committee configuration rather than sent - in each block. Relays and synchronization responses for MAC blocks still - carry only one 32-byte recipient tag. Because Bluestreak and - Sparse-Starfish-Speed remove most other traffic, authentication bytes - produce large percentages while absolute bandwidth remains well below the - denser protocols. -- For repeatable all-protocol authentication comparisons on this single host, - 40 validators remains the safer configuration. Sixty validators is a useful - stress configuration and a clean operating point for the sparse and - Bluestreak families; denser families should move to multiple machines for - stronger conclusions. - -## Caveats - -- There is one sequential run per configuration, with no randomized order or - confidence interval. Host scheduling and thermal state can affect results. -- The ML-DSA-65 rows were appended after the original matrix on source - revision `3a82c04`. The marked plain-Starfish row encountered host transport - errors and should be treated only as evidence that this configuration - exceeds the local machine's clean operating point. -- The 600 tx/s load differs from the earlier 40-validator 1,000 tx/s matrix; - the two experiments should not be treated as a pure committee-size scaling - comparison. -- Six validators share each synthetic AWS region profile. The harness injects - the delay distribution but does not model independent machines or WAN - bandwidth constraints. -- Displayed TPS includes the 13-second startup delay. Warm-up-adjusted TPS is - derived rather than measured in a separately gated metrics window. diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index 2dbf4077..39afa7b3 100644 --- a/crates/orchestrator/README.md +++ b/crates/orchestrator/README.md @@ -129,7 +129,13 @@ each load generator submits a fixed load of 100 tx/s or more precisely 10 tx every 100ms. Performance measurements are collected by regularly scraping the Prometheus metrics exposed by the load generators. -Available consensus protocols: `starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-ml-dsa-65`, `starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, `starfish-speed-ml-dsa-65`, `sparse-starfish-speed`, `sparse-starfish-speed-mac`, `sparse-starfish-speed-ml-dsa-44`, `sparse-starfish-speed-ml-dsa-65`, `bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, `bluestreak-ml-dsa-65`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `cordial-miners`, `sailfish-pp`. +Available consensus protocols: `starfish`, `starfish-speed`, +`sparse-starfish-speed`, `bluestreak`, `starfish-bls`, `mysticeti`, +`mysticeti-bls`, `cordial-miners`, and `sailfish-pp`. Select the block signature +for any protocol with `--block-authentication ed25519|ml-dsa-44|ml-dsa-65`; +Ed25519 is the default. The `starfish-mac`, `starfish-speed-mac`, +`sparse-starfish-speed-mac`, and `bluestreak-mac` names are separate +experimental protocols and cannot be combined with that option. To run with Byzantine validators: diff --git a/crates/orchestrator/src/benchmark.rs b/crates/orchestrator/src/benchmark.rs index 68487b56..cec244f1 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -54,15 +54,8 @@ pub struct BenchmarkParametersGeneric { /// single VPC, they should use their internal IPs to avoid /// paying for data sent between the nodes. pub use_internal_ip_address: bool, - // Consensus protocol to deploy - // (starfish | starfish-mac | starfish-ml-dsa-44 | starfish-ml-dsa-65 | - // starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | - // starfish-speed-ml-dsa-65 | - // sparse-starfish-speed | sparse-starfish-speed-mac | - // sparse-starfish-speed-ml-dsa-44 | sparse-starfish-speed-ml-dsa-65 | - // bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | - // bluestreak-ml-dsa-65 | starfish-bls | mysticeti | mysticeti-bls | - // cordial-miners | sailfish-pp) + /// Consensus protocol to deploy. The block signature is configured in + /// `node_parameters`; the `*-mac` names denote experimental protocols. pub consensus_protocol: String, /// number Byzantine nodes pub byzantine_nodes: usize, diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index fc2cf3f6..ebd04de2 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -58,6 +58,11 @@ pub struct Opts { )] settings_path: String, + /// Block signature scheme used by every selected consensus protocol. + /// Defaults to Ed25519. Not applicable to experimental `*-mac` protocols. + #[clap(long, value_name = "ed25519|ml-dsa-44|ml-dsa-65", global = true)] + block_authentication: Option, + /// The type of operation to run. #[clap(subcommand)] operation: Operation, @@ -134,14 +139,9 @@ pub enum Operation { #[clap(long, action, default_value_t = false, global = true)] skip_testbed_configuration: bool, - /// Protocols to benchmark in order. Available options: - /// starfish | starfish-mac | starfish-ml-dsa-44 | starfish-ml-dsa-65 | - /// starfish-speed | starfish-speed-mac | starfish-speed-ml-dsa-44 | starfish-speed-ml-dsa-65 | - /// sparse-starfish-speed | sparse-starfish-speed-mac | - /// sparse-starfish-speed-ml-dsa-44 | sparse-starfish-speed-ml-dsa-65 | - /// bluestreak | bluestreak-mac | bluestreak-ml-dsa-44 | bluestreak-ml-dsa-65 | - /// starfish-bls | mysticeti | mysticeti-bls | - /// cordial-miners | sailfish-pp + /// Consensus protocols to benchmark in order. The `*-mac` names are + /// separate experimental protocols; signature schemes are selected + /// with `--block-authentication`. #[clap( long, value_name = "STRING", @@ -851,6 +851,7 @@ fn load_benchmark_configs( dissemination_mode: &Option, compress_network: Option, bls_workers: Option, + block_authentication: &Option, ) -> eyre::Result<(NodeParameters, ClientParameters)> { let mut node_parameters = match &settings.node_parameters_path { Some(path) => NodeParameters::load(path).wrap_err("Failed to load node's parameters")?, @@ -858,6 +859,9 @@ fn load_benchmark_configs( }; node_parameters.adversarial_latency = adversarial_latency; node_parameters.adversarial_latency_percent = adversarial_latency_percent; + if block_authentication.is_some() { + node_parameters.block_authentication = block_authentication.clone(); + } if let Some(workers) = bls_workers { node_parameters.bls_verification_workers = workers; } @@ -1037,6 +1041,7 @@ async fn run( .await .wrap_err("Failed to crate testbed")?; + let block_authentication = opts.block_authentication.clone(); match opts.operation { Operation::Testbed { action } => match action { // Display the current status of the testbed. @@ -1233,6 +1238,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1399,6 +1405,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1605,6 +1612,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1770,6 +1778,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1975,6 +1984,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -2283,6 +2293,8 @@ mod tests { let opts = Opts::try_parse_from([ "orchestrator", "benchmark", + "--block-authentication", + "ml-dsa-65", "--protocols", "starfish", "mysticeti", @@ -2294,6 +2306,8 @@ mod tests { ]) .unwrap(); + assert_eq!(opts.block_authentication.as_deref(), Some("ml-dsa-65")); + match opts.operation { Operation::Benchmark { protocols, diff --git a/crates/starfish-core/src/config.rs b/crates/starfish-core/src/config.rs index 625d5f4b..e1203010 100644 --- a/crates/starfish-core/src/config.rs +++ b/crates/starfish-core/src/config.rs @@ -57,6 +57,10 @@ pub struct NodeParameters { pub bls_verification_workers: usize, #[serde(default)] pub dissemination_mode: DisseminationMode, + /// Block signature scheme. `None` selects Ed25519. Experimental MAC + /// protocols select their authentication through the consensus name. + #[serde(default)] + pub block_authentication: Option, #[serde(default = "node_defaults::default_causal_push_shard_round_lag")] pub causal_push_shard_round_lag: RoundNumber, #[serde( @@ -130,6 +134,7 @@ impl Default for NodeParameters { compress_network: node_defaults::default_compress_network(), bls_verification_workers: node_defaults::default_bls_verification_workers(), dissemination_mode: DisseminationMode::default(), + block_authentication: None, causal_push_shard_round_lag: node_defaults::default_causal_push_shard_round_lag(), enable_strong_vote_adaptive_acknowledgments: node_defaults::default_enable_strong_vote_adaptive_acknowledgments(), diff --git a/crates/starfish-core/src/dag_state.rs b/crates/starfish-core/src/dag_state.rs index 39b04008..979eeab3 100644 --- a/crates/starfish-core/src/dag_state.rs +++ b/crates/starfish-core/src/dag_state.rs @@ -289,30 +289,53 @@ pub struct ProtocolConfig { impl ProtocolConfig { pub fn from_str(value: &str) -> Result { - let (protocol_name, block_authentication_scheme) = [ - ("-ml-dsa-65", BlockAuthenticationScheme::MlDsa65), - ("-ml-dsa-44", BlockAuthenticationScheme::MlDsa44), - ("-mac", BlockAuthenticationScheme::MacVector), - ] - .into_iter() - .find_map(|(suffix, scheme)| value.strip_suffix(suffix).map(|base| (base, scheme))) - .unwrap_or((value, BlockAuthenticationScheme::Ed25519)); + Self::from_selection(value, None) + } + pub fn from_selection( + consensus: &str, + block_authentication: Option<&str>, + ) -> Result { + let (protocol_name, is_mac_experiment) = consensus + .strip_suffix("-mac") + .map(|base| (base, true)) + .unwrap_or((consensus, false)); let consensus_protocol = ConsensusProtocol::from_known_str(protocol_name) - .ok_or_else(|| format!("Unknown consensus protocol '{value}'"))?; - if block_authentication_scheme != BlockAuthenticationScheme::Ed25519 - && !matches!( + .ok_or_else(|| format!("Unknown consensus protocol '{consensus}'"))?; + + let block_authentication_scheme = if is_mac_experiment { + if block_authentication.is_some() { + return Err(format!( + "'{consensus}' is an experimental MAC protocol and cannot be combined with \ + --block-authentication" + )); + } + if !matches!( consensus_protocol, ConsensusProtocol::Starfish | ConsensusProtocol::StarfishSpeed | ConsensusProtocol::SparseStarfishSpeed | ConsensusProtocol::Bluestreak - ) - { - return Err(format!( - "Block authentication variants are not supported for '{protocol_name}'" - )); - } + ) { + return Err(format!( + "The experimental MAC protocol is not available for '{protocol_name}'" + )); + } + BlockAuthenticationScheme::MacVector + } else { + match block_authentication.unwrap_or("ed25519") { + "ed25519" => BlockAuthenticationScheme::Ed25519, + "ml-dsa-44" => BlockAuthenticationScheme::MlDsa44, + "ml-dsa-65" => BlockAuthenticationScheme::MlDsa65, + value => { + return Err(format!( + "Unknown block authentication scheme '{value}'. Use 'ed25519', \ + 'ml-dsa-44', or 'ml-dsa-65'." + )); + } + } + }; + Ok(Self { consensus_protocol, block_authentication_scheme, @@ -551,6 +574,32 @@ impl DagState { storage_backend: &StorageBackend, strong_vote_adaptive_acknowledgments: bool, dissemination_mode: DisseminationMode, + ) -> RecoveredState { + let protocol_config = ProtocolConfig::from_str(&consensus).expect("validated protocol"); + Self::open_with_protocol_config( + authority, + path, + metrics, + committee, + byzantine_strategy, + protocol_config, + storage_backend, + strong_vote_adaptive_acknowledgments, + dissemination_mode, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn open_with_protocol_config( + authority: AuthorityIndex, + path: impl AsRef, + metrics: Arc, + committee: Arc, + byzantine_strategy: String, + protocol_config: ProtocolConfig, + storage_backend: &StorageBackend, + strong_vote_adaptive_acknowledgments: bool, + dissemination_mode: DisseminationMode, ) -> RecoveredState { assert!( committee.len() <= crate::types::MAX_COMMITTEE_SIZE as usize, @@ -574,7 +623,6 @@ impl DagState { Arc::new(RocksStore::open(&path).expect("Failed to open RocksDB")) } }; - let protocol_config = ProtocolConfig::from_str(&consensus).expect("validated protocol"); let consensus_protocol = protocol_config.consensus_protocol; let resolved_dissemination = consensus_protocol.resolve_dissemination_mode(dissemination_mode); @@ -4919,122 +4967,60 @@ mod tests { #[test] fn protocol_config_selects_block_authentication() { - assert_eq!( - ProtocolConfig::from_str("starfish").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Starfish, - block_authentication_scheme: BlockAuthenticationScheme::Ed25519, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-mac").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Starfish, - block_authentication_scheme: BlockAuthenticationScheme::MacVector, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-ml-dsa-44").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Starfish, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-ml-dsa-65").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Starfish, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-speed").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::StarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::Ed25519, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-speed-mac").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::StarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MacVector, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-speed-ml-dsa-44").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::StarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, - } - ); - assert_eq!( - ProtocolConfig::from_str("starfish-speed-ml-dsa-65").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::StarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, - } - ); - assert_eq!( - ProtocolConfig::from_str("sparse-starfish-speed").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::Ed25519, - } - ); - assert_eq!( - ProtocolConfig::from_str("sparse-starfish-speed-mac").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MacVector, - } - ); - assert_eq!( - ProtocolConfig::from_str("sparse-starfish-speed-ml-dsa-44").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, - } - ); - assert_eq!( - ProtocolConfig::from_str("sparse-starfish-speed-ml-dsa-65").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::SparseStarfishSpeed, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, - } - ); - assert_eq!( - ProtocolConfig::from_str("bluestreak").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Bluestreak, - block_authentication_scheme: BlockAuthenticationScheme::Ed25519, - } - ); - assert_eq!( - ProtocolConfig::from_str("bluestreak-mac").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Bluestreak, - block_authentication_scheme: BlockAuthenticationScheme::MacVector, - } - ); - assert_eq!( - ProtocolConfig::from_str("bluestreak-ml-dsa-44").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Bluestreak, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa44, - } - ); - assert_eq!( - ProtocolConfig::from_str("bluestreak-ml-dsa-65").unwrap(), - ProtocolConfig { - consensus_protocol: ConsensusProtocol::Bluestreak, - block_authentication_scheme: BlockAuthenticationScheme::MlDsa65, + let protocols = [ + ("mysticeti", ConsensusProtocol::Mysticeti), + ("cordial-miners", ConsensusProtocol::CordialMiners), + ("starfish", ConsensusProtocol::Starfish), + ("starfish-speed", ConsensusProtocol::StarfishSpeed), + ("starfish-bls", ConsensusProtocol::StarfishBls), + ("sailfish-pp", ConsensusProtocol::SailfishPlusPlus), + ("bluestreak", ConsensusProtocol::Bluestreak), + ("mysticeti-bls", ConsensusProtocol::MysticetiBls), + ( + "sparse-starfish-speed", + ConsensusProtocol::SparseStarfishSpeed, + ), + ]; + let signature_schemes = [ + (None, BlockAuthenticationScheme::Ed25519), + (Some("ed25519"), BlockAuthenticationScheme::Ed25519), + (Some("ml-dsa-44"), BlockAuthenticationScheme::MlDsa44), + (Some("ml-dsa-65"), BlockAuthenticationScheme::MlDsa65), + ]; + + for (name, consensus_protocol) in protocols { + for (selection, block_authentication_scheme) in signature_schemes { + assert_eq!( + ProtocolConfig::from_selection(name, selection).unwrap(), + ProtocolConfig { + consensus_protocol, + block_authentication_scheme, + } + ); } - ); - assert!(ProtocolConfig::from_str("mysticeti-ml-dsa-65").is_err()); + } + + for (name, consensus_protocol) in [ + ("starfish-mac", ConsensusProtocol::Starfish), + ("starfish-speed-mac", ConsensusProtocol::StarfishSpeed), + ( + "sparse-starfish-speed-mac", + ConsensusProtocol::SparseStarfishSpeed, + ), + ("bluestreak-mac", ConsensusProtocol::Bluestreak), + ] { + assert_eq!( + ProtocolConfig::from_str(name).unwrap(), + ProtocolConfig { + consensus_protocol, + block_authentication_scheme: BlockAuthenticationScheme::MacVector, + } + ); + assert!(ProtocolConfig::from_selection(name, Some("ed25519")).is_err()); + } + + assert!(ProtocolConfig::from_str("mysticeti-mac").is_err()); + assert!(ProtocolConfig::from_selection("starfish", Some("unknown")).is_err()); assert!(ProtocolConfig::from_str("starfish-unknown").is_err()); - assert!(ProtocolConfig::from_str("starfish-speed-unknown").is_err()); - assert!(ProtocolConfig::from_str("sparse-starfish-speed-unknown").is_err()); - assert!(ProtocolConfig::from_str("bluestreak-unknown").is_err()); } } diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index 0b234c4c..ec9942eb 100644 --- a/crates/starfish-core/src/net_sync.rs +++ b/crates/starfish-core/src/net_sync.rs @@ -59,8 +59,8 @@ const SAILFISH_CERT_BATCH_MAX_LEN: usize = 256; /// Enforce the MAC experiment's transport contract before cryptographic /// verification: /// -/// - a full vector is accepted only on proactive block streaming directly -/// from the block's claimed author; +/// - a full vector is accepted only on proactive block streaming directly from +/// the block's claimed author; /// - every relay and synchronization path must carry one recipient tag; /// - a direct author stream must carry the full vector, so recipients retain /// the material needed for one-hop relay. diff --git a/crates/starfish-core/src/validator.rs b/crates/starfish-core/src/validator.rs index a7d3a9f2..d9e0e510 100644 --- a/crates/starfish-core/src/validator.rs +++ b/crates/starfish-core/src/validator.rs @@ -45,7 +45,11 @@ impl Validator { byzantine_strategy: String, consensus: String, ) -> Result { - let protocol_config = ProtocolConfig::from_str(&consensus).map_err(|error| eyre!(error))?; + let protocol_config = ProtocolConfig::from_selection( + &consensus, + public_config.parameters.block_authentication.as_deref(), + ) + .map_err(|error| eyre!(error))?; match protocol_config.block_authentication_scheme { BlockAuthenticationScheme::Ed25519 => { if committee.get_public_key(authority) != Some(&private_config.keypair.public_key()) @@ -135,13 +139,13 @@ impl Validator { // Open the DAG state. let rocks_path = private_config.rocksdb(); - let recovered = DagState::open( + let recovered = DagState::open_with_protocol_config( authority, rocks_path, metrics.clone(), committee.clone(), byzantine_strategy, - consensus, + protocol_config, ¶meters.storage_backend, public_config .parameters @@ -295,11 +299,16 @@ mod smoke_tests { } } - async fn run_commit_test(consensus: &str, port_offset: u16) { + async fn run_commit_test( + consensus: &str, + block_authentication: Option<&str>, + port_offset: u16, + ) { let committee_size = 4; let committee = Committee::new_for_benchmarks(committee_size); - let public_config = + let mut public_config = NodePublicConfig::new_for_tests(committee_size).with_port_offset(port_offset); + public_config.parameters.block_authentication = block_authentication.map(str::to_string); let parameters = Parameters::default(); let dir = TempDir::new().unwrap(); @@ -343,42 +352,52 @@ mod smoke_tests { } } - #[test_case("mysticeti", 0)] - #[test_case("cordial-miners", 40)] - #[test_case("starfish", 60)] - #[test_case("starfish-mac", 700)] - #[test_case("starfish-ml-dsa-44", 720)] - #[test_case("starfish-ml-dsa-65", 1000)] - #[test_case("starfish-speed", 80)] - #[test_case("starfish-speed-mac", 760)] - #[test_case("starfish-speed-ml-dsa-44", 780)] - #[test_case("starfish-speed-ml-dsa-65", 1040)] - #[test_case("starfish-bls", 100)] - #[test_case("sailfish++", 120)] - #[test_case("bluestreak", 140)] - #[test_case("mysticeti-bls", 160)] - #[test_case("sparse-starfish-speed", 180)] - #[test_case("sparse-starfish-speed-mac", 840)] - #[test_case("sparse-starfish-speed-ml-dsa-44", 860)] - #[test_case("sparse-starfish-speed-ml-dsa-65", 1080)] - #[test_case("bluestreak-mac", 920)] - #[test_case("bluestreak-ml-dsa-44", 940)] - #[test_case("bluestreak-ml-dsa-65", 1120)] + #[test_case("mysticeti", None, 0)] + #[test_case("mysticeti", Some("ml-dsa-65"), 1280)] + #[test_case("cordial-miners", None, 40)] + #[test_case("cordial-miners", Some("ml-dsa-65"), 1300)] + #[test_case("starfish", None, 60)] + #[test_case("starfish-mac", None, 700)] + #[test_case("starfish", Some("ml-dsa-44"), 720)] + #[test_case("starfish", Some("ml-dsa-65"), 1000)] + #[test_case("starfish-speed", None, 80)] + #[test_case("starfish-speed-mac", None, 760)] + #[test_case("starfish-speed", Some("ml-dsa-44"), 780)] + #[test_case("starfish-speed", Some("ml-dsa-65"), 1040)] + #[test_case("starfish-bls", None, 100)] + #[test_case("starfish-bls", Some("ml-dsa-65"), 1320)] + #[test_case("sailfish++", None, 120)] + #[test_case("sailfish++", Some("ml-dsa-65"), 1340)] + #[test_case("bluestreak", None, 140)] + #[test_case("mysticeti-bls", None, 160)] + #[test_case("mysticeti-bls", Some("ml-dsa-65"), 1360)] + #[test_case("sparse-starfish-speed", None, 180)] + #[test_case("sparse-starfish-speed-mac", None, 840)] + #[test_case("sparse-starfish-speed", Some("ml-dsa-44"), 860)] + #[test_case("sparse-starfish-speed", Some("ml-dsa-65"), 1080)] + #[test_case("bluestreak-mac", None, 920)] + #[test_case("bluestreak", Some("ml-dsa-44"), 940)] + #[test_case("bluestreak", Some("ml-dsa-65"), 1120)] #[tokio::test] - async fn validator_commit(consensus: &str, port_offset: u16) { - run_commit_test(consensus, port_offset).await; + async fn validator_commit( + consensus: &str, + block_authentication: Option<&str>, + port_offset: u16, + ) { + run_commit_test(consensus, block_authentication, port_offset).await; } #[tokio::test] async fn validator_commit_bluestreak_basic() { - run_commit_test("bluestreak", 150).await; + run_commit_test("bluestreak", None, 150).await; } - async fn run_sync_test(consensus: &str, port_offset: u16) { + async fn run_sync_test(consensus: &str, block_authentication: Option<&str>, port_offset: u16) { let committee_size = 4; let committee = Committee::new_for_benchmarks(committee_size); - let public_config = + let mut public_config = NodePublicConfig::new_for_tests(committee_size).with_port_offset(port_offset); + public_config.parameters.block_authentication = block_authentication.map(str::to_string); let parameters = Parameters::default(); let dir = TempDir::new().unwrap(); @@ -455,30 +474,30 @@ mod smoke_tests { } } - #[test_case("mysticeti", 100)] - #[test_case("cordial-miners", 140)] - #[test_case("starfish", 160)] - #[test_case("starfish-mac", 740)] - #[test_case("starfish-ml-dsa-44", 1020)] - #[test_case("starfish-ml-dsa-65", 1200)] - #[test_case("starfish-speed", 180)] - #[test_case("starfish-speed-mac", 800)] - #[test_case("starfish-speed-ml-dsa-44", 820)] - #[test_case("starfish-speed-ml-dsa-65", 1220)] - #[test_case("starfish-bls", 200)] - #[test_case("sailfish++", 220)] - #[test_case("bluestreak", 260)] - #[test_case("mysticeti-bls", 280)] - #[test_case("sparse-starfish-speed", 320)] - #[test_case("sparse-starfish-speed-mac", 880)] - #[test_case("sparse-starfish-speed-ml-dsa-44", 900)] - #[test_case("sparse-starfish-speed-ml-dsa-65", 1240)] - #[test_case("bluestreak-mac", 960)] - #[test_case("bluestreak-ml-dsa-44", 980)] - #[test_case("bluestreak-ml-dsa-65", 1260)] + #[test_case("mysticeti", None, 100)] + #[test_case("cordial-miners", None, 140)] + #[test_case("starfish", None, 160)] + #[test_case("starfish-mac", None, 740)] + #[test_case("starfish", Some("ml-dsa-44"), 1020)] + #[test_case("starfish", Some("ml-dsa-65"), 1200)] + #[test_case("starfish-speed", None, 180)] + #[test_case("starfish-speed-mac", None, 800)] + #[test_case("starfish-speed", Some("ml-dsa-44"), 820)] + #[test_case("starfish-speed", Some("ml-dsa-65"), 1220)] + #[test_case("starfish-bls", None, 200)] + #[test_case("sailfish++", None, 220)] + #[test_case("bluestreak", None, 260)] + #[test_case("mysticeti-bls", None, 280)] + #[test_case("sparse-starfish-speed", None, 320)] + #[test_case("sparse-starfish-speed-mac", None, 880)] + #[test_case("sparse-starfish-speed", Some("ml-dsa-44"), 900)] + #[test_case("sparse-starfish-speed", Some("ml-dsa-65"), 1240)] + #[test_case("bluestreak-mac", None, 960)] + #[test_case("bluestreak", Some("ml-dsa-44"), 980)] + #[test_case("bluestreak", Some("ml-dsa-65"), 1260)] #[tokio::test] - async fn validator_sync(consensus: &str, port_offset: u16) { - run_sync_test(consensus, port_offset).await; + async fn validator_sync(consensus: &str, block_authentication: Option<&str>, port_offset: u16) { + run_sync_test(consensus, block_authentication, port_offset).await; } async fn run_crash_faults_test(consensus: &str, port_offset: u16) { diff --git a/crates/starfish/src/main.rs b/crates/starfish/src/main.rs index ee02e1c5..73a6288c 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -65,10 +65,13 @@ enum Operation { parameters_path: String, #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, - /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). + /// Consensus protocol. The `*-mac` names are experimental protocols. #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, + /// Block signature scheme. Defaults to Ed25519 and is not applicable + /// to the experimental `*-mac` protocols. + #[clap(long, value_name = "ed25519|ml-dsa-44|ml-dsa-65")] + block_authentication: Option, }, /// Deploy a local validator for test. Dryrun mode uses /// default keys and committee configurations. @@ -95,10 +98,13 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). + /// Consensus protocol. The `*-mac` names are experimental protocols. #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, + /// Block signature scheme. Defaults to Ed25519 and is not applicable + /// to the experimental `*-mac` protocols. + #[clap(long, value_name = "ed25519|ml-dsa-44|ml-dsa-65")] + block_authentication: Option, /// Directory to store validator data (default: current directory) #[clap(long, value_name = "PATH")] data_dir: Option, @@ -147,10 +153,13 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, - /// Consensus/authentication variant (for example `starfish-mac`, - /// `bluestreak-mac`, or `sparse-starfish-speed-ml-dsa-65`). + /// Consensus protocol. The `*-mac` names are experimental protocols. #[clap(long, value_name = "STRING", default_value = "starfish")] consensus: String, + /// Block signature scheme. Defaults to Ed25519 and is not applicable + /// to the experimental `*-mac` protocols. + #[clap(long, value_name = "ed25519|ml-dsa-44|ml-dsa-65")] + block_authentication: Option, #[clap(long, value_name = "INT", default_value_t = 600)] duration_secs: u64, /// Dissemination mode override: @@ -184,6 +193,7 @@ async fn main() -> Result<()> { parameters_path, byzantine_strategy, consensus: consensus_protocol, + block_authentication, } => { run( authority, @@ -193,6 +203,7 @@ async fn main() -> Result<()> { parameters_path, byzantine_strategy, consensus_protocol, + block_authentication, ) .await? } @@ -206,6 +217,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus: consensus_protocol, + block_authentication, data_dir, base_ip, storage_backend, @@ -224,6 +236,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus_protocol, + block_authentication, data_dir, base_ip, storage_backend, @@ -244,6 +257,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus: consensus_protocol, + block_authentication, duration_secs, dissemination_mode, } => { @@ -253,6 +267,7 @@ async fn main() -> Result<()> { } node_parameters.adversarial_latency = adversarial_latency; node_parameters.adversarial_latency_percent = adversarial_latency_percent; + node_parameters.block_authentication = block_authentication; if let Some(ref mode) = dissemination_mode { node_parameters.dissemination_mode = parse_dissemination_mode(mode)?; } @@ -345,6 +360,17 @@ async fn local_benchmark( } println!("Transaction Load: {load} tx/s"); println!("Consensus Protocol: {consensus_protocol}"); + println!( + "Block Authentication: {}", + if consensus_protocol.ends_with("-mac") { + "mac-vector (experimental)" + } else { + node_parameters + .block_authentication + .as_deref() + .unwrap_or("ed25519") + } + ); if let Some(latency) = node_parameters.uniform_latency_ms { println!("Network Latency: {latency} ms (uniform)"); } else { @@ -517,14 +543,18 @@ async fn run( parameters_path: String, byzantine_strategy: String, consensus_protocol: String, + block_authentication: Option, ) -> Result<()> { tracing::info!("Starting node {authority}"); let committee = Committee::load(&committee_path) .wrap_err(format!("Failed to load committee file '{committee_path}'"))?; - let public_config = NodePublicConfig::load(&public_config_path).wrap_err(format!( + let mut public_config = NodePublicConfig::load(&public_config_path).wrap_err(format!( "Failed to load parameters file '{public_config_path}'" ))?; + if block_authentication.is_some() { + public_config.parameters.block_authentication = block_authentication; + } let private_config = NodePrivateConfig::load(&private_config_path).wrap_err(format!( "Failed to load private configuration file '{private_config_path}'" ))?; @@ -561,6 +591,7 @@ async fn dryrun( adversarial_latency: bool, adversarial_latency_percent: u32, consensus_protocol: String, + block_authentication: Option, data_dir: Option, base_ip: Option, storage_backend: Option, @@ -602,6 +633,7 @@ async fn dryrun( node_parameters.adversarial_latency = adversarial_latency; node_parameters.adversarial_latency_percent = adversarial_latency_percent; node_parameters.compress_network = compress_network; + node_parameters.block_authentication = block_authentication; if let Some(workers) = bls_workers { node_parameters.bls_verification_workers = workers; } @@ -710,7 +742,9 @@ pub fn default_table_format() -> format::TableFormat { mod tests { use std::net::Ipv4Addr; - use super::ipv4_add_offset; + use clap::Parser; + + use super::{Args, Operation, ipv4_add_offset}; #[test] fn ipv4_add_offset_crosses_octet_boundary() { @@ -726,4 +760,32 @@ mod tests { let base = Ipv4Addr::new(255, 255, 255, 255); assert!(ipv4_add_offset(base, 1).is_err()); } + + #[test] + fn dry_run_parses_block_authentication_separately_from_consensus() { + let args = Args::try_parse_from([ + "starfish", + "dry-run", + "--authority", + "0", + "--committee-size", + "4", + "--consensus", + "mysticeti", + "--block-authentication", + "ml-dsa-65", + ]) + .unwrap(); + + let Operation::DryRun { + consensus, + block_authentication, + .. + } = args.operation + else { + panic!("expected dry-run operation"); + }; + assert_eq!(consensus, "mysticeti"); + assert_eq!(block_authentication.as_deref(), Some("ml-dsa-65")); + } } diff --git a/local-dryrun/README.md b/local-dryrun/README.md index 8b4e0b7b..5b568c2e 100644 --- a/local-dryrun/README.md +++ b/local-dryrun/README.md @@ -30,18 +30,16 @@ NUM_NODES=10 DESIRED_TPS=1000 CONSENSUS=starfish \ | `NUM_CRASHED_NODES` | `0` | Number of highest-numbered authorities to leave down from startup | | `DESIRED_TPS` | `1000` | Target transactions per second (split evenly across nodes) | | `CONSENSUS` | `bluestreak` | Consensus protocol (see below) | +| `BLOCK_AUTHENTICATION` | *(unset)* | `ed25519`, `ml-dsa-44`, or `ml-dsa-65`; unset defaults to Ed25519 | | `TEST_TIME` | `3000` | Experiment duration in seconds | -Supported `CONSENSUS` values include the Ed25519, MAC, ML-DSA-44, and ML-DSA-65 variants -of Starfish, Starfish Speed, Sparse-Starfish-Speed, and Bluestreak: -`starfish`, `starfish-mac`, `starfish-ml-dsa-44`, `starfish-ml-dsa-65`, -`starfish-speed`, `starfish-speed-mac`, `starfish-speed-ml-dsa-44`, -`starfish-speed-ml-dsa-65`, -`sparse-starfish-speed`, `sparse-starfish-speed-mac`, -`sparse-starfish-speed-ml-dsa-44`, `sparse-starfish-speed-ml-dsa-65`, -`bluestreak`, `bluestreak-mac`, `bluestreak-ml-dsa-44`, -`bluestreak-ml-dsa-65`, `starfish-bls`, `cordial-miners`, `mysticeti`, -`sailfish-pp`, and `mysticeti-bls`. +Supported `CONSENSUS` values are `starfish`, `starfish-speed`, +`sparse-starfish-speed`, `bluestreak`, `starfish-bls`, `cordial-miners`, +`mysticeti`, `sailfish-pp`, and `mysticeti-bls`. `BLOCK_AUTHENTICATION` +selects Ed25519, ML-DSA-44, or ML-DSA-65 for any of them. The +`starfish-mac`, `starfish-speed-mac`, `sparse-starfish-speed-mac`, and +`bluestreak-mac` names are separate experimental protocols; leave +`BLOCK_AUTHENTICATION` unset when using one. ### Protocol Tuning diff --git a/local-dryrun/dryrun.sh b/local-dryrun/dryrun.sh index 117404d1..3ca409f0 100755 --- a/local-dryrun/dryrun.sh +++ b/local-dryrun/dryrun.sh @@ -6,11 +6,10 @@ NUM_NODES=${NUM_NODES:-10} NUM_CRASHED_NODES=${NUM_CRASHED_NODES:-0} DESIRED_TPS=${DESIRED_TPS:-100} -# Authentication variants append -mac, -ml-dsa-44, or -ml-dsa-65 to starfish, -# starfish-speed, sparse-starfish-speed, or bluestreak. -# Other options: starfish-bls, cordial-miners, mysticeti, -# sailfish-pp, mysticeti-bls +# Signature schemes: ed25519 (default), ml-dsa-44, ml-dsa-65. +# The *-mac names remain separate experimental protocols. CONSENSUS=${CONSENSUS:- sparse-starfish-speed} +BLOCK_AUTHENTICATION=${BLOCK_AUTHENTICATION:-} NUM_BYZANTINE_NODES=${NUM_BYZANTINE_NODES:-0} # Options: timeout-leader, leader-withholding, # equivocating-chains, equivocating-two-chains, @@ -574,6 +573,10 @@ EOH if [ "${COMPRESS_NETWORK:-0}" = 1 ]; then PARAM_FLAGS+=" --compress-network" fi + if [ -n "$BLOCK_AUTHENTICATION" ]; then + PARAM_FLAGS+=" --block-authentication" + PARAM_FLAGS+=" $BLOCK_AUTHENTICATION" + fi cat < Date: Tue, 14 Jul 2026 19:07:21 +0200 Subject: [PATCH 19/19] Fix dependency policy checks for ML-DSA --- deny.toml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 3f76a6c3..8ce45edd 100644 --- a/deny.toml +++ b/deny.toml @@ -28,7 +28,7 @@ allow = [ "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0", - "GPL-2.0", + "GPL-2.0-or-later", "ISC", "LicenseRef-ring", "MIT", @@ -63,6 +63,14 @@ name = "minibytes" expression = "MIT" license-files = [{ path = "LICENSE", hash = 0x6b2d3210 }] +# bloom 0.3.2 uses the deprecated SPDX identifier `GPL-2.0`; its license +# grants redistribution under GPL version 2 or any later version. +[[licenses.clarify]] +name = "bloom" +version = "0.3.2" +expression = "GPL-2.0-or-later" +license-files = [{ path = "LICENSE", hash = 0xeaa66bfd }] + [licenses.private] # If true, ignores workspace crates that aren't published, or are only # published to private registries. @@ -90,6 +98,10 @@ skip = [ # tidehunter uses prometheus 0.14 (protobuf 3.x), starfish-core uses 0.13 (protobuf 2.x) { name = "prometheus", version = "0.13" }, { name = "protobuf", version = "2" }, + # ml-dsa 0.1 uses RustCrypto digest 0.11 and crypto-common 0.2 while + # existing workspace dependencies still require their previous releases. + { name = "crypto-common", version = "0.2" }, + { name = "digest", version = "0.11" }, ] skip-tree = [ # aws-smithy-http-client depends on both hyper 0.14 and 1.x,