// 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);
}
}