Skip to main content

Assurances (ζU\zeta_U)

The Assurances transition processes availability assurances from validators, marking work-reports as available once a supermajority confirms they hold the necessary data.

Purpose

Before a work-report can be integrated into state, validators must prove they have:

  1. Their erasure-coded chunk of the work-package
  2. Their chunks for all exported segments

Assurances are signed statements from validators confirming data availability. Once ≥⅔ of validators assure a work-report, it becomes available and can be integrated.

State Structure

ρ ∈ [Option<WorkReportState>; 341]  // Pending work-reports

WorkReportState {
report: WorkReport, // The pending work-report
timeout: Slot // Slot when report expires if not assured
}

Assurance Extrinsic:

Assurance {
anchor: H, // Parent block hash
bitfield: [bool; 341], // Which cores this validator assures
validator_index: u16, // Index of assuring validator
signature: Ed25519Sig // Signature over anchor + bitfield
}

How It Works

1. Assurance Submission

Validators submit assurances in blocks:

# Validator i has chunks for work-reports on cores [0, 5, 12]
assurance = Assurance(
anchor=block.parent_hash,
bitfield=[True, False, False, False, False, True, ...], // 341 bools
validator_index=42,
signature=sign(ed25519_key, msg)
)

# Message signed:
msg = b"$jam_available" + blake2b(anchor + bitfield.encode())

2. Validation

Each assurance must satisfy:

# Anchor matches block parent
assurance.anchor == block.header.parent

# Validator index is valid
assurance.validator_index < VALIDATOR_COUNT

# Signature is valid
verify_ed25519(
kappa[assurance.validator_index].ed25519,
assurance.signature,
b"$jam_available" + blake2b(anchor + bitfield)
)

# Bitfield matches pending reports
for core in range(341):
if bitfield[core]:
assert rho[core] is not None // Core must have pending report

3. Assurances Must Be Ordered and Unique

# Sort by validator index
assurances.sort(key=lambda a: a.validator_index)

# No duplicates
assert len(assurances) == len(set(a.validator_index for a in assurances))

4. Counting Assurances

core_assurances = [0] * 341

for assurance in block.extrinsic.assurances:
for core in range(341):
if assurance.bitfield[core] and rho[core] is not None:
core_assurances[core] += 1

5. Marking as Available

supermajority = floor(2 * VALIDATOR_COUNT / 3)  //682
newly_available = []

for core in range(341):
if rho[core] is None:
continue

# Supermajority reached
if core_assurances[core] > supermajority:
newly_available.append(rho[core].report)
rho[core] = None // Remove from pending

6. Timeout Handling

If a work-report doesn't get assured within the timeout period, it's discarded:

UNAVAILABLE_WORK_EXPIRY = 8  # slots

for core in range(341):
if rho[core] is not None:
if current_slot >= rho[core].timeout + UNAVAILABLE_WORK_EXPIRY:
rho[core] = None // Expired, remove from pending

State Transition

def assurances_transition(state, block):
rho = state.rho
kappa = state.kappa

# Validate all assurances
for assurance in block.extrinsic.assurances:
validate_assurance(assurance, block, kappa)

# Count assurances per core
core_assurances = count_assurances(block.extrinsic.assurances, rho)

# Mark available or expired
newly_available = []
supermajority = floor(2 * VALIDATOR_COUNT / 3)

for core in range(341):
if rho[core] is None:
continue

# Check supermajority
if core_assurances[core] > supermajority:
newly_available.append(rho[core].report)
rho[core] = None
# Check timeout
elif block.header.slot >= rho[core].timeout + 8:
rho[core] = None

state.rho = rho
return state, newly_available

Complete Workflow

Step 1: Work-Report Guaranteed

# From guarantees extrinsic (previous transition)
rho[core] = WorkReportState(
report=work_report,
timeout=current_slot
)

Step 2: Validators Acquire Data

# Off-chain: validators fetch erasure-coded chunks
chunk = fetch_chunk(work_report_hash, validator_index)
exported_chunks = [fetch_export_chunk(seg, validator_index)
for seg in work_report.exports]

# Verify chunks
verify_chunk_inclusion(chunk, work_report.erasure_root, validator_index)

Step 3: Validators Submit Assurances

# Validator creates assurance for all cores they have data for
bitfield = [False] * 341
for core in cores_with_complete_data:
bitfield[core] = True

assurance = create_assurance(bitfield, validator_index, block.parent)
broadcast(assurance)

Step 4: Block Author Includes Assurances

# Block author collects assurances from network
block.extrinsic.assurances = collect_assurances()

Step 5: State Transition Marks Available

# On-chain transition processes assurances
state, newly_available = assurances_transition(state, block)

# Newly available reports proceed to integration
for report in newly_available:
integrate_report(report) # Next transition phase

Security Properties

Supermajority Requirement

  • Threshold: >⅔ validators (>682 out of 1023)
  • Guarantee: If >⅔ assure, data is retrievable even if ⅓ are malicious/offline
  • Erasure Coding: Each chunk can reconstruct full data with ⅔ chunks

Timeout Protection

  • Duration: 8 slots (48 seconds)
  • Purpose: Prevents stale reports from blocking cores indefinitely
  • Trade-off: Must be long enough for validators to fetch and assure

Fork Safety

  • Anchor: Assurances reference parent block hash
  • Prevents: Assurances from one fork being reused on another fork
  • Ensures: Assurances are specific to one chain history

Implementation Details

Location: tessera/jam/state/transitions/assurances/assurances.py

Key Functions:

Assurances.transition(pre_state, state, block) -> (Sigma, WorkReports)

Returns: Updated state + list of newly available work-reports

Validations:

  • ensure_validators_valid(): Check validator indices
  • ensure_valid_signature(): Verify Ed25519 signatures
  • ensure_assurances_order(): Check ascending order
  • ensure_assurances_unique(): No duplicate validators

Constants:

  • VALIDATOR_COUNT = 1023
  • UNAVAILABLE_WORK_EXPIRY = 8 slots
  • Supermajority = floor(2 * 1023 / 3) = 682

Error Conditions

class AssurancesErrorCode:
BAD_ATTESTATION_PARENT # Anchor ≠ block parent
BAD_SIGNATURE # Invalid Ed25519 signature
BAD_VALIDATOR_INDEX # Index >= VALIDATOR_COUNT
CORE_NOT_ENGAGED # Bitfield set for empty core
NOT_SORTED_OR_UNIQUE # Assurances not ordered or duplicated

Example Transition

Slot 1000:

# Initial state: 3 pending work-reports
rho[5] = WorkReportState(report=WR_A, timeout=995)
rho[12] = WorkReportState(report=WR_B, timeout=998)
rho[20] = WorkReportState(report=WR_C, timeout=1000)

# Block contains 700 assurances
assurances = [
Assurance(anchor=parent, bitfield=[..., True at 5, ...], index=0, sig=...),
Assurance(anchor=parent, bitfield=[..., True at 5, 12, ...], index=1, sig=...),
# ... 698 more assurances
]

# Count assurances:
core_assurances[5] = 690 # > 682 ✓ supermajority
core_assurances[12] = 650 # < 682 ✗ not enough
core_assurances[20] = 700 # > 682 ✓ supermajority

# State transition:
newly_available = [WR_A, WR_C]
rho[5] = None # Available
rho[12] = pending # Still waiting
rho[20] = None # Available

# These reports proceed to integration

Integration with Other Transitions

Before Assurances:

  • Guarantees: Work-reports are added to ρ as pending

After Assurances:

  • Accumulation: Newly available reports are integrated into service state
  • Disputes: Can invalidate assured reports if proven faulty
  • Auditing: Off-chain process that precedes assurances

References

  • Gray Paper: Section on Availability Assurance
  • Erasure Coding: Appendix on data availability
  • Auditing: See Auditing for off-chain validation
  • Implementation: tessera/jam/state/transitions/assurances/

Next: Disputes | Reports