Authorization ()
The Authorization system manages which work-packages are permitted to execute on each core, decoupling coretime purchase from work-package submission to support both Ethereum-style and Polkadot-style interaction patterns.
Purpose
Authorization solves a fundamental problem: How do we verify that a work-package is allowed to use a specific core?
In Ethereum, gas purchase happens at transaction submission. In Polkadot, parachain slots are purchased months in advance. JAM's authorization system supports both models and everything in between by introducing:
- Authorizers: Logic that determines if a work-package can use a core
- Tokens: Opaque data proving authorization eligibility
- Traces: Results from successful authorization checks
This enables flexible coretime markets and allocation strategies.
Key Concepts
Authorizers
An authorizer is identified by the hash of:
authorizer_hash = hash(PVM_code || configuration_blob)
The authorizer runs in-core (off-chain) to verify work-packages before guaranteeing. On-chain state only tracks which authorizers are valid for each core.
Authorization Pool vs Queue
Each core has two data structures:
Authorization Pool (): Currently valid authorizers for core
- Up to 4 authorizers can be active simultaneously
- Work-packages must match one of these authorizers
Authorization Queue (): Future authorizers waiting to be activated
- Circular array indexed by slot number
- Services can update this via privileged calls
State Structure
α ∈ [[H; ≤4]; 341] // Authorization pool (341 cores)
φ ∈ [[H; Q]; 341] // Authorization queue (Q slots)
// For each core c:
α[c] = [auth_hash_1, auth_hash_2, ...] // Active authorizers
φ[c] = [queued_auth_1, queued_auth_2, ...] // Queued authorizers
Constants:
CORE_COUNT = 341coresMAX_AUTH_POOL_ITEMS = 4authorizers per core- Queue size
Qis configurable
How It Works
State Transition
On each block, the authorization pool is updated for every core:
Step 1: Remove Used Authorizer
If a work-package was guaranteed for core , remove its authorizer from the pool:
for guarantee in block.extrinsic.guarantees:
if guarantee.report.core_index == c:
used_authorizer = guarantee.report.authorizer_hash
if used_authorizer in alpha[c]:
alpha[c].remove(used_authorizer)
This ensures each authorizer can only be used once (or a limited number of times).
Step 2: Add New Authorizer from Queue
Regardless of whether an authorizer was used, add a new one from the queue:
# Use slot number to index into circular queue
slot_index = header.slot % len(phi[c])
new_authorizer = phi[c][slot_index]
# Append to pool (keeping only newest MAX_AUTH_POOL_ITEMS)
alpha[c].append(new_authorizer)
alpha[c] = alpha[c][-4:] // Keep only last 4
The queue is circular: it cycles through based on slot number, providing deterministic rotation.
Complete Transition
def authorization_transition(state, block):
alpha_new = []
for core in range(CORE_COUNT):
pool = state.alpha[core].copy()
# Find and remove used authorizer
used_auth = None
for guarantee in block.extrinsic.guarantees:
if guarantee.report.core_index == core:
used_auth = guarantee.report.authorizer_hash
break
if used_auth and used_auth in pool:
pool.remove(used_auth)
# Add authorizer from queue
queue_index = block.header.slot % len(state.phi[core])
new_auth = state.phi[core][queue_index]
pool.append(new_auth)
# Keep only last 4 entries
alpha_new.append(pool[-4:])
state.alpha = alpha_new
return state
Authorization Workflow
1. Service Configures Authorization Queue
A privileged service (like the Coretime service) can update via exogenous calls:
# Service allocates coretime by setting authorizers
phi[core][future_slot] = new_authorizer_hash
This happens during accumulation (see Accumulation).
2. Validators Check Work-Packages In-Core
Before guaranteeing a work-package, validators run the authorizer:
# Off-chain validation (not part of state transition)
work_package = receive_work_package()
# Check if authorizer is in pool
if work_package.authorizer_hash not in alpha[work_package.core]:
reject("Authorizer not valid for this core")
# Run authorizer PVM code
result = execute_authorizer(
code=authorizer_code,
config=authorizer_config,
token=work_package.authorization_token
)
if result.success:
trace = result.trace
guarantee_work_package(work_package, trace)
else:
reject("Authorization failed")
3. On-Chain State Updates
When the guaranteed work-package appears on-chain:
# State transition removes used authorizer
alpha[core].remove(work_package.authorizer_hash)
# And adds next queued authorizer
alpha[core].append(phi[core][slot % Q])
Use Cases
Ethereum-Style: Pay-Per-Use
# Authorizer checks if token contains valid payment
def authorizer(work_package, token):
payment = decode_payment(token)
if payment.amount >= required_fee:
return Success(trace=payment)
return Failure()
# User submits work-package with payment token
work_package = {
"authorizer": payment_authorizer_hash,
"token": encode_payment(value=100),
"code": user_code
}
Polkadot-Style: Pre-Allocated Slots
# Authorizer checks if work-package hash is pre-registered
def authorizer(work_package, token):
if work_package.hash in registered_packages:
return Success(trace=work_package.hash)
return Failure()
# Parachain registers future work-packages in advance
phi[parachain_core] = [
authorizer_for_block_100,
authorizer_for_block_101,
# ...
]
Hybrid: Bulk Purchase + Flexible Usage
# Buy coretime in bulk
purchase_coretime(cores=[5, 6, 7], duration=1_epoch)
# Use flexibly across purchased cores
for core in [5, 6, 7]:
phi[core] = [my_authorizer] * EPOCH_LENGTH
# Submit work-packages as needed
submit_work_package(core=5, urgent_task)
submit_work_package(core=7, batch_job)
State Transition Equation
The authorization pool is updated for each core by removing any used authorizer and adding one from the queue:
α'[c] = (F(c) + φ'[c][slot mod |φ'[c]|]) [last 4 elements]
Where F(c) removes the used authorizer if a work-report was guaranteed:
- If core c has a guaranteed report: remove its authorizer from α[c]
- Otherwise: keep α[c] unchanged
Key points:
- Used authorizers are removed from the pool
- New authorizers are added from the circular queue
- Pool is capped at 4 authorizers (FIFO)
Implementation Details
Location: tessera/jam/state/transitions/authorization/authorization.py
Key Function:
Authorization.transition(pre_state, state, block) -> Sigma
Process:
- For each core, check if a work-report was guaranteed
- If yes, remove that authorizer from the pool
- Add authorizer from queue at
slot % queue_length - Keep only the last 4 authorizers in the pool
Dependencies:
state.alpha: Authorization poolsstate.phi: Authorization queues (updated during accumulation)block.extrinsic.guarantees: Guaranteed work-reports
Important Notes
Pool Size Limits
The pool is capped at 4 authorizers:
- Allows some flexibility (multiple valid authorizers)
- Prevents unbounded growth
- FIFO: oldest entries are dropped when pool is full
Queue Circularity
The queue is indexed by slot % queue_length:
- Provides deterministic rotation
- Services can pre-program authorization schedules
- Empty slots can use default/fallback authorizers
Authorization Happens Off-Chain
The actual authorization check (running PVM code) is not part of the state transition:
- Too expensive to run on-chain
- Validators perform checks before guaranteeing
- On-chain state only tracks which authorizers are valid, not the authorization logic itself
Timing
Authorization transition happens after accumulation:
- Accumulation may update (authorization queue)
- Authorization transition reads from updated
- This allows services to dynamically manage coretime
Error Conditions
# Invalid alpha length
if len(state.alpha) != CORE_COUNT:
raise ValueError("Invalid alpha length")
# Pool manipulation errors are prevented by:
# - Only adding from pre-defined queue
# - Only removing when work-report proves usage
# - Capping pool size at 4
Example Transition
Slot 1000, Core 5:
# Initial state
alpha[5] = [auth_A, auth_B, auth_C]
phi[5] = [auth_X, auth_Y, auth_Z, auth_W, ...] # 600 entries
# Block contains guarantee for core 5 using auth_B
guarantee = Guarantee(
core=5,
authorizer=auth_B,
work_report=...
)
# State transition:
# 1. Remove used authorizer
alpha[5].remove(auth_B) # → [auth_A, auth_C]
# 2. Add from queue
queue_index = 1000 % 600 # = 400
new_auth = phi[5][400] # = auth_Z
alpha[5].append(auth_Z) # → [auth_A, auth_C, auth_Z]
# Final state
alpha[5] = [auth_A, auth_C, auth_Z]
References
- Gray Paper: Section on Authorization
- Work-Packages: See Reports for how authorizers are used
- Accumulation: See Accumulation for queue updates
- Implementation:
tessera/jam/state/transitions/authorization/
Next: Assurances | Disputes