use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
use super::program_graph::{ProgramGraph, ProgramGraphError, ShapeDim, ValueLifetime};
use super::program_graph_analysis::ProgramGraphAnalysisError;
pub const PROGRAM_GRAPH_IDENTITY_VERSION: u16 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramGraphIdentityContext {
pub artifact_schema_version: u32,
pub configuration_digest: [u8; 32],
pub symbolic_bindings: BTreeMap<String, u64>,
pub constant_identities: BTreeMap<String, [u8; 32]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProgramGraphIdentity {
pub format_version: u16,
pub digest: [u8; 32],
}
#[derive(Debug, Error)]
pub enum ProgramGraphIdentityError {
#[error("program graph identity rejected invalid topology: {source}")]
InvalidGraph {
#[source]
source: ProgramGraphAnalysisError,
},
#[error("program graph identity could not encode canonical topology: {source}")]
NonCanonicalGraph {
#[source]
source: ProgramGraphError,
},
#[error("program graph identity is missing symbolic binding `{symbol}`")]
MissingSymbol {
symbol: String,
},
#[error("program graph identity has unexpected symbolic binding `{symbol}`")]
UnexpectedSymbol {
symbol: String,
},
#[error("program graph identity is missing constant identity `{name}`")]
MissingConstantIdentity {
name: String,
},
#[error("program graph identity has unexpected constant identity `{name}`")]
UnexpectedConstantIdentity {
name: String,
},
#[error("program graph identity input length exceeds u64")]
LengthOverflow,
}
impl ProgramGraph {
pub fn identity(
&self,
context: &ProgramGraphIdentityContext,
) -> Result<ProgramGraphIdentity, ProgramGraphIdentityError> {
self.analyze()
.map_err(|source| ProgramGraphIdentityError::InvalidGraph { source })?;
validate_context(self, context)?;
let graph_wire = self
.to_wire()
.map_err(|source| ProgramGraphIdentityError::NonCanonicalGraph { source })?;
let mut hasher = blake3::Hasher::new();
hasher.update(b"vyre-program-graph-identity\0");
hasher.update(&PROGRAM_GRAPH_IDENTITY_VERSION.to_le_bytes());
hasher.update(&context.artifact_schema_version.to_le_bytes());
update_bytes(&mut hasher, &graph_wire)?;
hasher.update(&context.configuration_digest);
update_count(&mut hasher, context.symbolic_bindings.len())?;
for (symbol, value) in &context.symbolic_bindings {
update_bytes(&mut hasher, symbol.as_bytes())?;
hasher.update(&value.to_le_bytes());
}
update_count(&mut hasher, context.constant_identities.len())?;
for (name, identity) in &context.constant_identities {
update_bytes(&mut hasher, name.as_bytes())?;
hasher.update(identity);
}
Ok(ProgramGraphIdentity {
format_version: PROGRAM_GRAPH_IDENTITY_VERSION,
digest: *hasher.finalize().as_bytes(),
})
}
}
fn validate_context(
graph: &ProgramGraph,
context: &ProgramGraphIdentityContext,
) -> Result<(), ProgramGraphIdentityError> {
let symbols = graph
.values()
.iter()
.flat_map(|value| &value.contract.shape)
.filter_map(|dimension| match dimension {
ShapeDim::Symbol(symbol) => Some(symbol.as_str()),
ShapeDim::Known(_) => None,
})
.collect::<BTreeSet<_>>();
for symbol in &symbols {
if !context.symbolic_bindings.contains_key(*symbol) {
return Err(ProgramGraphIdentityError::MissingSymbol {
symbol: (*symbol).to_owned(),
});
}
}
for symbol in context.symbolic_bindings.keys() {
if !symbols.contains(symbol.as_str()) {
return Err(ProgramGraphIdentityError::UnexpectedSymbol {
symbol: symbol.clone(),
});
}
}
let constants = graph
.values()
.iter()
.filter(|value| value.contract.lifetime == ValueLifetime::Constant)
.map(|value| value.name.as_str())
.collect::<BTreeSet<_>>();
for name in &constants {
if !context.constant_identities.contains_key(*name) {
return Err(ProgramGraphIdentityError::MissingConstantIdentity {
name: (*name).to_owned(),
});
}
}
for name in context.constant_identities.keys() {
if !constants.contains(name.as_str()) {
return Err(ProgramGraphIdentityError::UnexpectedConstantIdentity {
name: name.clone(),
});
}
}
Ok(())
}
fn update_count(
hasher: &mut blake3::Hasher,
count: usize,
) -> Result<(), ProgramGraphIdentityError> {
let count = u64::try_from(count).map_err(|_| ProgramGraphIdentityError::LengthOverflow)?;
hasher.update(&count.to_le_bytes());
Ok(())
}
fn update_bytes(
hasher: &mut blake3::Hasher,
bytes: &[u8],
) -> Result<(), ProgramGraphIdentityError> {
update_count(hasher, bytes.len())?;
hasher.update(bytes);
Ok(())
}