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,
|
# When ceremony-engine is published to crates.io,
|
||||||
# this becomes a version dependency.
|
# this becomes a version dependency.
|
||||||
accord-core = { path = "../../guildhouse/services/accord-core" }
|
accord-core = { path = "../../guildhouse/services/accord-core" }
|
||||||
|
governance-types = { path = "../../substrate/crates/governance-types" }
|
||||||
registry-protocol = { path = "../../guildhouse/services/registry-protocol" }
|
registry-protocol = { path = "../../guildhouse/services/registry-protocol" }
|
||||||
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
//! resolved ceremonies can be merkle-anchored in the notary chain,
|
//! resolved ceremonies can be merkle-anchored in the notary chain,
|
||||||
//! and [`MutationVerb`] on [`CeremonyVerb`] for SAT scope encoding.
|
//! and [`MutationVerb`] on [`CeremonyVerb`] for SAT scope encoding.
|
||||||
|
|
||||||
|
use governance_types::GovernanceEnvelope;
|
||||||
use registry_protocol::{RegistryArtifact, MutationVerb};
|
use registry_protocol::{RegistryArtifact, MutationVerb};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -55,28 +56,64 @@ impl RegistryArtifact for CeremonyResolution {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonical_bytes(&self) -> Vec<u8> {
|
fn canonical_bytes(&self) -> Vec<u8> {
|
||||||
// For git-originated ceremonies (PipelineMerge, SchematicPublish,
|
// For git-originated ceremonies, construct a GovernanceEnvelope
|
||||||
// GitOpsSync), bind the git ref into the canonical form so the
|
// and use its canonical_hash as the merkle leaf. This binds
|
||||||
// Quartermaster merkle leaf transitively includes git's hash.
|
// git ref + governance metadata into one auditable hash.
|
||||||
// Format: proof_hash bytes || git ref bytes.
|
// Use resolved_at as a fixed timestamp so canonical_bytes is
|
||||||
let mut bytes = self.proof_hash.as_bytes().to_vec();
|
// 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 {
|
match &self.subject {
|
||||||
CeremonySubject::PipelineMerge { commit_hash, .. } => {
|
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, .. } => {
|
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, .. } => {
|
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.
|
// Non-git ceremonies: proof_hash alone is the canonical form.
|
||||||
CeremonySubject::MutationIntent { .. }
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
@ -144,14 +181,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 res = sample_resolution(); // PipelineMerge with commit_hash "abc"
|
||||||
let bytes = res.canonical_bytes();
|
let bytes = res.canonical_bytes();
|
||||||
let proof_bytes = res.proof_hash.as_bytes();
|
// PipelineMerge: canonical_bytes is the 32-byte canonical_hash
|
||||||
// Must contain proof_hash + commit_hash
|
// of a GovernanceEnvelope (not the old proof_hash || commit_hash).
|
||||||
assert!(bytes.len() > proof_bytes.len());
|
assert_eq!(bytes.len(), 32);
|
||||||
assert_eq!(&bytes[..proof_bytes.len()], proof_bytes);
|
|
||||||
assert_eq!(&bytes[proof_bytes.len()..], b"abc");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue