1use core::{fmt, marker::PhantomData};
4use hashsigs_types::{
5 CHAIN_COUNT, HASH_BYTES, PUBLIC_KEY_BYTES, Profile, PublicKey, SIGNATURE_BYTES, Signature,
6};
7
8pub const OUTPUT_BEAT_BYTES: usize = 64;
10
11pub const FUSED_OUTPUT_BEATS: usize = 35;
13
14const FULL_KEEP: u64 = u64::MAX;
15const FINAL_KEEP: u64 = (1_u64 << 32) - 1;
16
17const _: () = {
18 assert!(SIGNATURE_BYTES == 2_144);
19 assert!(PUBLIC_KEY_BYTES == 64);
20 assert!(SIGNATURE_BYTES + PUBLIC_KEY_BYTES == 2_208);
21 assert!((SIGNATURE_BYTES + PUBLIC_KEY_BYTES).div_ceil(OUTPUT_BEAT_BYTES) == FUSED_OUTPUT_BEATS);
22};
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct FusedBeat {
33 pub data: [u8; OUTPUT_BEAT_BYTES],
35 pub keep: u64,
37 pub last: bool,
39}
40
41impl Default for FusedBeat {
42 fn default() -> Self {
43 Self {
44 data: [0; OUTPUT_BEAT_BYTES],
45 keep: 0,
46 last: false,
47 }
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct FusedFrame<P: Profile> {
63 beats: [FusedBeat; FUSED_OUTPUT_BEATS],
64 profile: PhantomData<fn() -> P>,
65}
66
67impl<P: Profile> FusedFrame<P> {
68 #[must_use]
76 pub const fn from_beats(beats: [FusedBeat; FUSED_OUTPUT_BEATS]) -> Self {
77 Self {
78 beats,
79 profile: PhantomData,
80 }
81 }
82
83 #[must_use]
85 pub const fn beats(&self) -> &[FusedBeat; FUSED_OUTPUT_BEATS] {
86 &self.beats
87 }
88
89 #[must_use]
91 pub const fn into_beats(self) -> [FusedBeat; FUSED_OUTPUT_BEATS] {
92 self.beats
93 }
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum OutputFrameError {
99 NonFinalKeep {
101 beat: usize,
103 actual: u64,
105 },
106 FinalKeep {
108 actual: u64,
110 },
111 Last {
113 beat: usize,
115 actual: bool,
117 },
118 NonzeroPadding {
120 lane: usize,
122 actual: u8,
124 },
125}
126
127impl fmt::Display for OutputFrameError {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match *self {
130 Self::NonFinalKeep { beat, actual } => {
131 write!(
132 formatter,
133 "beat {beat} has noncanonical keep mask {actual:#018x}"
134 )
135 }
136 Self::FinalKeep { actual } => {
137 write!(
138 formatter,
139 "final beat has noncanonical keep mask {actual:#018x}"
140 )
141 }
142 Self::Last { beat, actual } => {
143 write!(formatter, "beat {beat} has noncanonical last={actual}")
144 }
145 Self::NonzeroPadding { lane, actual } => {
146 write!(
147 formatter,
148 "invalid final byte lane {lane} contains {actual:#04x}"
149 )
150 }
151 }
152 }
153}
154
155impl std::error::Error for OutputFrameError {}
156
157#[must_use]
178pub fn pack_fused_output<P: Profile>(
179 signature: &Signature<P>,
180 public_key: &PublicKey<P>,
181) -> FusedFrame<P> {
182 let signature = signature.to_bytes();
183 let public_key = public_key.to_bytes();
184 let mut beats = [FusedBeat::default(); FUSED_OUTPUT_BEATS];
185 for (beat_index, beat) in beats.iter_mut().enumerate() {
186 let stream_offset = beat_index * OUTPUT_BEAT_BYTES;
187 for (lane, output) in beat.data.iter_mut().enumerate() {
188 let source = stream_offset + lane;
189 *output = if source < SIGNATURE_BYTES {
190 signature[source]
191 } else if source < SIGNATURE_BYTES + PUBLIC_KEY_BYTES {
192 public_key[source - SIGNATURE_BYTES]
193 } else {
194 0
195 };
196 }
197 beat.keep = if beat_index + 1 == FUSED_OUTPUT_BEATS {
198 FINAL_KEEP
199 } else {
200 FULL_KEEP
201 };
202 beat.last = beat_index + 1 == FUSED_OUTPUT_BEATS;
203 }
204 FusedFrame::from_beats(beats)
205}
206
207pub fn unpack_fused_output<P: Profile>(
218 frame: &FusedFrame<P>,
219) -> Result<(Signature<P>, PublicKey<P>), OutputFrameError> {
220 let beats = frame.beats();
221 let mut signature = [0_u8; SIGNATURE_BYTES];
222 let mut public_key = [0_u8; PUBLIC_KEY_BYTES];
223
224 for (beat_index, beat) in beats.iter().enumerate() {
225 let final_beat = beat_index + 1 == FUSED_OUTPUT_BEATS;
226 if !final_beat && beat.keep != FULL_KEEP {
227 return Err(OutputFrameError::NonFinalKeep {
228 beat: beat_index,
229 actual: beat.keep,
230 });
231 }
232 if final_beat && beat.keep != FINAL_KEEP {
233 return Err(OutputFrameError::FinalKeep { actual: beat.keep });
234 }
235 if beat.last != final_beat {
236 return Err(OutputFrameError::Last {
237 beat: beat_index,
238 actual: beat.last,
239 });
240 }
241
242 let stream_offset = beat_index * OUTPUT_BEAT_BYTES;
243 for (lane, byte) in beat.data.iter().copied().enumerate() {
244 let destination = stream_offset + lane;
245 if destination < SIGNATURE_BYTES {
246 signature[destination] = byte;
247 } else if destination < SIGNATURE_BYTES + PUBLIC_KEY_BYTES {
248 public_key[destination - SIGNATURE_BYTES] = byte;
249 } else if byte != 0 {
250 return Err(OutputFrameError::NonzeroPadding { lane, actual: byte });
251 }
252 }
253 }
254 let mut segments = [[0_u8; HASH_BYTES]; CHAIN_COUNT];
255 for (segment, bytes) in segments.iter_mut().zip(signature.chunks_exact(HASH_BYTES)) {
256 segment.copy_from_slice(bytes);
257 }
258 let mut public_seed = [0_u8; HASH_BYTES];
259 let mut public_key_hash = [0_u8; HASH_BYTES];
260 public_seed.copy_from_slice(&public_key[..HASH_BYTES]);
261 public_key_hash.copy_from_slice(&public_key[HASH_BYTES..]);
262 Ok((
263 Signature::new(segments),
264 PublicKey::new(public_seed, public_key_hash),
265 ))
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use hashsigs_types::LegacyKeccak;
272
273 fn signature_fixture() -> [u8; SIGNATURE_BYTES] {
274 core::array::from_fn(|index| {
275 let folded = (index.wrapping_mul(149) ^ (index >> 3)) & 0xff;
276 u8::try_from(folded).expect("fixture byte is masked to eight bits")
277 })
278 }
279
280 fn public_key_fixture() -> [u8; PUBLIC_KEY_BYTES] {
281 core::array::from_fn(|index| {
282 0xa5_u8.wrapping_add(u8::try_from(index).expect("public-key fixture has 64 bytes"))
283 })
284 }
285
286 fn typed_fixture() -> (Signature<LegacyKeccak>, PublicKey<LegacyKeccak>) {
287 let signature =
288 Signature::from_slice(&signature_fixture()).expect("fixture signature has exact size");
289 let public_key = PublicKey::from_slice(&public_key_fixture())
290 .expect("fixture public key has exact size");
291 (signature, public_key)
292 }
293
294 #[test]
295 fn canonical_layout_matches_segment_boundaries() {
296 let signature_bytes = signature_fixture();
297 let public_key_bytes = public_key_fixture();
298 let (signature, public_key) = typed_fixture();
299 let frame = pack_fused_output(&signature, &public_key);
300 let beats = frame.beats();
301
302 for beat in &beats[..34] {
303 assert_eq!(beat.keep, u64::MAX);
304 assert!(!beat.last);
305 }
306 assert_eq!(&beats[0].data, &signature_bytes[..64]);
307 assert_eq!(&beats[32].data, &signature_bytes[2_048..2_112]);
308 assert_eq!(&beats[33].data[..32], &signature_bytes[2_112..]);
309 assert_eq!(&beats[33].data[32..], &public_key_bytes[..32]);
310 assert_eq!(&beats[34].data[..32], &public_key_bytes[32..]);
311 assert_eq!(&beats[34].data[32..], &[0_u8; 32]);
312 assert_eq!(beats[34].keep, 0xffff_ffff);
313 assert!(beats[34].last);
314 }
315
316 #[test]
317 fn canonical_frame_round_trips() {
318 let (signature, public_key) = typed_fixture();
319 let unpacked = unpack_fused_output(&pack_fused_output(&signature, &public_key))
320 .expect("canonical output must unpack");
321 assert_eq!(unpacked, (signature, public_key));
322 }
323
324 #[test]
325 fn malformed_control_and_padding_are_rejected() {
326 let (signature, public_key) = typed_fixture();
327 let mut beats = pack_fused_output(&signature, &public_key).into_beats();
328 beats[12].keep ^= 1;
329 assert!(matches!(
330 unpack_fused_output(&FusedFrame::<LegacyKeccak>::from_beats(beats)),
331 Err(OutputFrameError::NonFinalKeep { beat: 12, .. })
332 ));
333
334 let mut beats = pack_fused_output(&signature, &public_key).into_beats();
335 beats[34].last = false;
336 assert!(matches!(
337 unpack_fused_output(&FusedFrame::<LegacyKeccak>::from_beats(beats)),
338 Err(OutputFrameError::Last { beat: 34, .. })
339 ));
340
341 let mut beats = pack_fused_output(&signature, &public_key).into_beats();
342 beats[34].data[63] = 1;
343 assert!(matches!(
344 unpack_fused_output(&FusedFrame::<LegacyKeccak>::from_beats(beats)),
345 Err(OutputFrameError::NonzeroPadding { lane: 63, .. })
346 ));
347 }
348}