diff --git a/Cargo.lock b/Cargo.lock index 33c4e14..fc80648 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 8afdc05..5b2db96 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,55 @@ achieving 2-round optimistic commit latency. leader, data availability) in block headers, with async verification offloaded from the critical path. +### 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 +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. + +#### 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 Every protocol can run with any of three dissemination strategies diff --git a/crates/orchestrator/README.md b/crates/orchestrator/README.md index f441425..39afa7b 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-speed`, `sparse-starfish-speed`, `starfish-bls`, `mysticeti`, `mysticeti-bls`, `bluestreak`, `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 4f8d84f..cec244f 100644 --- a/crates/orchestrator/src/benchmark.rs +++ b/crates/orchestrator/src/benchmark.rs @@ -54,9 +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-speed | sparse-starfish-speed | starfish-bls | - // mysticeti | mysticeti-bls | cordial-miners | bluestreak | 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 eec38ca..ebd04de 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,10 +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-speed | sparse-starfish-speed | - /// starfish-bls | mysticeti | mysticeti-bls | - /// cordial-miners | bluestreak | 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", @@ -847,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")?, @@ -854,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; } @@ -1033,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. @@ -1229,6 +1238,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1395,6 +1405,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1601,6 +1612,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1766,6 +1778,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -1971,6 +1984,7 @@ async fn run( &dissemination_mode, compress_network, resolved_bls_workers.override_workers, + &block_authentication, )?; display::newline(); @@ -2279,6 +2293,8 @@ mod tests { let opts = Opts::try_parse_from([ "orchestrator", "benchmark", + "--block-authentication", + "ml-dsa-65", "--protocols", "starfish", "mysticeti", @@ -2290,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/Cargo.toml b/crates/starfish-core/Cargo.toml index fc9fd95..0a6332f 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_manager.rs b/crates/starfish-core/src/block_manager.rs index f05e8d5..4b34b86 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/broadcaster.rs b/crates/starfish-core/src/broadcaster.rs index d7770fc..8db012c 100644 --- a/crates/starfish-core/src/broadcaster.rs +++ b/crates/starfish-core/src/broadcaster.rs @@ -21,7 +21,7 @@ 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, @@ -430,6 +430,11 @@ where .inner .dag_state .get_transmission_parts(&refs_to_send, &refs_to_send); + let headers = prepare_forwarded_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() { @@ -466,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 @@ -1308,6 +1318,7 @@ where fn materialize_push_batch( inner: &Arc>, + to_whom_authority_index: AuthorityIndex, plan: PushBatchParts, ) -> BlockBatch where @@ -1317,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, @@ -1337,6 +1353,11 @@ where let (headers, shards) = inner .dag_state .get_transmission_parts(&plan.other_refs, &plan.shard_refs); + let headers = prepare_forwarded_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 +1438,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 +1547,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, BlockAuthenticationScheme}, + }; fn holder_set(authorities: &[AuthorityIndex]) -> StakeAggregator { let committee = Committee::new_test(vec![1, 1, 1, 1]); @@ -1574,4 +1599,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_forwarded_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_forwarded_blocks_for_peer(BlockAuthenticationScheme::MacVector, 3, relayed); + assert!(second_hop.is_empty()); + } } diff --git a/crates/starfish-core/src/committee.rs b/crates/starfish-core/src/committee.rs index fe8a3bc..8518d75 100644 --- a/crates/starfish-core/src/committee.rs +++ b/crates/starfish-core/src/committee.rs @@ -10,7 +10,11 @@ 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, 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}, }; @@ -143,6 +147,18 @@ 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 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() } @@ -206,15 +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_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) - .map(|(keypair, bls_keypair)| Authority { - stake: 1, - public_key: keypair.public_key(), - bls_public_key: bls_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(), ) } @@ -225,6 +249,8 @@ pub struct Authority { stake: Stake, public_key: PublicKey, bls_public_key: BlsPublicKey, + ml_dsa_44_public_key: MlDsa44PublicKey, + ml_dsa_65_public_key: MlDsa65PublicKey, } impl Authority { @@ -233,6 +259,8 @@ 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(), + ml_dsa_65_public_key: dummy_ml_dsa_65_public_key(), } } @@ -247,6 +275,14 @@ 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 + } + + 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 552bbb9..e120301 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, 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}, }; @@ -54,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( @@ -127,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(), @@ -270,6 +278,9 @@ pub struct NodePrivateConfig { authority: AuthorityIndex, 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, } @@ -279,6 +290,9 @@ impl NodePrivateConfig { authority: index, 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"), } } @@ -286,20 +300,34 @@ 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_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_44_signers) + .zip(ml_dsa_65_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), ml_dsa_65_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, + ml_dsa_65_keypair, + mac_keys, + storage_path: path, + } + }, + ) .collect() } diff --git a/crates/starfish-core/src/core.rs b/crates/starfish-core/src/core.rs index 2a0311c..abfeece 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, Signer}, + crypto::{ + self, AsBytes, BlsSignatureBytes, BlsSigner, MacKey, MlDsa44Signer, MlDsa65Signer, Signer, + }, dag_state::{ ByzantineStrategy, CACHED_ROUNDS, CommitData, ConsensusProtocol, DagState, DataSource, OwnBlockData, @@ -32,9 +34,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 +63,9 @@ pub struct Core { pub(crate) metrics: Arc, 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 recovered_committed_blocks: Option>, @@ -185,6 +191,9 @@ impl Core { metrics, 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), recovered_committed_leaders_count: Some(committed_leaders_count), @@ -206,6 +215,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 +1004,20 @@ 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), + BlockAuthenticationScheme::MlDsa65 => BlockAuthorizer::MlDsa65(&self.ml_dsa_65_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 f34e03c..ee74e5e 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, MlDsa65, 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,15 @@ 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_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; @@ -88,6 +101,13 @@ 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]); + // Box ensures value is not copied in memory when Signer itself is moved around // for better security #[derive(Clone)] @@ -175,13 +195,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 +214,6 @@ impl BlockDigest { block_references, acknowledgment_references, meta_creation_time_ns, - signature, merkle_root, strong_vote, None, @@ -204,13 +226,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 +242,6 @@ impl BlockDigest { strong_vote, ); Self::hash_unprovable_certificate(&mut hasher, unprovable_certificate); - hasher.update(signature.as_bytes()); Self(hasher.finalize().into()) } @@ -231,7 +251,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 +260,6 @@ impl BlockDigest { block_references, acknowledgment_references, meta_creation_time_ns, - signature, transactions_commitment, strong_vote, None, @@ -254,13 +272,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 +288,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 +321,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 +489,308 @@ 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 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) + } +} + +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) + } +} + +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 AsRef<[u8]> for $signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } + } + + impl fmt::Debug for $signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}Sig({})", $label, &hex::encode(&self.0[..4])) + } + } + + impl Serialize for $signature { + fn serialize(&self, serializer: S) -> Result { + serialize_fixed_bytes(self.0.as_ref(), serializer) + } + } + + impl<'de> Deserialize<'de> for $signature { + fn deserialize>(deserializer: D) -> Result { + deserialize_fixed_bytes::( + deserializer, + concat!($label, " signature"), + ) + .map(Self::from_bytes) + } + } + + 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) + } + } + + impl PartialEq for $public_key { + fn eq(&self, other: &Self) -> bool { + self.to_bytes() == other.to_bytes() + } + } + + impl Eq for $public_key {} + + 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 Serialize for $public_key { + 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 $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 fmt::Debug for $signer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}Signer(public_key={:?})", $label, self.public_key()) + } + } + + 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) + } + } + + 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)))) + } + } + + pub fn $dummy_signer() -> $signer { + let seed = ml_dsa::Seed::from([0; $seed_size]); + $signer(Box::new(MlDsaSigningKey::from_seed(&seed))) + } + + pub fn $dummy_public_key() -> $public_key { + $dummy_signer().public_key() + } + }; +} + +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] { let mut bytes = [0u8; 32]; bytes.copy_from_slice(self.0.as_ref()); @@ -530,56 +821,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 { @@ -1052,6 +1293,71 @@ 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 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); @@ -1111,6 +1417,14 @@ 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, + ml_dsa_65_signer: MlDsa65Signer, + ml_dsa_65_public_key: MlDsa65PublicKey, + ml_dsa_65_signature: MlDsa65SignatureBytes, } #[test] @@ -1118,15 +1432,28 @@ 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 ml_dsa_65_signer = dummy_ml_dsa_65_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, + 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(); @@ -1142,6 +1469,20 @@ 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.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 4192c2d..979eeab 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, BlockAuthentication, 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,68 @@ 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 { + 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 '{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!( + "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, + }) + } +} + const STARFISH_SPEED_HINT_WINDOW_LEADER_ROUNDS: usize = 10; #[allow(unused)] @@ -324,6 +392,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, @@ -505,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, @@ -528,7 +623,7 @@ impl DagState { Arc::new(RocksStore::open(&path).expect("Failed to open RocksDB")) } }; - let consensus_protocol = ConsensusProtocol::from_str(&consensus); + let consensus_protocol = protocol_config.consensus_protocol; let resolved_dissemination = consensus_protocol.resolve_dissemination_mode(dissemination_mode); let push_mode = matches!( @@ -815,6 +910,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, @@ -1124,6 +1220,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, @@ -3386,19 +3542,20 @@ mod tests { use super::{ ByzantineStrategy, CACHED_ROUNDS, CertificateEvent, ConsensusProtocol, - DacCertificateVerificationState, DagState, DataSource, OwnBlockData, + DacCertificateVerificationState, DagState, DataSource, OwnBlockData, ProtocolConfig, }; use crate::{ 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, BlockReference, BlsAggregateCertificate, + AuthorityIndex, AuthoritySet, BaseTransaction, BlockAuthentication, + BlockAuthenticationScheme, BlockAuthorizer, BlockReference, BlsAggregateCertificate, ProvableShard, RoundNumber, SailfishFields, SailfishNoVoteCert, Transaction, VerifiedBlock, }, @@ -3657,6 +3814,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); @@ -4748,4 +4964,63 @@ mod tests { DisseminationMode::PushCausal ); } + + #[test] + fn protocol_config_selects_block_authentication() { + 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, + } + ); + } + } + + 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()); + } } diff --git a/crates/starfish-core/src/net_sync.rs b/crates/starfish-core/src/net_sync.rs index cef8dd4..ec9942e 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}, @@ -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,80 @@ 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(()), + } +} + +/// 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, @@ -161,6 +236,7 @@ fn eligible_missing_parent_refs( struct FilterForBlocks { digests: parking_lot::RwLock>, + full_mac_vectors: parking_lot::RwLock>, queue: parking_lot::RwLock>, } @@ -168,6 +244,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 +254,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 +527,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) { @@ -435,12 +541,28 @@ fn spawn_header_worker( } let mut block: VerifiedBlock = (*data_block).clone(); tracing::debug!("Received {} from {}", block, peer); - match block.verify( + 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, 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") @@ -464,11 +586,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!( @@ -960,27 +1085,45 @@ 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; } let mut block: VerifiedBlock = (*data_block).clone(); tracing::debug!("Received {} from {}", block, self.peer); - let shard = match block.verify( + if let Err(e) = verify_mac_transport( + &block, + self.inner.dag_state.block_authentication_scheme, + self.peer_id, + source, + ) { + tracing::warn!( + "Rejected incorrectly transported block {} from {}: {:?}", + block.reference(), + self.peer, + e + ); + break; + } + let shard = match block.verify_with_authentication( &self.inner.committee, self.own_id as usize, self.peer_id as usize, &mut self.encoder, self.consensus_protocol, + self.inner.dag_state.block_authentication_scheme, + &self.inner.mac_keys, ) { Ok(shard) => shard, Err(e) => { @@ -1008,8 +1151,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 --- @@ -1246,6 +1397,11 @@ impl ConnectionHandler ConnectionHandler { 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 +1529,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 +1622,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(), @@ -2363,7 +2527,7 @@ mod tests { use super::*; use crate::{ - crypto::SignatureBytes, + crypto::{self, SignatureBytes}, types::{BaseTransaction, BlockReference}, }; @@ -2381,6 +2545,140 @@ 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 full_mac_vectors_require_direct_author_block_streaming() { + let committee = Committee::new_for_benchmarks(4); + let keyrings = crypto::mac_keyrings_for_test(committee.len()); + let mut full = VerifiedBlock::new( + 1, + 1, + Vec::new(), + Vec::new(), + 0, + SignatureBytes::default(), + Vec::new(), + None, + None, + None, + None, + ); + let tags = keyrings[1] + .iter() + .enumerate() + .map(|(recipient, key)| key.compute_tag(1, recipient as AuthorityIndex, &full.digest())) + .collect(); + full.header.authentication = BlockAuthentication::MacVector(tags); + + assert!( + verify_mac_transport( + &full, + BlockAuthenticationScheme::MacVector, + 1, + DataSource::BlockBundleStreaming, + ) + .is_ok() + ); + assert!( + verify_mac_transport( + &full, + BlockAuthenticationScheme::MacVector, + 1, + DataSource::BlockBundleStreamingHeader, + ) + .is_ok() + ); + assert!( + verify_mac_transport( + &full, + BlockAuthenticationScheme::MacVector, + 2, + DataSource::BlockBundleStreaming, + ) + .is_err() + ); + assert!( + verify_mac_transport( + &full, + BlockAuthenticationScheme::MacVector, + 1, + DataSource::BlockHeaderRequest, + ) + .is_err() + ); + + let tagged = full.with_recipient_mac(0).unwrap(); + assert!( + verify_mac_transport( + &tagged, + BlockAuthenticationScheme::MacVector, + 2, + DataSource::BlockBundleStreaming, + ) + .is_ok() + ); + assert!( + verify_mac_transport( + &tagged, + BlockAuthenticationScheme::MacVector, + 1, + DataSource::BlockHeaderRequest, + ) + .is_ok() + ); + assert!( + verify_mac_transport( + &tagged, + BlockAuthenticationScheme::MacVector, + 1, + DataSource::BlockBundleStreaming, + ) + .is_err() + ); + + let round_gap_blocks = prepare_forwarded_blocks_for_peer( + BlockAuthenticationScheme::MacVector, + 0, + vec![Data::new(full)], + ); + assert_eq!(round_gap_blocks.len(), 1); + assert!(matches!( + round_gap_blocks[0].authentication(), + BlockAuthentication::MacTag(_) + )); + assert!( + verify_mac_transport( + &round_gap_blocks[0], + BlockAuthenticationScheme::MacVector, + 2, + DataSource::RoundGapResponse, + ) + .is_ok() + ); + } + #[test] fn acknowledgments_imply_peer_knows_shard_data() { let ack_ref = BlockReference::new_test(2, 3); diff --git a/crates/starfish-core/src/sailfish_service.rs b/crates/starfish-core/src/sailfish_service.rs index 62fb3d0..b580fac 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 b70bf4c..7ba883b 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/types.rs b/crates/starfish-core/src/types.rs index 53cbc6e..df6a36e 100644 --- a/crates/starfish-core/src/types.rs +++ b/crates/starfish-core/src/types.rs @@ -35,13 +35,14 @@ 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, crypto, crypto::{ - AsBytes, BlsSignatureBytes, BlsSigner, CryptoHash, SignatureBytes, Signer, + AsBytes, BlsSignatureBytes, BlsSigner, CryptoHash, MacKey, MacTag, MlDsa44SignatureBytes, + MlDsa44Signer, MlDsa65SignatureBytes, MlDsa65Signer, SignatureBytes, Signer, TransactionsCommitment, }, dag_state::ConsensusProtocol, @@ -87,9 +88,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 +238,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 +251,127 @@ 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), + /// 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), + MlDsa65(MlDsa65SignatureBytes), +} + +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" + ) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result { + 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(), + crypto::MAC_TAG_SIZE + ))); + } + + Ok(chunks + .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, + MacVector, + MlDsa44, + MlDsa65, +} + +pub enum BlockAuthorizer<'a> { + Ed25519(&'a Signer), + MacVector(&'a [MacKey]), + MlDsa44(&'a MlDsa44Signer), + MlDsa65(&'a MlDsa65Signer), +} + +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)) + } + Self::MlDsa65(signer) => { + BlockAuthentication::MlDsa65(signer.sign_digest(content_digest)) + } + } + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct BlockHeader { // -- Base fields (all protocols) ------------------------------------------ @@ -267,8 +382,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 +483,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 +893,6 @@ impl VerifiedBlock { &block_references, &acknowledgments, meta_creation_time_ns, - &signature, merkle_root, strong_vote, unprovable_certificate.as_ref(), @@ -785,7 +900,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 +937,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 +1029,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 +1112,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 +1122,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 +1159,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 +1228,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 { @@ -1145,6 +1298,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 { @@ -1153,6 +1348,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. @@ -1208,12 +1418,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 +1514,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 +1550,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 +1560,72 @@ 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, 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" + ); + 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!(*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 { + 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:?}"); + } + } + (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:?}") + } } for include in &self.header.block_references { ensure!( @@ -2147,6 +2438,430 @@ impl std::hash::Hash for VerifiedBlock { mod tests { use super::*; + fn make_authenticated_starfish_block( + committee: &Committee, + authorizer: &BlockAuthorizer<'_>, + ) -> VerifiedBlock { + 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(); + 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_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_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_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_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_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_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 [ + ConsensusProtocol::Starfish, + ConsensusProtocol::StarfishSpeed, + ConsensusProtocol::SparseStarfishSpeed, + ] { + // A Sparse-Starfish-Speed non-leader has compressed references, + // while its leader carries the full frontier used by this test + // block. Using the round-one leader makes the same authenticated + // content structurally valid under all three protocols. + let author = committee.elect_leader(1) as usize; + let cases = [ + ( + make_authenticated_starfish_block_for_author( + &committee, + author as AuthorityIndex, + &BlockAuthorizer::Ed25519(&ed_signers[author]), + ), + BlockAuthenticationScheme::Ed25519, + ), + ( + make_authenticated_starfish_block_for_author( + &committee, + author as AuthorityIndex, + &BlockAuthorizer::MacVector(&mac_keyrings[author]), + ), + BlockAuthenticationScheme::MacVector, + ), + ( + make_authenticated_starfish_block_for_author( + &committee, + author as AuthorityIndex, + &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() { + 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(); + } + } + } + } + + #[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 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 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); + 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); + 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); + 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 +3083,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 32955dc..d9e0e51 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,48 @@ impl Validator { byzantine_strategy: String, consensus: String, ) -> Result { + 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()) + { + 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}" + )); + } + } + 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 .network_address(authority) @@ -76,7 +118,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( @@ -96,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 @@ -256,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(); @@ -304,30 +352,52 @@ mod smoke_tests { } } - #[test_case("mysticeti", 0)] - #[test_case("cordial-miners", 40)] - #[test_case("starfish", 60)] - #[test_case("starfish-speed", 80)] - #[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("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(); @@ -404,18 +474,30 @@ mod smoke_tests { } } - #[test_case("mysticeti", 100)] - #[test_case("cordial-miners", 140)] - #[test_case("starfish", 160)] - #[test_case("starfish-speed", 180)] - #[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("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 2c8e24e..73a6288 100644 --- a/crates/starfish/src/main.rs +++ b/crates/starfish/src/main.rs @@ -65,8 +65,13 @@ enum Operation { parameters_path: String, #[clap(long, value_name = "STRING", default_value = "")] byzantine_strategy: String, + /// 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. @@ -93,8 +98,13 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, + /// 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, @@ -143,8 +153,13 @@ enum Operation { /// `--adversarial-latency` is enabled (0-100). #[clap(long, value_name = "INT", default_value_t = 34)] adversarial_latency_percent: u32, + /// 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: @@ -178,6 +193,7 @@ async fn main() -> Result<()> { parameters_path, byzantine_strategy, consensus: consensus_protocol, + block_authentication, } => { run( authority, @@ -187,6 +203,7 @@ async fn main() -> Result<()> { parameters_path, byzantine_strategy, consensus_protocol, + block_authentication, ) .await? } @@ -200,6 +217,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus: consensus_protocol, + block_authentication, data_dir, base_ip, storage_backend, @@ -218,6 +236,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus_protocol, + block_authentication, data_dir, base_ip, storage_backend, @@ -238,6 +257,7 @@ async fn main() -> Result<()> { adversarial_latency, adversarial_latency_percent, consensus: consensus_protocol, + block_authentication, duration_secs, dissemination_mode, } => { @@ -247,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)?; } @@ -339,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 { @@ -511,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}'" ))?; @@ -555,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, @@ -596,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; } @@ -704,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() { @@ -720,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/deny.toml b/deny.toml index 3f76a6c..8ce45ed 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, diff --git a/local-dryrun/README.md b/local-dryrun/README.md index 390d43c..5b568c2 100644 --- a/local-dryrun/README.md +++ b/local-dryrun/README.md @@ -30,11 +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: `starfish`, `starfish-speed`, -`sparse-starfish-speed`, `starfish-bls`, `cordial-miners`, -`mysticeti`, `sailfish-pp`, `bluestreak`, `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 b4ed06a..3ca409f 100755 --- a/local-dryrun/dryrun.sh +++ b/local-dryrun/dryrun.sh @@ -6,10 +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 +# 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, @@ -573,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 <