Assurances ()
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:
- Their erasure-coded chunk of the work-package
- 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 indicesensure_valid_signature(): Verify Ed25519 signaturesensure_assurances_order(): Check ascending orderensure_assurances_unique(): No duplicate validators
Constants:
VALIDATOR_COUNT = 1023UNAVAILABLE_WORK_EXPIRY = 8slots- 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
Related:
- 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/