Join our community of builders on Discord!

Slashing & Rehabilitation

Lightchain AI workers stake 50,000 LCAI to be eligible for inference jobs. That stake is the lever the protocol pulls when a worker misbehaves: the stake is reduced (slashed), and after enough offenses the worker is temporarily removed from the eligible set (suspended). Suspension is always recoverable — there is no permanent tombstoning in the current design — but the operator must explicitly call reinstate() once the cooldown has passed. This page covers every offense that can lead to a slash, how the slash propagates through the contracts, the states a worker can end up in, and the exact runbook for getting a suspended worker back online.

Slashing offenses

A worker can be slashed for three reasons. All three are enforced by JobRegistry and result in a call to WorkerRegistry.slash(worker, bps).
OffenseRate (bps)LCAI burned (at 50k stake)Triggering function
Acknowledgement timeout — worker did not ack a job within ackTimeout (default 90 s)200 (2%)1,000JobRegistry.claimTimeout(jobId) while the job is in Submitted state
Completion timeout — worker acked but did not submit a result within completionTimeout (default 120 s)500 (5%)2,500JobRegistry.claimTimeout(jobId) while the job is in Acknowledged state
Lost dispute / response mismatch — Disputer (or consumer) proves the worker's response is invalid1500 (15%)7,500JobRegistry.resolveDispute(jobId, true, …) or JobRegistry.disputeResponseMismatch(…)
The slash amount is computed as:
CodeHTML
The setters that update these rates are gated by a governance bound maxSlashBps = 5000 (50%), so no future proposal can set a single offense rate higher than half of minWorkerStake. The current values for timeoutSlashBps, completionTimeoutSlashBps, disputeSlashBps, minWorkerStake, suspensionThreshold, and suspensionCooldown are all defined in AIConfig and listed in Governance & Protocol Parameters.

How a slash propagates

When any of the three offenses fires, the contracts execute the following sequence:
  1. JobRegistry calls WorkerRegistry.slash(worker, bps). The function is onlyJobRegistry — no other contract or EOA can slash directly.
  2. The worker's stake is reduced by slashAmount and the burned amount is added to a protocol-level slashedFunds accounting bucket.
  3. WorkerSlashed(address indexed worker, uint256 amount, uint256 newStake) is emitted.
  4. If newStake < minWorkerStake, the worker is immediately removed from every model's eligible set. WorkerDeactivated(worker, modelId) is emitted once per model. The worker can no longer be assigned new jobs.
  5. The worker's offenseCount is incremented.
  6. If offenseCount >= suspensionThreshold (default 3), the worker is suspended: isSuspended = true, suspendedUntil = block.timestamp + suspensionCooldown (default 7 days), and WorkerSuspended(worker, until) is emitted. The worker is again removed from every eligible set if not already.
A slashed-but-not-suspended worker with stake still ≥ minWorkerStake keeps receiving jobs. Only the suspended state — or the below-minimum-stake state — actually takes the worker offline from the dispatcher's perspective.

Worker states

StateHow it is reachedEligible for new jobs?Recovery
ActiveRegistered, stake ≥ minimum, offenseCount < 3Yes
Slashed (still active)At least one slash, stake still ≥ minimum, offenseCount < 3YesNone needed — keep operating cleanly
Below-minimum stakeA slash dropped stake under minWorkerStakeNoCall topUpStake() to top stake back up to ≥ minimum
SuspendedoffenseCount >= suspensionThreshold (default 3)No, for suspensionCooldown (default 7 days)Wait out the cooldown, top up if needed, then call reinstate()
reinstate() clears both isSuspended and resets offenseCount to 0. There is no permanent removal — a worker can be suspended, reinstated, suspended again, and reinstated again indefinitely. Each suspension consumes another 7 days of downtime and at least 15,000 LCAI of stake (three offenses at the cheapest rate).

How disputes lead to slashing

The most expensive offense (15%) comes from losing a dispute. Two paths trigger it:
  • Consumer-filed dispute. A consumer who is unhappy with the response can call JobRegistry.disputeJob(jobId) within disputeWindow (default 24 h) by posting a bond proportional to the job fee. The Disputer service re-executes the prompt against the same model and compares the worker's response to the re-execution using cosine similarity. If similarity < similarityThreshold (default 60%), the verdict is "worker guilty" and the worker is slashed by disputeSlashBps. The consumer's bond is returned and the job fee is refunded.
  • Canary sampling. Independently of consumer behavior, the Disputer canary-samples ~samplingRateBps (default 5%) of completed jobs and runs the same re-execution check. A guilty verdict slashes the worker identically.
  • Response mismatch. If a consumer can produce the worker's signed ciphertext and prove keccak256(ciphertext) != job.responseCiphertextHash, calling JobRegistry.disputeResponseMismatch(jobId, ciphertext, signature) slashes the worker by disputeSlashBps without going through similarity scoring at all.
See AIVM EL Architecture — Verification for the full disputer pipeline.

Rehabilitation runbook

If your worker has been suspended, the recovery flow is straightforward but currently requires raw contract calls — there is no lightchain-worker reinstate subcommand yet. The canonical path is cast send against WorkerRegistry. The examples below assume you already have these environment variables set as in the standard Run a Worker — Mainnet guide:
CodeBASH
(The WorkerRegistry address above is the mainnet precompile-style fixed address; see Contract Addresses for the canonical list.)

1. Confirm the worker is suspended and read the cooldown deadline

CodeBASH
The returned tuple contains isSuspended (bool) and suspendedUntil (uint256, Unix seconds). The cooldown has expired once block.timestamp >= suspendedUntil. Compare against current chain time:
CodeBASH
You can also use the container-based status check from the run-a-worker guide, which prints registration and suspension flags in one call.

2. Top up stake if it fell below the minimum

If the slashes that caused suspension also pushed your stake under 50,000 LCAI, reinstate() will clear the suspension flag but will not re-add you to model eligible sets — the contract requires stake >= minWorkerStake for that. Check current stake (read directly from the worker info struct in getWorker(...)'s return tuple), then top up the difference:
CodeBASH
topUpStake() is not a substitute for reinstate(). Topping up alone does not clear the isSuspended flag — you must still call reinstate() once the cooldown has elapsed. Always treat reinstate() as the canonical path out of suspension.

3. Call reinstate() once the cooldown has passed

CodeBASH
This call:
  • Reverts with StillSuspended(worker, suspendedUntil) if the cooldown has not elapsed.
  • Reverts with WorkerNotSuspended(worker) if the worker is not currently suspended.
  • Otherwise: clears isSuspended, resets offenseCount = 0, and re-adds the worker to the eligible set for every supported model that is still whitelisted and enabled (assuming stake >= minWorkerStake).
  • Emits WorkerReinstated(address indexed worker).
[!NOTE] reinstate() has no fee — you only pay gas. The dispatcher listens for WorkerReinstated and marks the worker "stale" until the next heartbeat, so expect a short delay (one heartbeat interval, ~10 s) before the dispatcher resumes assigning jobs.

4. Verify recovery

  1. Confirm WorkerReinstated is present in the transaction receipt:
    CodeBASH
  2. Re-run the status check from Run a Worker — Mainnet § Check registration status and confirm is_suspended is false.
  3. Tail the worker container logs and look for fresh heartbeats and ws_job_received lines.

Events emitted

The authoritative event signatures from IWorkerRegistry.sol:
CodeSOLIDITY
Useful correlations the indexer or any off-chain monitor should track:
  • WorkerSlashed typically pairs with the JobTimedOut event or with a DisputeResolved(workerGuilty=true) event in the same transaction.
  • WorkerSuspended is always preceded by the third WorkerSlashed that crossed the threshold.
  • WorkerDeactivated may be emitted per model on either a below-minimum-stake slash or a suspension.
The indexed worker activity feed in the Worker API surfaces all of these.

Parameter reference

These are the defaults at genesis. All can be changed by governance through AIConfig.
ParameterDefaultNotes
minWorkerStake50,000 LCAIRequired to be eligible for any model
timeoutSlashBps200 (2%)Ack-timeout slash rate
completionTimeoutSlashBps500 (5%)Completion-timeout slash rate
disputeSlashBps1,500 (15%)Lost-dispute slash rate
maxSlashBps5,000 (50%)Governance setter bound — no single rate can exceed this
suspensionThreshold3Offenses before automatic suspension
suspensionCooldown604,800 s (7 days)Minimum wait before reinstate() succeeds
ackTimeout90 sWorker must ack a job within this
completionTimeout120 sWorker must complete an acked job within this
similarityThreshold60%Minimum cosine similarity for a consumer-filed dispute to clear
samplingRateBps500 (5%)Fraction of jobs sampled by the canary disputer
For the canonical, governance-mutable view see Governance & Protocol Parameters.