Expand description
The fixed timetable that lets one BLAKE3 lane serve a whole slice.
This module is pure compile-time arithmetic: no hardware. It answers one
question for crate::slice_factory: “in which cycle does each BLAKE3
compression of a key run?” The factory then turns the answer into small
constant tables (SLOTS,
WINDOW_LOADS) that a 0..=67
counter indexes.
§The problem
A slice starts one key every KEY_PERIOD = 68 cycles,
because its chain engine accepts one of the key’s 68 chains per cycle. Each
key needs 46 BLAKE3 compressions (seed, two nonces, 20 XOF blocks, a 21-block
endpoint commitment, a Merkle share and a message hash), and they all share
one pipelined BLAKE3 lane that accepts one compression per cycle.
Several keys are in flight at once, and some compressions depend on others,
up to 16 in a strict chain.
(The “21-block endpoint commitment” counts its 20 data blocks plus the parent that joins its two chunks: 3 + 20 + 21 + 2 = 46 slots, out of 68.)
§The trick: a modulo schedule
Give every compression of a key a fixed offset from the cycle T when that
key’s chain 0 enters the chain engine. Key k starts at T₀ + 68k, so its
compression with offset x issues at T₀ + 68k + x. Two keys collide on the
lane only if two offsets are equal mod 68. So if every offset in the table
has a different residue mod 68, no two compressions ever collide, however
many keys overlap.
cycle mod 68: 0 1 2 3 4 5 … 67
op issued: X7 C0 · P X8 C1 … one op (or idle) per residue
│ └ chunk-0 block of the key that started 1 period ago
└ XOF block 7 of the key that starts next periodThe hardware is then a 68-entry table (Schedule::residue) and a counter,
with no arbiter. To stall, a slice simply does not start a key in some
period; every compression of that key becomes an idle slot.
Words used below:
| Term | Meaning |
|---|---|
T | the cycle at which the key’s chain 0 enters the chain engine |
| offset | an op’s issue cycle minus T; negative = before chain 0 |
| residue | a cycle number mod 68; the hardware’s counter value |
| period | 68 consecutive cycles, residue 0 to 67 |
| lag | offset div 68: in which period, relative to the key’s own, the op runs |
D | Schedule::use_delay: cycles from issue until a result can be used |
§One key’s life on the timeline
offset: ≪0 ───────────────── 0 ────────── 67 ─────────────────────────▶
Seed ─D─▶ PkNonce, Nonce
└D─▶ Xof(0..19), each D before the first chain that needs it
chains 0..67 enter, one per cycle
chain ends emerge (end_delay later)
Chunk0(0) ─≥D─▶ Chunk0(1) … Chunk0(15)
Chunk1(0) ─≥D─▶ … Chunk1(3)
─D─▶ Parent → pk_hashPreparation ops are placed as late as possible (so their results wait in storage for the shortest time), and commitment ops as early as possible (so the key finishes soon). Each op then lands on the nearest residue no other op has taken.
§Constraints the solver enforces
A result is usable Schedule::use_delay cycles after its compression
issues: the BLAKE3 latency, one output register, then a write into and a
registered read from the slice’s per-key context RAM (latency + 4).
- Seed first. Nonces and XOF blocks hash the seed: each issues ≥
Dafter it. - XOF in time. Chain
ineeds bytes18i..18i+18of the XOF stream when it issues atT + i, so each XOF block is ready before its first chain. - Salt in time.
pk_noncesalts every chain, so it is ready beforeT. - Commitment follows the chain ends. The commitment hashes
pk_nonce ‖ end₀ ‖ … ‖ end₆₇(1,240 bytes) as 20 blocks: chunk 0 is blocks 0–15, chunk 1 is blocks 16–19. Blockjneeds every chain end that touches it, and each chunk’s blocks chain through their chaining values, so consecutive blocks are ≥Dapart. - Parent last. The
pk_hashparent combines both chunks.
The Merkle-share and message slots are reserved residues whose data comes
from elsewhere; their readiness is checked by the controllers that use them.
tests/slice_schedule.rs re-verifies every constraint independently.
§How the solver works
solve is a greedy, compile-time search. A table of 68 “used” flags
records which residues are taken.
- Chunk 0 first: 16 blocks, each needing the previous one’s chaining
value, is the hardest fit.
solve_chunktries every startbaseand every spacinggap ≥ D(within one period of the earliest choice) and keeps the placement that finishes earliest without collisions. - Chunk 1 the same way, around what chunk 0 took.
- The parent at the first free residue
Dafter both chunks. - XOF blocks, last to first, each at the latest free residue that is
still
Dbefore its first chain. - The two nonces at the latest free residue
DbeforeT. - The seed at the latest free residue
Dbefore its earliest user. - The Merkle and message slots at the first free residues from offset 0.
If any step finds no room, solve fails at compile time, so a bad
timetable can never reach hardware.
Structs§
- Schedule
- A complete, collision-free timetable for one slice.
Enums§
- Op
- One kind of compression in the per-key timetable.
Constants§
- CHUN
K0_ BLOCKS - Endpoint-commitment blocks in the first 1,024-byte chunk.
- CHUN
K1_ BLOCKS - Endpoint-commitment blocks in the 216-byte second chunk.
- COMMITMENT_
BYTES - Bytes hashed by the endpoint commitment:
pk_nonceand 68 chain ends. - KEY_
PERIOD - Cycles between consecutive key starts in a slice (one chain per cycle).
- OPS
- Number of scheduled compressions or reserved slots per key.
- TAIL_
SLACK - Cycles between a message result leaving the lane and its tail request being ready (result register, signer latch, request register, margin).
- XOF_
BLOCKS - XOF output blocks holding the 68 eighteen-byte chain secrets.
Functions§
- block_
ready - First cycle (relative to
T) at which commitment blockblockis complete. - first_
chain_ of_ xof - First chain that reads XOF block
block. - op_at
- The op stored at position
indexofSchedule::offsets. - op_
index - Index of an op in
Schedule::offsets; the inverse ofop_at. - solve
- Build the timetable for a BLAKE3 latency and a chain-end delay.