diff --git a/build.rs b/build.rs index f00ab7f..1e200ed 100644 --- a/build.rs +++ b/build.rs @@ -8,4 +8,29 @@ fn main() { .flag_if_supported("-fno-stack-clash-protection") .compile("smarm_canary"); println!("cargo:rerun-if-changed=canary/canary.c"); + + // RFC 010 c6d — build_hash inputs. The compile-time facts a peer must + // share for a mesh link: the exact toolchain and the declared (enabled) + // feature set. Emitted as a plain string; the hashing (FNV-1a folded + // with PROTO_VERSION) happens in src/cluster.rs where the protocol + // version actually lives — parsing it out of a source file here would + // be a second, fragile copy. Always emitted, even for non-cluster + // builds: one env var costs the default build nothing. + let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); + let version = std::process::Command::new(&rustc) + .arg("-V") + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| "rustc-unknown".to_string()); + let mut feats: Vec = std::env::vars() + .filter_map(|(k, _)| k.strip_prefix("CARGO_FEATURE_").map(str::to_string)) + .collect(); + feats.sort(); + println!( + "cargo:rustc-env=SMARM_BUILD_HASH_INPUTS={version};features={}", + feats.join(",") + ); + println!("cargo:rerun-if-env-changed=RUSTC"); } diff --git a/src/cluster.rs b/src/cluster.rs index b3cea4f..aea18d8 100644 --- a/src/cluster.rs +++ b/src/cluster.rs @@ -26,6 +26,43 @@ pub use conn::{spawn_established, ConnHandle}; pub use connect::{dial, spawn_acceptor, AcceptorHandle}; pub use manager::{Manager, MANAGER}; +/// c6d — the derived build hash for [`handshake::LocalNode::build_hash`]: +/// two builds may mesh only when this matches, and it is a pure function of +/// the compile-time inputs that define wire compatibility today — the exact +/// toolchain (`rustc -V`), the declared feature set, and +/// [`envelope::PROTO_VERSION`]. FNV-1a 64 over the build-script string, then +/// the proto version folded byte-wise, so a proto bump moves the hash even +/// on an identical toolchain. The domain is deliberately lean and +/// tightenable later without a wire change — it is just a `u64`. +pub const BUILD_HASH: u64 = fold_u32( + fnv1a64(env!("SMARM_BUILD_HASH_INPUTS").as_bytes()), + envelope::PROTO_VERSION, +); + +/// FNV-1a 64 (const so [`BUILD_HASH`] is a compile-time fact). +const fn fnv1a64(bytes: &[u8]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + let mut i = 0; + while i < bytes.len() { + h ^= bytes[i] as u64; + h = h.wrapping_mul(0x0000_0100_0000_01b3); + i += 1; + } + h +} + +/// Continue an FNV-1a state over a `u32`'s little-endian bytes. +const fn fold_u32(mut h: u64, v: u32) -> u64 { + let b = v.to_le_bytes(); + let mut i = 0; + while i < b.len() { + h ^= b[i] as u64; + h = h.wrapping_mul(0x0000_0100_0000_01b3); + i += 1; + } + h +} + /// A running cluster subtree: an explicitly-started supervisor over the /// connection [`Manager`]. Roles will eventually mount this subtree; until the /// role mechanism lands it is started by hand (RFC 010 §7). Dropping the handle @@ -64,3 +101,37 @@ fn manager_child() { }; let _ = monitor(m.pid()).rx.recv(); } + +#[cfg(test)] +mod tests { + use super::*; + + /// The hash core against the published FNV-1a 64 test vectors — the + /// contract is "this is FNV-1a", not "whatever the fn does". + #[test] + fn fnv1a64_known_vectors() { + assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325); + assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c); + assert_eq!(fnv1a64(b"foobar"), 0x85944171f73967e8); + } + + /// Folding the proto version continues the same FNV state: identical + /// inputs with a different version must land on a different hash. + #[test] + fn proto_version_moves_the_hash() { + let base = fnv1a64(b"same-toolchain;features=CLUSTER"); + assert_ne!(fold_u32(base, 1), fold_u32(base, 2)); + // And it equals hashing the bytes in one pass — the fold is a + // continuation, not a second construction. + let mut all = b"same-toolchain;features=CLUSTER".to_vec(); + all.extend_from_slice(&1u32.to_le_bytes()); + assert_eq!(fold_u32(base, 1), fnv1a64(&all)); + } + + /// The derived constant exists, is compile-time, and is not degenerate. + #[test] + fn build_hash_is_nonzero() { + const H: u64 = BUILD_HASH; + assert_ne!(H, 0); + } +}