// Licensed to StellaOps under the BUSL-1.1 license. using Blake3; using StellaOps.ReachGraph.Schema; using StellaOps.ReachGraph.Serialization; namespace StellaOps.ReachGraph.Hashing; /// /// Computes BLAKE3-256 digests for reachability graphs using canonical serialization. /// public sealed partial class ReachGraphDigestComputer { private readonly CanonicalReachGraphSerializer _serializer; public ReachGraphDigestComputer() : this(new CanonicalReachGraphSerializer()) { } public ReachGraphDigestComputer(CanonicalReachGraphSerializer serializer) { _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); } /// /// Compute BLAKE3-256 digest of canonical JSON (excluding signatures). /// /// The reachability graph to hash. /// Digest in format "blake3:{hex}". public string ComputeDigest(ReachGraphMinimal graph) { ArgumentNullException.ThrowIfNull(graph); // Remove signatures before hashing (avoid circular dependency) var unsigned = graph with { Signatures = null }; var canonical = _serializer.SerializeMinimal(unsigned); using var hasher = Hasher.New(); hasher.Update(canonical); var hash = hasher.Finalize(); return $"blake3:{Convert.ToHexString(hash.AsSpan()).ToLowerInvariant()}"; } /// /// Compute BLAKE3-256 digest from raw canonical JSON bytes. /// /// The canonical JSON bytes to hash. /// Digest in format "blake3:{hex}". public static string ComputeDigest(ReadOnlySpan canonicalJson) { using var hasher = Hasher.New(); hasher.Update(canonicalJson); var hash = hasher.Finalize(); return $"blake3:{Convert.ToHexString(hash.AsSpan()).ToLowerInvariant()}"; } /// /// Verify digest matches graph content. /// /// The reachability graph to verify. /// The expected digest. /// True if digest matches, false otherwise. public bool VerifyDigest(ReachGraphMinimal graph, string expectedDigest) { ArgumentNullException.ThrowIfNull(graph); ArgumentException.ThrowIfNullOrEmpty(expectedDigest); var computed = ComputeDigest(graph); return string.Equals(computed, expectedDigest, StringComparison.Ordinal); } }