Skip to main content

tape_sha256/
batch.rs

1//! Message framing, transposition, and the multi-buffer driver loop
2
3use crate::{
4    core::{compress, H0},
5    lanes::Lanes,
6};
7
8/// Largest lane count any backend uses
9///
10/// Sizes the stack scratch buffers so the driver never allocates.
11pub(crate) const MAX_LANES: usize = 16;
12
13/// Largest chunk any driver stages: the 2x16 interlace takes 32 messages
14///
15/// Separate from MAX_LANES so hash_lanes' per-group scratch does not double
16/// for backends that never need it.
17pub(crate) const MAX_WIDTH: usize = 32;
18
19pub(crate) const BLOCK: usize = 64;
20
21/// One message to hash: an optional shared prefix plus a body
22///
23/// Keeping the prefix separate lets the Merkle-leaf case skip materialising
24/// `prefix || body` per message; the driver reads from both slices directly.
25#[derive(Clone, Copy)]
26pub struct Message<'a> {
27    pub prefix: &'a [u8],
28    pub body: &'a [u8],
29    /// Optional third segment, hashed after `body`
30    pub tail: &'a [u8],
31}
32
33impl<'a> Message<'a> {
34    #[inline]
35    pub fn new(body: &'a [u8]) -> Self {
36        Message {
37            prefix: &[],
38            body,
39            tail: &[],
40        }
41    }
42
43    #[inline]
44    pub fn prefixed(prefix: &'a [u8], body: &'a [u8]) -> Self {
45        Message {
46            prefix,
47            body,
48            tail: &[],
49        }
50    }
51
52    /// A message of `prefix || left || right`, for Merkle interior nodes
53    #[inline]
54    pub fn pair(prefix: &'a [u8], left: &'a [u8], right: &'a [u8]) -> Self {
55        Message {
56            prefix,
57            body: left,
58            tail: right,
59        }
60    }
61
62    #[inline]
63    pub(crate) fn len(&self) -> usize {
64        self.prefix.len() + self.body.len() + self.tail.len()
65    }
66
67    /// Blocks after padding: message, 0x80 terminator, 64-bit big-endian length
68    #[inline]
69    pub(crate) fn blocks(&self) -> usize {
70        (self.len() + 1 + 8).div_ceil(BLOCK)
71    }
72
73    /// True when block `k` lies wholly inside `body`, so no staging copy is needed
74    ///
75    /// For a 1KB Merkle leaf that is 15 of 17 blocks, which is nearly all the
76    /// per-batch memcpy.
77    #[inline]
78    pub(crate) fn block_is_interior(&self, k: usize) -> bool {
79        let start = k * BLOCK;
80        // Must end before `tail` begins, not merely before the message does.
81        start >= self.prefix.len() && start + BLOCK <= self.prefix.len() + self.body.len()
82    }
83
84    /// Borrows block `k` straight out of `body`
85    ///
86    /// Only valid when block_is_interior says so.
87    #[inline]
88    pub(crate) fn interior_block(&self, k: usize) -> &'a [u8] {
89        let off = k * BLOCK - self.prefix.len();
90        &self.body[off..off + BLOCK]
91    }
92
93    /// Writes block `k` of the padded message into `out`
94    #[inline]
95    pub(crate) fn fill_block(&self, k: usize, out: &mut [u8; BLOCK]) {
96        let start = k * BLOCK;
97        let len = self.len();
98        let plen = self.prefix.len();
99
100        out.fill(0);
101
102        if start < plen {
103            let n = (plen - start).min(BLOCK);
104            out[..n].copy_from_slice(&self.prefix[start..start + n]);
105        }
106        // Body, then tail if there is one.
107        let bend = plen + self.body.len();
108        let from = start.max(plen);
109        let to = (start + BLOCK).min(bend);
110        if from < to {
111            let off = from - start;
112            let n = to - from;
113            let sfrom = from - plen;
114            out[off..off + n].copy_from_slice(&self.body[sfrom..sfrom + n]);
115        }
116        if !self.tail.is_empty() {
117            let tend = bend + self.tail.len();
118            let from = start.max(bend);
119            let to = (start + BLOCK).min(tend);
120            if from < to {
121                let off = from - start;
122                let n = to - from;
123                let sfrom = from - bend;
124                out[off..off + n].copy_from_slice(&self.tail[sfrom..sfrom + n]);
125            }
126        }
127        // Terminator lands in whichever block the message ends in.
128        if start <= len && len < start + BLOCK {
129            out[len - start] = 0x80;
130        }
131        // Length field only exists in the last block.
132        if start + BLOCK == self.blocks() * BLOCK {
133            let bits = (len as u64).wrapping_mul(8);
134            out[BLOCK - 8..].copy_from_slice(&bits.to_be_bytes());
135        }
136    }
137}
138
139/// Shared length split of a batch, for the same-shape fast path
140///
141/// Merkle batches share one prefix length, body length, and total length,
142/// so block interiority is a property of the block index alone: decide it
143/// once per block instead of per lane, and every interior source pointer
144/// is then pure arithmetic. Body length is part of the shape, not just the
145/// total, because two messages can split one total differently between
146/// body and tail, and the interior path indexes `body` alone.
147///
148/// The bound every same-shape fast path leans on: when `same` holds and
149/// `k` is in `k_lo..k_hi`, then `k * BLOCK >= plen` and
150/// `k * BLOCK + BLOCK <= plen + body.len()`, so the 64 bytes at
151/// `body_ptr.add(k * BLOCK - plen)` lie wholly inside every lane's `body`.
152pub(crate) struct Shape {
153    pub(crate) same: bool,
154    pub(crate) plen: usize,
155    pub(crate) same_prefix: bool,
156    pub(crate) k_lo: usize,
157    pub(crate) k_hi: usize,
158}
159
160impl Shape {
161    /// Computes the shared shape; `msgs` must be non-empty
162    #[inline(always)]
163    pub(crate) fn of(msgs: &[Message<'_>]) -> Shape {
164        let p0 = msgs[0].prefix;
165        let plen = p0.len();
166        let blen = msgs[0].body.len();
167        let len = msgs[0].len();
168        let mut same = true;
169        let mut same_prefix = true;
170        for m in msgs {
171            same &= m.prefix.len() == plen && m.body.len() == blen && m.len() == len;
172            same_prefix &= m.prefix == p0;
173        }
174        Shape {
175            same,
176            plen,
177            same_prefix,
178            k_lo: plen.div_ceil(BLOCK),
179            k_hi: (plen + blen) / BLOCK,
180        }
181    }
182}
183
184/// Stages the prefix-straddling block for every lane from one template.
185#[inline]
186pub(crate) fn stage_prefix_block(
187    msgs: &[Message<'_>],
188    shape: &Shape,
189    staging: &mut [[u8; BLOCK]],
190) -> bool {
191    let plen = shape.plen;
192    if !shape.same_prefix || !(1..BLOCK).contains(&plen) {
193        return false;
194    }
195    let bn = BLOCK - plen;
196
197    if !msgs.iter().all(|m| m.body.len() >= bn) {
198        return false;
199    }
200    let mut tmpl = [0u8; BLOCK];
201    tmpl[..plen].copy_from_slice(&msgs[0].prefix[..plen]);
202    for (s, m) in staging.iter_mut().zip(msgs) {
203        *s = tmpl;
204        s[plen..].copy_from_slice(&m.body[..bn]);
205    }
206    true
207}
208
209#[inline]
210pub(crate) fn write_digest<const W: usize>(state: &[[u32; W]; 8], lane: usize, out: &mut [u8; 32]) {
211    for (i, chunk) in out.chunks_exact_mut(4).enumerate() {
212        chunk.copy_from_slice(&state[i][lane].to_be_bytes());
213    }
214}
215
216/// Hashes up to `L::N` messages in lockstep, one per lane
217///
218/// Lengths may differ; a lane that finishes early has its digest taken and
219/// then idles to the end of the longest lane.
220///
221/// `W` sizes the per-call scratch and must be `L::N`; callers pass
222/// `{ <T as Lanes>::N }`. It cannot be read off `L` directly because array
223/// lengths may not depend on an associated const. Sizing it to the lane count
224/// rather than `MAX_LANES` is worth ~8% at one lane, where the fixed-size form
225/// zeroed 2.2 KB of stack per call to use a sixteenth of it.
226#[inline(always)]
227pub(crate) fn hash_lanes<L: Lanes, const W: usize>(msgs: &[Message<'_>], out: &mut [[u8; 32]]) {
228    // A wider backend would silently overrun the staging arrays.
229    const { assert!(L::N <= MAX_LANES) };
230    const { assert!(L::N == W) };
231    assert!(msgs.len() <= L::N);
232    assert_eq!(msgs.len(), out.len());
233    let n = msgs.len();
234    if n == 0 {
235        return;
236    }
237
238    let mut state = H0.map(L::splat);
239
240    // Needed per lane per iteration, so derive them once up front.
241    let mut nblocks = [0usize; W];
242    let mut max_blocks = 0usize;
243    for (lane, m) in msgs.iter().enumerate() {
244        let b = m.blocks();
245        nblocks[lane] = b;
246        max_blocks = max_blocks.max(b);
247    }
248    let uniform = nblocks[..n].iter().all(|&b| b == max_blocks);
249
250    let mut blocks = [[0u8; BLOCK]; W];
251    let mut unpacked = [[0u32; W]; 8];
252
253    let shape = Shape::of(msgs);
254    let mut bases: [*const u8; W] = [std::ptr::null(); W];
255    for (b, m) in bases.iter_mut().zip(msgs) {
256        *b = m.body.as_ptr();
257    }
258    let mut staged = [usize::MAX; W];
259    let mut kk = [0usize; W];
260    let mut interior = [false; W];
261
262    let mut srcs: [*const u8; W] = [std::ptr::null(); W];
263    let staged0 = stage_prefix_block(msgs, &shape, &mut blocks);
264    for k in 0..max_blocks {
265        // Pre-filled then overwritten.
266        if shape.same {
267            if k >= shape.k_lo && k < shape.k_hi {
268                let off = k * BLOCK - shape.plen;
269                for (s, base) in srcs.iter_mut().zip(bases.iter()).take(n) {
270                    // SAFETY: the interior bound documented on `Shape`.
271                    *s = unsafe { base.add(off) };
272                }
273            } else {
274                if !(k == 0 && staged0) {
275                    for (lane, m) in msgs.iter().enumerate() {
276                        m.fill_block(k, &mut blocks[lane]);
277                    }
278                }
279                for (lane, b) in blocks.iter().enumerate().take(n) {
280                    srcs[lane] = b.as_ptr();
281                }
282            }
283        } else {
284            // Stage only blocks straddling the prefix, terminator, or length.
285            if k == 0 && staged0 {
286                kk[..n].fill(0);
287                interior[..n].fill(false);
288                staged[..n].fill(0);
289            } else {
290                for (lane, m) in msgs.iter().enumerate() {
291                    let idx = k.min(nblocks[lane] - 1);
292                    kk[lane] = idx;
293                    let is_interior = m.block_is_interior(idx);
294                    interior[lane] = is_interior;
295                    if !is_interior && staged[lane] != idx {
296                        m.fill_block(idx, &mut blocks[lane]);
297                        staged[lane] = idx;
298                    }
299                }
300            }
301            // Separate pass so the staging array is borrowed immutably here.
302            for (lane, m) in msgs.iter().enumerate() {
303                srcs[lane] = if interior[lane] {
304                    m.interior_block(kk[lane]).as_ptr()
305                } else {
306                    blocks[lane].as_ptr()
307                };
308            }
309        }
310        // SAFETY: every lane below `n` was just set to a pointer valid for a
311        // full 64-byte block, either into a borrowed body or into `blocks`.
312        let w = unsafe { L::transpose(&srcs, n) };
313        compress::<L>(&mut state, w);
314
315        // Uniform lanes all finish together.
316        if !uniform {
317            let mut any = false;
318            for lane in 0..n {
319                if nblocks[lane] != k + 1 {
320                    continue;
321                }
322                if !any {
323                    for (i, s) in state.iter().enumerate() {
324                        s.store(&mut unpacked[i][..L::N]);
325                    }
326                    any = true;
327                }
328                write_digest(&unpacked, lane, &mut out[lane]);
329            }
330        }
331    }
332
333    if uniform {
334        for (i, s) in state.iter().enumerate() {
335            s.store(&mut unpacked[i][..L::N]);
336        }
337        for (lane, o) in out.iter_mut().enumerate().take(n) {
338            write_digest(&unpacked, lane, o);
339        }
340    }
341}
342
343/// Hashes `prefix || left || right` per pair, without materialising the join
344///
345/// # Safety
346///
347/// As for `drive`.
348pub(crate) unsafe fn drive_pairs(
349    width: usize,
350    group: GroupFn,
351    prefix: &[u8],
352    left: &[&[u8]],
353    right: &[&[u8]],
354    out: &mut [[u8; 32]],
355) {
356    // Staging is sized to the caller's width class so narrow backends do
357    // not pay MAX_WIDTH's init: 32 slots is 1.5KB of stores per call, and
358    // only the interlace reads past 16.
359    if width == 1 {
360        drive_pairs_staged::<1>(width, group, prefix, left, right, out)
361    } else if width <= MAX_LANES {
362        drive_pairs_staged::<MAX_LANES>(width, group, prefix, left, right, out)
363    } else {
364        drive_pairs_staged::<MAX_WIDTH>(width, group, prefix, left, right, out)
365    }
366}
367
368unsafe fn drive_pairs_staged<const W: usize>(
369    width: usize,
370    group: GroupFn,
371    prefix: &[u8],
372    left: &[&[u8]],
373    right: &[&[u8]],
374    out: &mut [[u8; 32]],
375) {
376    debug_assert!(width <= W);
377    assert_eq!(left.len(), right.len(), "left and right must pair up");
378    assert_eq!(
379        left.len(),
380        out.len(),
381        "output slice must have one digest per pair"
382    );
383    let mut staging = [Message::new(&[]); W];
384    for ((l, r), o) in left
385        .chunks(width)
386        .zip(right.chunks(width))
387        .zip(out.chunks_mut(width))
388    {
389        for ((slot, a), b) in staging.iter_mut().zip(l).zip(r) {
390            *slot = Message::pair(prefix, a, b);
391        }
392        group(&staging[..l.len()], o);
393    }
394}
395
396/// One lane-group's worth of hashing, the unit the drivers dispatch through.
397pub(crate) type GroupFn = unsafe fn(&[Message<'_>], &mut [[u8; 32]]);
398
399/// Hashes every message, `width` at a time, through `group`
400///
401/// # Safety
402///
403/// `group`'s CPU-feature contract must hold on this machine, and `width` must
404/// be the lane count `group` was monomorphized for.
405pub(crate) unsafe fn drive(
406    width: usize,
407    group: GroupFn,
408    msgs: &[Message<'_>],
409    out: &mut [[u8; 32]],
410) {
411    assert_eq!(
412        msgs.len(),
413        out.len(),
414        "output slice must have one digest per message"
415    );
416    for (m, o) in msgs.chunks(width).zip(out.chunks_mut(width)) {
417        group(m, o);
418    }
419}
420
421/// Hashes bare byte slices without materialising a `Vec<Message>`
422///
423/// The public wrappers take `&[&[u8]]`, so attaching a prefix would otherwise
424/// cost an allocation per call. Chunks are built on the stack instead; the
425/// intended caller is a hot validator path.
426///
427/// # Safety
428///
429/// As for drive.
430pub(crate) unsafe fn drive_slices(
431    width: usize,
432    group: GroupFn,
433    prefix: &[u8],
434    bodies: &[&[u8]],
435    out: &mut [[u8; 32]],
436) {
437    // See drive_pairs for why staging is width-classed.
438    if width == 1 {
439        drive_slices_staged::<1>(width, group, prefix, bodies, out)
440    } else if width <= MAX_LANES {
441        drive_slices_staged::<MAX_LANES>(width, group, prefix, bodies, out)
442    } else {
443        drive_slices_staged::<MAX_WIDTH>(width, group, prefix, bodies, out)
444    }
445}
446
447unsafe fn drive_slices_staged<const W: usize>(
448    width: usize,
449    group: GroupFn,
450    prefix: &[u8],
451    bodies: &[&[u8]],
452    out: &mut [[u8; 32]],
453) {
454    debug_assert!(width <= W);
455    assert_eq!(
456        bodies.len(),
457        out.len(),
458        "output slice must have one digest per message"
459    );
460    let mut staging = [Message::new(&[]); W];
461    for (chunk, o) in bodies.chunks(width).zip(out.chunks_mut(width)) {
462        for (slot, body) in staging.iter_mut().zip(chunk) {
463            *slot = Message::prefixed(prefix, body);
464        }
465        group(&staging[..chunk.len()], o);
466    }
467}