use alloc::string::{String, ToString};
use core::fmt;
use rand::Rng;
use sha2::{Digest, Sha256};
use crate::common::errors::CardError;
use crate::seal::commit::hex;
pub const TAG_CONTRIBUTION: &[u8] = b"cardpack/commit-reveal/v1/contribution";
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct Contribution([u8; 32]);
impl Contribution {
pub fn random(rng: &mut dyn Rng) -> Self {
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
Self(bytes)
}
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn commit(&self) -> Commitment {
let mut h = Sha256::new();
h.update(TAG_CONTRIBUTION);
h.update(self.0);
Commitment(h.finalize().into())
}
}
impl fmt::Debug for Contribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Contribution(<redacted>)")
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Commitment([u8; 32]);
impl Commitment {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, CardError> {
hex::decode_32(s)
.map(Self)
.ok_or_else(|| CardError::InvalidHex(s.to_string()))
}
#[must_use]
pub fn verify(&self, c: &Contribution) -> bool {
c.commit() == *self
}
}
impl fmt::Debug for Commitment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Commitment({})", self.to_hex())
}
}
impl fmt::Display for Commitment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod seal__commit__commitment_tests {
use super::*;
use alloc::format;
use alloc::string::ToString;
use rand::SeedableRng;
use rand::rngs::StdRng;
const GOLDEN: &str = "a070e19c3356bc6b0c8c012c303815182a16674d480faf5169f5381f14f6942e";
#[test]
fn commitment__golden_vector() {
let c = Contribution::from_bytes([0x11; 32]).commit();
assert_eq!(c.to_hex(), GOLDEN);
}
#[test]
fn commitment__verify_roundtrip() {
for b in [0x00u8, 0x11, 0x7f, 0xff] {
let c = Contribution::from_bytes([b; 32]);
assert!(c.commit().verify(&c));
}
}
#[test]
fn commitment__verify_rejects_other() {
let a = Contribution::from_bytes([0x11; 32]);
let mut other = [0x11; 32];
other[31] ^= 1;
let b = Contribution::from_bytes(other);
assert!(!a.commit().verify(&b));
}
#[test]
fn contribution__debug_redacted() {
let c = Contribution::from_bytes([0xab; 32]);
let dbg = format!("{c:?}");
assert!(!dbg.contains("ab"), "leaked: {dbg}");
assert!(!dbg.contains("171"), "leaked: {dbg}");
assert_eq!(dbg, "Contribution(<redacted>)");
}
#[test]
fn contribution__as_bytes_roundtrip() {
let c = Contribution::from_bytes([0x42; 32]);
assert_eq!(c.as_bytes(), &[0x42; 32]);
}
#[test]
fn contribution__random_differs() {
let mut rng = StdRng::seed_from_u64(7);
let a = Contribution::random(&mut rng);
let b = Contribution::random(&mut rng);
assert_ne!(a, b);
}
#[test]
fn commitment__hex_roundtrip() {
let c = Contribution::from_bytes([0x11; 32]).commit();
let hex = c.to_hex();
assert_eq!(hex.len(), 64);
assert_eq!(Commitment::from_hex(&hex).unwrap(), c);
assert_eq!(Commitment::from_hex(&hex.to_uppercase()).unwrap(), c);
}
#[test]
fn commitment__debug_is_hex() {
let c = Contribution::from_bytes([0x11; 32]).commit();
assert_eq!(format!("{c:?}"), format!("Commitment({GOLDEN})"));
}
#[test]
fn commitment__from_hex_rejects_garbage() {
assert_eq!(
Commitment::from_hex("zz"),
Err(CardError::InvalidHex("zz".to_string()))
);
let short = "ab".repeat(31);
assert_eq!(
Commitment::from_hex(&short),
Err(CardError::InvalidHex(short.clone()))
);
let bad_char = format!("{}g", "ab".repeat(31));
assert_eq!(
Commitment::from_hex(&bad_char),
Err(CardError::InvalidHex(bad_char.clone()))
);
}
#[test]
fn commitment__as_bytes_is_32() {
let c = Contribution::from_bytes([0x11; 32]).commit();
assert_eq!(c.as_bytes().len(), 32);
}
}