Skip to main content

Authorization (ζA\zeta_A)

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 (α[c]\alpha[c]): Currently valid authorizers for core cc

  • Up to 4 authorizers can be active simultaneously
  • Work-packages must match one of these authorizers

Authorization Queue (ϕ[c]\phi[c]): 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 = 341 cores
  • MAX_AUTH_POOL_ITEMS = 4 authorizers per core
  • Queue size Q is 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 cc, 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 ϕ\phi 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:

  1. For each core, check if a work-report was guaranteed
  2. If yes, remove that authorizer from the pool
  3. Add authorizer from queue at slot % queue_length
  4. Keep only the last 4 authorizers in the pool

Dependencies:

  • state.alpha: Authorization pools
  • state.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 ϕ\phi (authorization queue)
  • Authorization transition reads from updated ϕ\phi'
  • 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