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