Skip to main content

Module slice_factory

Module slice_factory 

Source
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:

  1. derive the key’s secret seed from (epoch, batch, index) (BLAKE3);
  2. derive two nonces from the seed: the public chain salt pk_nonce and the private message nonce;
  3. expand the seed into 68 × 18 secret bytes (a BLAKE3 XOF, an “extendable output”, read as 20 blocks of 64 bytes);
  4. push the 68 chain secrets through the chain engine, one per cycle, and write all four positions of each chain into the cache;
  5. hash pk_nonce and the 68 chain ends into the key’s pk_hash (the endpoint commitment, 20 BLAKE3 blocks plus one parent);
  6. report the finished key (KeyDone) so the subtree unit can use pk_hash as 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

StorageWidth × depthWritten byRead by
contexts.seed8 words × CONTEXTSSeed resultnonce and XOF requests
contexts.pkn_feed16 bytes × CONTEXTSPkNonce resultchain-salt latch (residue 66/67)
contexts.pkn_meta16 bytes × CONTEXTSPkNonce resultKeyDone metadata
contexts.nonce16 bytes × CONTEXTSNonce resultKeyDone metadata
contexts.cv0 / cv18 words × CONTEXTSchunk-0 / chunk-1 resultsnext block of that chunk, parent
xof_ram16 words × 64Xof resultschain window
commit_ram64 bytes × (32 × CONTEXTS)packerchunk 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:

  1. decode (cycle y − 1): look up residue y mod 68, compute the key’s context index, present context-RAM read addresses, register the op;
  2. 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

Modules§

code
Op codes in the residue table.

Structs§

CacheWrite
Four positions of one chain for the cache.
ChainPick
Where chain i’s 18 secret bytes sit in the two-block XOF window.
ContextRams
The six small per-key context RAMs, grouped into one child.
ContextRamsIn
Ports of ContextRams, one simple-dual-port interface each.
ContextRamsOut
Registered read data of ContextRams.
Decoded
A decoded op waiting to issue next cycle.
FactoryConfig
Run configuration; hold stable while run is set.
FactoryControl
Resettable control state.
FactoryData
Resetless datapath state.
KeyDone
One finished key’s public and private metadata.
KeyEntry
Per-key bookkeeping, one entry per context.
SliceFactory
The factory: one BLAKE3 lane, one chain engine and their bookkeeping.
SliceFactoryInput
Factory inputs.
SliceFactoryOutput
Factory outputs.
Slot
One timetable row in RHDL types.
SlotRequest
A request for one of the two externally fed slots.
SlotResult
Result of a Merkle or message slot request.
WindowLoad
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, CHAINS rows 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_BIAS as a Ctx.
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_LEAD as a Ctx.
CTX_ONE
1 as a Ctx.
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 k and its chain 0: one before its first op.
KEY_LEAD_U
KEY_LEAD as a usize, usable inside RHDL kernels.
LAG_BIAS
Bias added to lags so they fit an unsigned CTX_BITS-bit field.
LAG_BIAS_U
LAG_BIAS as a usize, 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_nonce derivation 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§

CommitAddress
Commitment-RAM address: context and word.
Ctx
A key-context index (wraps modulo CONTEXTS).