Skip to main content

confium_tc_core/
commitment.rs

1//! Hash-based commitment scheme.
2//!
3//! A commitment protocol has two phases:
4//!
5//! 1. **Commit**: the sender picks random `r` and computes `C = H(r || m)`.
6//!    They send `C` to the receiver. `C` reveals nothing about `m`
7//!    (hiding) and the sender can't later claim a different `m'`
8//!    (binding).
9//!
10//! 2. **Reveal**: the sender sends `(r, m)`. The receiver checks
11//!    `H(r || m) == C`.
12//!
13//! Used in threshold signing nonce rounds: each party commits to
14//! their nonce before any party reveals, preventing last-minute
15//! adaptive attacks.
16
17use rand_core::{OsRng, RngCore};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21/// A commitment: hash of (randomness || value).
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
23pub struct Commitment {
24    /// 32-byte SHA-256 hash.
25    pub hash: [u8; 32],
26}
27
28/// The decommitment: randomness + value needed to open a commitment.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct Decommitment {
31    /// 32 bytes of randomness.
32    pub randomness: [u8; 32],
33    /// The committed value.
34    pub value: Vec<u8>,
35}
36
37impl Commitment {
38    /// Create a commitment to `value` with fresh randomness.
39    /// Returns (commitment, decommitment).
40    pub fn create(value: &[u8]) -> (Self, Decommitment) {
41        let mut randomness = [0u8; 32];
42        OsRng.fill_bytes(&mut randomness);
43        let hash = compute_hash(&randomness, value);
44        (
45            Self { hash },
46            Decommitment {
47                randomness,
48                value: value.to_vec(),
49            },
50        )
51    }
52
53    /// Create a commitment with explicit randomness (for testing or
54    /// deterministic protocols).
55    pub fn create_with_randomness(value: &[u8], randomness: [u8; 32]) -> (Self, Decommitment) {
56        let hash = compute_hash(&randomness, value);
57        (
58            Self { hash },
59            Decommitment {
60                randomness,
61                value: value.to_vec(),
62            },
63        )
64    }
65
66    /// Verify a decommitment against this commitment.
67    pub fn verify(&self, decommitment: &Decommitment) -> bool {
68        let computed = compute_hash(&decommitment.randomness, &decommitment.value);
69        use subtle::ConstantTimeEq;
70        self.hash.ct_eq(&computed).into()
71    }
72
73    /// Serialize commitment hash as hex.
74    pub fn to_hex(&self) -> String {
75        hex::encode(self.hash)
76    }
77
78    /// Deserialize from hex.
79    pub fn from_hex(hex_str: &str) -> Result<Self, String> {
80        let bytes = hex::decode(hex_str).map_err(|e| e.to_string())?;
81        if bytes.len() != 32 {
82            return Err(format!("expected 32 bytes, got {}", bytes.len()));
83        }
84        let mut hash = [0u8; 32];
85        hash.copy_from_slice(&bytes);
86        Ok(Self { hash })
87    }
88}
89
90fn compute_hash(randomness: &[u8; 32], value: &[u8]) -> [u8; 32] {
91    let mut hasher = Sha256::new();
92    hasher.update(randomness);
93    hasher.update(value);
94    let result = hasher.finalize();
95    let mut hash = [0u8; 32];
96    hash.copy_from_slice(&result);
97    hash
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn create_and_verify_round_trips() {
106        let value = b"my secret nonce";
107        let (commitment, decommitment) = Commitment::create(value);
108        assert!(commitment.verify(&decommitment));
109    }
110
111    #[test]
112    fn commitment_hides_value() {
113        let (commitment_a, _) = Commitment::create(b"value A");
114        let (commitment_b, _) = Commitment::create(b"value B");
115        // Different values with fresh randomness produce different commitments
116        // (overwhelmingly likely — not deterministic, but probabilistically)
117        assert_ne!(commitment_a.hash, commitment_b.hash);
118    }
119
120    #[test]
121    fn tampered_value_rejected() {
122        let (_, mut decommitment) = Commitment::create(b"original");
123        let commitment = Commitment {
124            hash: compute_hash(&decommitment.randomness, b"original"),
125        };
126        decommitment.value = b"tampered".to_vec();
127        assert!(!commitment.verify(&decommitment));
128    }
129
130    #[test]
131    fn tampered_randomness_rejected() {
132        let (commitment, mut decommitment) = Commitment::create(b"value");
133        decommitment.randomness[0] ^= 0xFF;
134        assert!(!commitment.verify(&decommitment));
135    }
136
137    #[test]
138    fn same_value_different_randomness() {
139        let value = b"deterministic value";
140        let (c1, _) = Commitment::create_with_randomness(value, [0xAA; 32]);
141        let (c2, _) = Commitment::create_with_randomness(value, [0xBB; 32]);
142        assert_ne!(c1.hash, c2.hash);
143    }
144
145    #[test]
146    fn deterministic_creation_reproducible() {
147        let value = b"test";
148        let r = [0x42; 32];
149        let (c1, d1) = Commitment::create_with_randomness(value, r);
150        let (c2, d2) = Commitment::create_with_randomness(value, r);
151        assert_eq!(c1, c2);
152        assert_eq!(d1, d2);
153        assert!(c1.verify(&d1));
154    }
155
156    #[test]
157    fn hex_round_trip() {
158        let (commitment, _) = Commitment::create(b"value");
159        let hex = commitment.to_hex();
160        let recovered = Commitment::from_hex(&hex).unwrap();
161        assert_eq!(commitment, recovered);
162    }
163
164    #[test]
165    fn from_hex_rejects_wrong_length() {
166        assert!(Commitment::from_hex("00").is_err());
167        assert!(Commitment::from_hex(&"ab".repeat(31)).is_err());
168        assert!(Commitment::from_hex(&"ab".repeat(33)).is_err());
169    }
170
171    #[test]
172    fn empty_value_commits() {
173        let (commitment, decommitment) = Commitment::create(b"");
174        assert!(commitment.verify(&decommitment));
175    }
176
177    #[test]
178    fn large_value_commits() {
179        let value = vec![0xFFu8; 100_000];
180        let (commitment, decommitment) = Commitment::create(&value);
181        assert!(commitment.verify(&decommitment));
182    }
183
184    #[test]
185    fn verify_uses_constant_time_comparison() {
186        let (commitment, decommitment) = Commitment::create(b"value");
187        // Should not panic and should return correct result
188        assert!(commitment.verify(&decommitment));
189    }
190}