refactor(ceremony-engine): use GovernanceEnvelope for merkle leaves
PipelineMerge, SchematicPublish, and GitOpsSync ceremony merkle leaves are now the canonical_hash() of a GovernanceEnvelope, binding git ref + governance metadata into a single auditable 32-byte hash. Uses the resolution's resolved_at timestamp for deterministic envelope construction. Non-git ceremony types (MutationIntent, Custom) unchanged. Signed-off-by: Tyler King <tking@guildhouse.dev>
This commit is contained in:
parent
3d5e5485ec
commit
e3fb2a9a58
2 changed files with 52 additions and 16 deletions
|
|
@ -10,6 +10,7 @@ description = "Governed state machine for multi-party approval workflows"
|
|||
# When ceremony-engine is published to crates.io,
|
||||
# this becomes a version dependency.
|
||||
accord-core = { path = "../../guildhouse/services/accord-core" }
|
||||
governance-types = { path = "../../substrate/crates/governance-types" }
|
||||
registry-protocol = { path = "../../guildhouse/services/registry-protocol" }
|
||||
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
//! resolved ceremonies can be merkle-anchored in the notary chain,
|
||||
//! and [`MutationVerb`] on [`CeremonyVerb`] for SAT scope encoding.
|
||||
|
||||
use governance_types::GovernanceEnvelope;
|
||||
use registry_protocol::{RegistryArtifact, MutationVerb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -55,29 +56,65 @@ impl RegistryArtifact for CeremonyResolution {
|
|||
}
|
||||
|
||||
fn canonical_bytes(&self) -> Vec<u8> {
|
||||
// For git-originated ceremonies (PipelineMerge, SchematicPublish,
|
||||
// GitOpsSync), bind the git ref into the canonical form so the
|
||||
// Quartermaster merkle leaf transitively includes git's hash.
|
||||
// Format: proof_hash bytes || git ref bytes.
|
||||
let mut bytes = self.proof_hash.as_bytes().to_vec();
|
||||
// For git-originated ceremonies, construct a GovernanceEnvelope
|
||||
// and use its canonical_hash as the merkle leaf. This binds
|
||||
// git ref + governance metadata into one auditable hash.
|
||||
// Use resolved_at as a fixed timestamp so canonical_bytes is
|
||||
// deterministic for the same resolution (not time-dependent).
|
||||
let resolved_ns = self.resolved_at.timestamp_nanos_opt().unwrap_or(0) as u64;
|
||||
|
||||
match &self.subject {
|
||||
CeremonySubject::PipelineMerge { commit_hash, .. } => {
|
||||
bytes.extend_from_slice(commit_hash.as_bytes());
|
||||
let mut envelope = GovernanceEnvelope::for_commit(
|
||||
parse_sha1_hex(commit_hash),
|
||||
None,
|
||||
"",
|
||||
[0; 32],
|
||||
"",
|
||||
);
|
||||
envelope.timestamp_ns = resolved_ns;
|
||||
envelope.canonical_hash().to_vec()
|
||||
}
|
||||
CeremonySubject::SchematicPublish { tree_hash, .. } => {
|
||||
bytes.extend_from_slice(tree_hash.as_bytes());
|
||||
let mut envelope = GovernanceEnvelope::for_blob(
|
||||
parse_sha1_hex(tree_hash),
|
||||
"",
|
||||
[0; 32],
|
||||
"",
|
||||
);
|
||||
envelope.timestamp_ns = resolved_ns;
|
||||
envelope.canonical_hash().to_vec()
|
||||
}
|
||||
CeremonySubject::GitOpsSync { target_revision, .. } => {
|
||||
bytes.extend_from_slice(target_revision.as_bytes());
|
||||
let mut envelope = GovernanceEnvelope::for_commit(
|
||||
parse_sha1_hex(target_revision),
|
||||
None,
|
||||
"",
|
||||
[0; 32],
|
||||
"",
|
||||
);
|
||||
envelope.timestamp_ns = resolved_ns;
|
||||
envelope.canonical_hash().to_vec()
|
||||
}
|
||||
// Non-git ceremonies: proof_hash alone is the canonical form.
|
||||
CeremonySubject::MutationIntent { .. }
|
||||
| CeremonySubject::Custom { .. } => {}
|
||||
| CeremonySubject::Custom { .. } => {
|
||||
self.proof_hash.as_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a hex SHA-1 string into [u8; 20]. Returns zeros on failure.
|
||||
fn parse_sha1_hex(hex_str: &str) -> [u8; 20] {
|
||||
let mut sha = [0u8; 20];
|
||||
if let Ok(bytes) = hex::decode(hex_str.trim()) {
|
||||
let len = bytes.len().min(20);
|
||||
sha[..len].copy_from_slice(&bytes[..len]);
|
||||
}
|
||||
sha
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -144,14 +181,12 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_bytes_pipeline_merge_includes_commit_hash() {
|
||||
fn canonical_bytes_pipeline_merge_is_envelope_hash() {
|
||||
let res = sample_resolution(); // PipelineMerge with commit_hash "abc"
|
||||
let bytes = res.canonical_bytes();
|
||||
let proof_bytes = res.proof_hash.as_bytes();
|
||||
// Must contain proof_hash + commit_hash
|
||||
assert!(bytes.len() > proof_bytes.len());
|
||||
assert_eq!(&bytes[..proof_bytes.len()], proof_bytes);
|
||||
assert_eq!(&bytes[proof_bytes.len()..], b"abc");
|
||||
// PipelineMerge: canonical_bytes is the 32-byte canonical_hash
|
||||
// of a GovernanceEnvelope (not the old proof_hash || commit_hash).
|
||||
assert_eq!(bytes.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue