feat(cluster): RFC 010 c6d — build_hash derivation
cluster::BUILD_HASH, the derived value for LocalNode::build_hash: a compile-time const, FNV-1a 64 over the build script's input string (rustc -V + sorted enabled CARGO_FEATURE_* set) with PROTO_VERSION folded in as a continuation of the same state — a proto bump moves the hash even on an identical toolchain. build.rs emits only the raw inputs (SMARM_BUILD_HASH_INPUTS); the hashing lives in src/cluster.rs next to PROTO_VERSION rather than build.rs parsing it out of a source file. The env var is emitted unconditionally — one string costs the default build nothing, and the const itself is behind the cluster feature with the rest of the module. Domain is the flagged lean (one-viable-answer, veto at diff review): toolchain + declared features + proto version. Tightenable later without a wire change — it is just a u64 on the Hello. Unit tests: FNV-1a 64 published vectors (empty/'a'/'foobar'); the proto fold is a continuation of the same FNV state and moves the hash; BUILD_HASH is const-evaluable and nonzero. All cluster suites regression-clean (7 files); clippy --lib green both configs; fmt clean; default build compiles.
This commit is contained in:
@@ -8,4 +8,29 @@ fn main() {
|
|||||||
.flag_if_supported("-fno-stack-clash-protection")
|
.flag_if_supported("-fno-stack-clash-protection")
|
||||||
.compile("smarm_canary");
|
.compile("smarm_canary");
|
||||||
println!("cargo:rerun-if-changed=canary/canary.c");
|
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<String> = 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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,43 @@ pub use conn::{spawn_established, ConnHandle};
|
|||||||
pub use connect::{dial, spawn_acceptor, AcceptorHandle};
|
pub use connect::{dial, spawn_acceptor, AcceptorHandle};
|
||||||
pub use manager::{Manager, MANAGER};
|
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
|
/// A running cluster subtree: an explicitly-started supervisor over the
|
||||||
/// connection [`Manager`]. Roles will eventually mount this subtree; until 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
|
/// 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();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user