Expand description
A slice’s key factory: one complete one-time key every 68 cycles.
This is the hardware form of slice_model::model_key:
it issues exactly the requests listed there, in the order fixed by the
slice_schedule, and writes every chain node to
the slice’s cache.
§The story so far
DSig signs a message with a one-time key: a W-OTS+ key with Winternitz
parameter w = 4. Such a key is 68 hash chains (64 message digits plus 4
checksum digits). Each chain starts at an 18-byte secret and is hashed
three times, so it has four positions, 0 to 3. Position 3 is the public
chain end. When we sign, each message digit tells us which position of its
chain to reveal. Making keys ahead of time, and storing all four positions,
turns signing into “look up 68 nodes and copy them out”.
A one-time key must never sign twice. So every key gets a fresh number
(batch, index), and its secrets are derived from that number. 128 keys
form a batch; the hash of each key’s chain ends is a leaf of a Merkle
tree, and one Ed25519 signature certifies the batch root. A finished DSig
signature is 1,642 bytes.
This module is the slice’s key maker. Its job, every 68 clock cycles at the 205 MHz design clock, is:
- derive the key’s secret seed from
(epoch, batch, index)(BLAKE3); - derive two nonces from the seed: the public chain salt
pk_nonceand the private message nonce; - expand the seed into 68 × 18 secret bytes (a BLAKE3 XOF, an “extendable output”, read as 20 blocks of 64 bytes);
- push the 68 chain secrets through the chain engine, one per cycle, and write all four positions of each chain into the cache;
- hash
pk_nonceand the 68 chain ends into the key’spk_hash(the endpoint commitment, 20 BLAKE3 blocks plus one parent); - report the finished key (
KeyDone) so the subtree unit can usepk_hashas a Merkle leaf.
§The reasoning behind the shape
Step 4 sets the pace: the chain engine (crate::slice_chain) accepts one
chain per cycle, and a key has 68 chains, so a new key must start every 68
cycles. Steps 1, 2, 3 and 5 need 44 BLAKE3 compressions per key
(1 seed + 2 nonces + 20 XOF + 20 commitment blocks + 1 parent), and two
more slots per period are lent to outsiders (see below): 46 in all. A
dedicated hash core per
step would cost far too many LUTs, so they all share one pipelined
BLAKE3 lane that accepts one compression per cycle. 46 of every 68 cycles
are therefore busy; the rest are idle.
Sharing creates two problems:
- Who uses the lane in this cycle? Several keys are in flight at once
(a key’s work spans several 68-cycle periods). Instead of an arbiter, we
use a fixed modulo schedule (
crate::slice_schedule): every op of a key has a fixed offset from that key’s start, and no two offsets are equal modulo 68. So the cycle number modulo 68 (the residue) alone says which op runs. The table lookup is all the control logic there is. - Where does each in-flight key keep its half-done state? Each key in
flight owns a context: one entry in a small ring of RAMs indexed by the
key’s number modulo
CONTEXTS. The seed, nonces and BLAKE3 chaining values of a key live in its context until the key is done.
An op’s lag is how many 68-cycle periods after its key’s start it runs
(negative for ops that run before chain 0). From the current period and an
op’s lag, the factory knows which key, and thus which context, the op
belongs to: context = (period − lag) mod CONTEXTS.
§Picture
residue r = cycle mod 68 ──▶ timetable ──▶ (op, key context)
│
prefix_cv, epoch, batch ──┐ ▼
context RAMs (seed, CVs) ─┼──▶ request mux ──▶ BLAKE3 lane (28 stages, II=1)
commitment words ─────────┘ │ result + tag
▲ ▼
│ write back into the key's context RAMs
packer ◀── chain ends ◀── chain engine ◀── XOF window ◀── XOF RAM
│
└──▶ cache writes (all four positions)Read the picture from the top. A counter gives the residue; the timetable
(SLOTS) turns it into an op and a lag; the lag picks the key context.
The request mux builds that op’s 64-byte BLAKE3 block from the run
configuration and the context RAMs, and the lane hashes it. 28 cycles
later the result comes out with its tag (a 32-bit label that says which
op and which context it belongs to) and is written back into that
context. XOF results go to a separate XOF RAM. From there, a two-block
window feeds 18-byte secrets into the chain engine, one per cycle. Chain
nodes go to the cache, and chain ends go through the packer into the
commitment RAM. Later commitment ops read them back as BLAKE3 blocks.
§Where the data lives
| Storage | Width × depth | Written by | Read by |
|---|---|---|---|
contexts.seed | 8 words × CONTEXTS | Seed result | nonce and XOF requests |
contexts.pkn_feed | 16 bytes × CONTEXTS | PkNonce result | chain-salt latch (residue 66/67) |
contexts.pkn_meta | 16 bytes × CONTEXTS | PkNonce result | KeyDone metadata |
contexts.nonce | 16 bytes × CONTEXTS | Nonce result | KeyDone metadata |
contexts.cv0 / cv1 | 8 words × CONTEXTS | chunk-0 / chunk-1 results | next block of that chunk, parent |
xof_ram | 16 words × 64 | Xof results | chain window |
commit_ram | 64 bytes × (32 × CONTEXTS) | packer | chunk requests |
pk_nonce is stored twice because two different readers need it at
different addresses in the same cycle, and each RAM has one read port.
§Cycle contract
Key k is decided (real or bubble) KEY_LEAD periods before its chain
0 enters the chain engine at residue 0 of period k. Each op of key k
issues at cycle 68k + offset, the offsets coming from
SCHEDULE. Every op is two registered steps:
- decode (cycle
y − 1): look up residuey mod 68, compute the key’s context index, present context-RAM read addresses, register the op; - issue (cycle
y): assemble the request from the RAM outputs and feed the lane.
Results leave the lane 28 cycles after issue, are registered once, and are
written into the key’s context RAM. They are readable by an op issuing
use_delay cycles after the
producer, which the schedule guarantees.
cycle: y−1 y y+1 … y+27 y+28 y+29
decode issue lane stages lane output result register
residue request ─▶ BLAKE3 lane ─▶ (q.lane) ─▶ (q.data.result)
lookup, enters ─▶ context-RAM write
RAM addr lane
An op that consumes this result issues use_delay = 28 + 4 = 32 cycles
after y, once the written value can be read back.§How a key moves through periods
One period is 68 cycles. Key k’s chain 0 enters the chain engine at
residue 0 of period k. Its preparation ops (seed, nonces, XOF) run in
earlier periods, its commitment ops in later ones. Meanwhile other keys
do their own ops in the same periods, on other residues:
period: k−KEY_LEAD … k−1 k k+1 …
key k: decided seed, nonces, XOF chains 0..67 commitment, parent → KeyDone
key k+1: decided … seed, XOF … chains 0..67 …The ring of CONTEXTS contexts must be larger than the number of
periods a key spans, so a context is never reused while its key still has
ops pending; compile-time assertions below check this.
The Merkle and message slots carry requests from outside (the subtree unit
and the signer). A request offered on SliceFactoryInput::merkle or
SliceFactoryInput::message is taken at that slot’s residue, signalled
by merkle_taken or message_taken, and its 16 result words return on
SliceFactoryOutput::slot_result with the caller’s tag.
§Safety rules carried by this block
- Each real key gets a fresh
(batch, index)from a monotonic counter; a key index is never issued twice within a run, and reset ends the run. - Bubble keys issue nothing: their slots are idle.
- The secret prefix CV is an input, never stored beyond the run’s config.
§Neighbours
crate::slice_schedulecomputes the timetable this block follows.crate::slice_chainis the chain engine fed one chain per cycle.crate::chain_lanepicks the chain hash (Haraka-512 by default) and with it the chain engine’s latency, which shapesEND_DELAY,PACK_HISTORYandCTX_BITS.crate::packerturns 18-byte chain ends into 64-byte commitment words.crate::blake3_coresupplies the shared BLAKE3 lane.crate::slice_modelis the software model the tests compare against.crate::slicewires this factory to the cache, subtree unit and signer.
Modules§
- code
- Op codes in the residue table.
Structs§
- Cache
Write - Four positions of one chain for the cache.
- Chain
Pick - Where chain
i’s 18 secret bytes sit in the two-block XOF window. - Context
Rams - The six small per-key context RAMs, grouped into one child.
- Context
Rams In - Ports of
ContextRams, one simple-dual-port interface each. - Context
Rams Out - Registered read data of
ContextRams. - Decoded
- A decoded op waiting to issue next cycle.
- Factory
Config - Run configuration; hold stable while
runis set. - Factory
Control - Resettable control state.
- Factory
Data - Resetless datapath state.
- KeyDone
- One finished key’s public and private metadata.
- KeyEntry
- Per-key bookkeeping, one entry per context.
- Slice
Factory - The factory: one BLAKE3 lane, one chain engine and their bookkeeping.
- Slice
Factory Input - Factory inputs.
- Slice
Factory Output - Factory outputs.
- Slot
- One timetable row in RHDL types.
- Slot
Request - A request for one of the two externally fed slots.
- Slot
Result - Result of a Merkle or message slot request.
- Window
Load - XOF window load: which block (if any) to address at each residue.
Constants§
- CACHE_
ADDRESS_ BITS - Bits of a cache-bank row address.
- CACHE_
ROWS - Rows of each cache bank: keys packed densely,
CHAINSrows per key. - CACHE_
SLOTS - One-time keys the slice cache can hold (ring size): 32 batches of 16.
- CHAIN_
PICKS - Chain-pick table as RHDL constants.
- CONTEXTS
- Key contexts held at once; must exceed the span of op lags.
- CTX_
BIAS LAG_BIASas aCtx.- CTX_
BITS - Bits of a key-context index (and of an op lag). The SHA-512 chain lanes (82 cycles each) stretch one key over more 68-cycle periods, so that profile needs 32 contexts; Haraka and BLAKE3 need 16.
- CTX_
INDEX - Every context index as a
Ctx(kernels compare against these). - CTX_
LEAD KEY_LEADas aCtx.- CTX_ONE
1as aCtx.- END_
DELAY - Cycles from a chain’s issue until its commitment word is readable.
- FLAGS_
END_ ROOT CHUNK_END | ROOT: profile 2’s message tail block.- FLAGS_
PARENT - Flags of the commitment parent.
- FLAGS_
SEED - Flags of the seed request.
- FLAGS_
SINGLE - Flags of a complete single-block root hash.
- FLAG_
END - Flag marking a chunk’s last block.
- FLAG_
START - Flag marking a chunk’s first block.
- IV_BITS
- BLAKE3 IV words.
- KEY_
LEAD - Periods between deciding key
kand its chain 0: one before its first op. - KEY_
LEAD_ U KEY_LEADas ausize, usable inside RHDL kernels.- LAG_
BIAS - Bias added to lags so they fit an unsigned
CTX_BITS-bit field. - LAG_
BIAS_ U LAG_BIASas ausize, usable inside RHDL kernels.- NONCE_
WORD - Message-nonce derivation prefix word.
- PACK_
HISTORY - Earlier keys’
pk_nonces kept for the commitment prefix. - PK_
NONCE_ WORD pk_noncederivation prefix word.- SCHEDULE
- The timetable this factory implements.
- SLOTS
- The residue table as RHDL constants.
- SLOT_
BITS - Bits of a cache slot number.
- TWO_
BLOCK_ MESSAGE - Profile 2 (
DSIG_MESSAGE_BYTES=64): the message digest is two blocks. - WINDOW_
LOADS - Window-load table as RHDL constants.
Functions§
- bytes_
from_ words_ kernel - Words to little-endian bytes.
- cache_
address_ kernel - Dense cache row of
(slot, chain):slot × 68 + chain. - context_
rams_ kernel - Pure wiring between the grouped RAM ports and their children.
- lane_
tag_ kernel - Pack (op, ctx) into a lane tag; slot requests keep their 16-bit tag above.
- pick_
secret_ kernel - Pick a chain’s 18 secret bytes from the two-block window.
- ring_
entry_ kernel - Read one ring entry by dynamic context index.
- slice_
factory_ kernel - The factory’s wiring.
- words_
from_ bytes_ kernel - Little-endian bytes to words.
Type Aliases§
- Commit
Address - Commitment-RAM address: context and word.
- Ctx
- A key-context index (wraps modulo
CONTEXTS).