hekate_crypto/
transcript.rs1use crate::{DefaultHasher, Hasher};
19#[cfg(feature = "transcript-trace")]
20use alloc::vec::Vec;
21use core::fmt;
22use core::marker::PhantomData;
23use hekate_math::TowerField;
24
25pub type Result<T> = core::result::Result<T, Error>;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum Error {
29 FieldTooLargeForChallenge {
33 field_bytes: usize,
34 max_entropy_bytes: usize,
35 },
36}
37
38impl fmt::Display for Error {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::FieldTooLargeForChallenge {
42 field_bytes,
43 max_entropy_bytes,
44 } => write!(
45 f,
46 "Field too large for transcript entropy: field_bytes={field_bytes}, max_entropy_bytes={max_entropy_bytes}",
47 ),
48 }
49 }
50}
51
52#[cfg(feature = "transcript-trace")]
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum TranscriptOp {
59 AppendMessage {
60 label: &'static [u8],
61 digest: [u8; 32],
62 },
63 AppendU64 {
64 label: &'static [u8],
65 value: u64,
66 },
67 AppendField {
68 label: &'static [u8],
69 digest: [u8; 32],
70 },
71 AppendFieldList {
72 label: &'static [u8],
73 count: u64,
74 digest: [u8; 32],
75 },
76 ChallengeField {
77 label: &'static [u8],
78 },
79}
80
81#[derive(Clone, Debug)]
91pub struct Transcript<H: Hasher = DefaultHasher> {
92 hasher: H,
93 #[cfg(feature = "transcript-trace")]
94 trace: Vec<TranscriptOp>,
95 _marker: PhantomData<H>,
96}
97
98impl<H: Hasher> Transcript<H> {
99 pub fn new(label: &'static [u8]) -> Self {
101 let mut hasher = H::new();
102 hasher.update(b"hekate-transcript-v1");
103 hasher.update(label);
104
105 Self {
106 hasher,
107 #[cfg(feature = "transcript-trace")]
108 trace: Vec::new(),
109 _marker: PhantomData,
110 }
111 }
112
113 #[cfg(feature = "transcript-trace")]
114 pub fn take_trace(&mut self) -> Vec<TranscriptOp> {
115 core::mem::take(&mut self.trace)
116 }
117
118 #[cfg(feature = "transcript-trace")]
119 pub fn trace(&self) -> &[TranscriptOp] {
120 &self.trace
121 }
122
123 pub fn append_message(&mut self, label: &'static [u8], message: &[u8]) {
127 self.hasher.update(label);
128
129 self.hasher.update(&(message.len() as u64).to_le_bytes());
130 self.hasher.update(message);
131
132 #[cfg(feature = "transcript-trace")]
133 self.trace.push(TranscriptOp::AppendMessage {
134 label,
135 digest: payload_digest::<H>(message),
136 });
137 }
138
139 pub fn append_u64(&mut self, label: &'static [u8], value: u64) {
142 self.hasher.update(label);
143 self.hasher.update(&value.to_le_bytes());
144
145 #[cfg(feature = "transcript-trace")]
146 self.trace.push(TranscriptOp::AppendU64 { label, value });
147 }
148
149 pub fn append_field<F: TowerField>(&mut self, label: &'static [u8], element: F) {
150 self.hasher.update(label);
151
152 let bytes = element.to_bytes();
153 self.hasher.update(&bytes);
154
155 #[cfg(feature = "transcript-trace")]
156 self.trace.push(TranscriptOp::AppendField {
157 label,
158 digest: payload_digest::<H>(&bytes),
159 });
160 }
161
162 pub fn append_field_list<F: TowerField>(&mut self, label: &'static [u8], elements: &[F]) {
168 self.hasher.update(label);
169
170 self.hasher.update(&(elements.len() as u64).to_le_bytes());
171
172 #[cfg(feature = "transcript-trace")]
173 let mut digest_h = H::new();
174 for element in elements {
175 let bytes = element.to_bytes();
176 self.hasher.update(&bytes);
177
178 #[cfg(feature = "transcript-trace")]
179 digest_h.update(&bytes);
180 }
181
182 #[cfg(feature = "transcript-trace")]
183 self.trace.push(TranscriptOp::AppendFieldList {
184 label,
185 count: elements.len() as u64,
186 digest: digest_h.finalize(),
187 });
188 }
189
190 pub fn challenge_field<F: TowerField>(&mut self, label: &'static [u8]) -> Result<F> {
197 self.hasher.update(label);
198
199 let challenge_hasher = self.hasher.clone();
200 let result = challenge_hasher.finalize();
201
202 self.hasher.update(&result);
203
204 #[cfg(feature = "transcript-trace")]
205 self.trace.push(TranscriptOp::ChallengeField { label });
206
207 Self::bytes_to_field(&result)
208 }
209
210 fn bytes_to_field<F: TowerField>(bytes: &[u8]) -> Result<F> {
211 let size = size_of::<F>();
212 let max_entropy_bytes = 32;
213
214 if size > max_entropy_bytes {
215 return Err(Error::FieldTooLargeForChallenge {
216 field_bytes: size,
217 max_entropy_bytes,
218 });
219 }
220
221 let mut elem = F::default();
226 unsafe {
227 let elem_ptr = &mut elem as *mut F as *mut u8;
228 core::ptr::copy_nonoverlapping(bytes.as_ptr(), elem_ptr, size);
229 }
230
231 Ok(elem)
232 }
233}
234
235#[cfg(feature = "transcript-trace")]
236fn payload_digest<H: Hasher>(bytes: &[u8]) -> [u8; 32] {
237 let mut h = H::new();
238 h.update(bytes);
239
240 h.finalize()
241}