Safrole
Safrole is JAM's block production consensus mechanism, named after the Sassafras protocol of which it is a simplified variant. It determines which validator may author each block while maintaining anonymity and generating high-quality randomness.
Purpose
SAFROLE serves three critical functions:
- Block Author Selection: Limits block authoring to one validator per 6-second timeslot
- Fork Prevention: Prevents multiple valid blocks at the same height under normal operation
- Randomness Generation: Produces unbiasable entropy for use across the protocol
It's a simplified variant of the Sassafras consensus mechanism, specifically designed for JAM's architecture.
Core Concepts
Tickets and Anonymity
Unlike traditional block production where the next author is publicly known, SAFROLE uses tickets to maintain anonymity:
- Validators submit anonymous Ring-VRF proofs
- Each proof demonstrates: "I control a key in the validator set" (without revealing which)
- The VRF output becomes a ticket ID - an unbiasable random value
- The best-scoring tickets win slots for the next epoch
This means:
- Before submission: No one knows who will author future blocks
- During production: Only the author knows their identity
- After production: The author's identity may remain anonymous (optional reveal)
Epochs and Timekeeping
Epoch: 600 slots (1 hour at 6 seconds/slot)
epoch_index = slot / 600
slot_phase = slot % 600 // Position within epoch
Each epoch has three phases:
- Ticket Submission (slots 0-299): Validators submit tickets for next epoch
- Transition Window (slots 300-599): Ticket processing complete
- Epoch Change (slot 0 of next epoch): Seal keys rotate, validators rotate
State Structure
SAFROLE State Components
γ ≡ ⟨p, z, s, a⟩ // Gamma: SAFROLE state
p ∈ [ValidatorKey; V] // Pending validator set (V ≈ 1023)
z ∈ RingRoot // Bandersnatch ring root
s ∈ SealKeys // Current epoch's sealing keys
a ∈ [Ticket; ≤600] // Ticket accumulator for next epoch
Validator Keys
Each validator has a composite key containing four cryptographic components:
ValidatorKey {
bandersnatch: [u8; 32], // For anonymous Ring-VRF tickets
ed25519: [u8; 32], // For validator identification
bls: [u8; 144], // For BLS signature schemes
metadata: [u8; 128] // Node address and other metadata
}
Ticket Structure
Ticket {
id: H, // VRF output hash (32 bytes)
attempt: u8 // Entry index (0-2 per validator)
}
Entropy State
η ∈ [H; 4] // Entropy array
η[0] = entropy_accumulator // Current entropy (updated each block)
η[1] = entropy_epoch_1 // Entropy at epoch e-1 end
η[2] = entropy_epoch_2 // Entropy at epoch e-2 end
η[3] = entropy_epoch_3 // Entropy at epoch e-3 end
How SAFROLE Works
Phase 1: Ticket Submission (Slots 0-299)
Validators create and submit tickets for the next epoch:
# Each validator can submit up to 3 tickets (attempts 0, 1, 2)
for attempt in range(3):
# Create Ring-VRF proof
message = b"$jam_ticket" + entropy[2] + bytes([attempt])
signature = ring_vrf_sign(message, validator_key, validator_ring)
# Extract VRF output as ticket ID
ticket_id = vrf_output(signature)[:32]
# Submit ticket
tickets.append(Ticket(id=ticket_id, attempt=attempt))
Ticket Validation:
- Verify Ring-VRF proof against ring root
z - Ensure attempt is valid (0-2)
- Check submission is before slot 300 of current epoch
- Verify tickets are sorted by ID
- No duplicate ticket IDs allowed
Accumulation:
# Tickets are accumulated and kept sorted by ID
gamma.a.append(ticket)
gamma.a.sort(key=lambda t: t.id)
gamma.a = gamma.a[:600] # Keep only best 600 tickets
Phase 2: Epoch Transition
At the start of a new epoch (when new_epoch > old_epoch):
2.1 Validator Rotation
# Rotate validator sets
lambda_ = kappa // Previous validators
kappa = gamma.p // Current validators
gamma.p = iota // Pending becomes current
# Filter out offenders (from disputes)
for validator in iota:
if validator.ed25519 in offenders:
gamma.p[i] = NULL_VALIDATOR // Replace with zeros
2.2 Entropy Rotation
# Shift entropy history
eta = [
entropy_accumulator, // New eta[0]
eta[0], // Old accumulator → eta[1]
eta[1], // eta[1] → eta[2]
eta[2] // eta[2] → eta[3]
]
2.3 Seal Key Selection
Two modes based on ticket availability:
Ticketed Mode (Normal Operation):
if len(gamma.a) == 600 and epoch_jump == 1:
# Use outside-in sequencing on accumulated tickets
seal_keys = outside_in(gamma.a)
gamma.s = SealKeys.Ticketed(seal_keys)
Outside-in sequencing interleaves best and worst tickets to balance security:
[ticket[0], ticket[599], ticket[1], ticket[598], ...]
Fallback Mode (Insufficient Tickets):
else:
# Use deterministic fallback based on entropy
fallback_keys = generate_fallback(eta[2], kappa)
gamma.s = SealKeys.Fallback(fallback_keys)
2.4 Ring Root Update
# Compute new ring root from pending validators
bandersnatch_keys = [v.bandersnatch for v in gamma.p]
gamma.z = get_ring_root(bandersnatch_keys)
Phase 3: Block Sealing
Every block must be sealed by the validator holding the slot's seal key:
# Get seal key for current slot
slot_index = slot % 600
seal_key = gamma.s[slot_index]
# Create seal signature
unsigned_header = encode_header_without_seal(header)
if ticketed_mode:
message = b"$jam_ticket_seal" + eta[3] + ticket.attempt
header.seal_sig = sign(seal_key, message, unsigned_header)
else: # Fallback mode
message = b"$jam_fallback_seal" + eta[3]
header.seal_sig = sign(seal_key, message, unsigned_header)
# Create VRF signature for entropy
entropy_message = b"$jam_entropy" + vrf_output(header.seal_sig)
header.vrf_sig = sign(seal_key, entropy_message, [])
Seal Verification:
- Extract seal key from
gamma.s[slot % 600] - Verify seal signature matches seal key
- Verify VRF signature matches seal key
- Confirm message format (ticketed vs fallback)
Phase 4: Entropy Accumulation
Each block updates the entropy accumulator:
# Combine current entropy with VRF output
eta[0] = blake2b(eta[0] + vrf_output(header.vrf_sig))
This creates a bias-resistant randomness pool that accumulates over the entire epoch.
State Transition Equation
The complete SAFROLE transition updates both gamma and eta:
γ' = ⟨p', z', s', a'⟩
η' = [η'₀, η'₁, η'₂, η'₃]
Where transitions depend on epoch change, ticket submission phase, and seal verification.
Security Properties
Anonymity
- Ticket Submission: Ring-VRF hides which validator created which ticket
- Block Production: Author identity only known to author (unless revealed)
- Ring Size: ~1023 validators provide strong anonymity set
Unbiasability
- VRF Outputs: Cryptographically guaranteed to be unbiasable
- Entropy Accumulation: Combines outputs over entire epoch
- Historical Dependency: Uses entropy from 2 epochs ago to prevent manipulation
Fork Resistance
- Single Author: Only one key can seal each slot
- Ticketed Security: Tickets provide stronger fork resistance than fallback
- Best Chain Selection: Prioritizes ticketed blocks (see Best Chain section)
Implementation Details
Location: tessera/jam/state/transitions/safrole/
Key Files:
safrole.py: Main transition logicworker.py: Ticket verification workersexecutor.py: Ring-VRF execution setuperrors.py: SAFROLE-specific errors
Dependencies:
py-ark-vrf: Bandersnatch Ring-VRF implementation- Ring root generation and verification
- VRF output extraction
Constants:
EPOCH_LENGTH = 600slotsTICKET_SUBMISSION_END = 300(slot 299 is last)TICKET_ENTRIES_PER_VALIDATOR = 3attemptsMAX_TICKETS_PER_EXTRINSIC = 10per block
Error Conditions
class SafroleErrorCode:
BAD_SLOT # Slot less than current tau
UNEXPECTED_TICKET # Ticket after slot 299
INVALID_ATTEMPT # Attempt not in 0-2
DUPLICATE_TICKET # Same ticket ID twice
INVALID_VRF_PROOF # Ring-VRF verification failed
TICKETS_NOT_SORTED # Tickets not in ascending order
Example Transition
At Slot 1200 (Epoch 2, Slot 0):
old_epoch = 1, new_epoch = 2
# 1. Validator rotation
lambda_ = kappa # Validators from epoch 0
kappa = gamma.p # Validators from epoch 1
gamma.p = filter_offenders(iota) # Pending for epoch 2
# 2. Entropy rotation
eta = [eta[0], eta[0], eta[1], eta[2]]
# 3. Seal keys selection
if len(gamma.a) == 600:
gamma.s = outside_in(gamma.a) # Use tickets
else:
gamma.s = fallback_keys # Use deterministic fallback
# 4. Ring root update
gamma.z = compute_ring_root(gamma.p)
# 5. Reset ticket accumulator
gamma.a = []
References
- Gray Paper: Section 4 - Block Production and Chain Growth (Safrole)
- Sassafras: SAFROLE is a simplified variant
- Ring-VRF: Bandersnatch curve-based anonymous VRF
- Implementation:
tessera/jam/state/transitions/safrole/
Next: Authorization | Assurances