exo_consensus/
commitment.rs1use exo_core::{hash::hash_structured, types::Hash256};
18use serde::Serialize;
19
20use crate::{
21 error::{ConsensusError, Result},
22 round::ModelDeliberationResponse,
23};
24
25pub fn commit(position_text: &str) -> Hash256 {
28 Hash256::digest(position_text.as_bytes())
29}
30
31pub fn verify_commitment(position_text: &str, commitment: &Hash256) -> bool {
33 commit(position_text) == *commitment
34}
35
36pub fn commit_response(response: &ModelDeliberationResponse) -> Result<Hash256> {
38 #[derive(Serialize)]
39 struct CommitmentPayload<'a> {
40 domain: &'static str,
41 schema_version: &'static str,
42 position_text: &'a str,
43 key_claims: &'a [String],
44 confidence_bps: u64,
45 }
46
47 let payload = CommitmentPayload {
48 domain: "exo.consensus.model_response.commitment.v1",
49 schema_version: "1",
50 position_text: &response.position_text,
51 key_claims: &response.key_claims,
52 confidence_bps: response.confidence_bps,
53 };
54
55 hash_structured(&payload).map_err(|source| ConsensusError::HashSerialization {
56 context: "structured consensus model response commitment",
57 source,
58 })
59}
60
61pub fn verify_response_commitment(
63 response: &ModelDeliberationResponse,
64 commitment: &Hash256,
65) -> Result<bool> {
66 Ok(commit_response(response)? == *commitment)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn raw_position_commitment_is_deterministic_and_bound_to_text() {
75 let first = commit("allow subject-signed AVC receipts");
76 let second = commit("allow subject-signed AVC receipts");
77 let changed = commit("deny unsigned AVC receipts");
78
79 assert_eq!(first, second);
80 assert_ne!(first, changed);
81 }
82
83 #[test]
84 fn raw_position_commitment_verification_rejects_changed_text() {
85 let commitment = commit("canonical position");
86
87 assert!(verify_commitment("canonical position", &commitment));
88 assert!(!verify_commitment("different position", &commitment));
89 }
90}