Join our community of builders on Discord!

Slashing & Rehabilitation

Lightchain AI workers stake 50,000 LCAI on mainnet (5,000 on testnet) 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 one of WorkerRegistry's slash functions (slash, slashWithBounty, slashWithBountyBps), all callable only by JobRegistry.
OffenseRate (bps)LCAI slashed at default rates (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 cap each rate at maxSlashBps, currently 5,000 (50%). The owner of AIConfig (the protocol Safe) can change maxSlashBps up to 10,000. The current values for timeoutSlashBps, completionTimeoutSlashBps, disputeSlashBps, minWorkerStake, suspensionThreshold, and suspensionCooldown are all defined in AIConfig and listed in Governance & Protocol Parameters.
Live values (checked 2026-10-21). Both networks have timeoutSlashBps, completionTimeoutSlashBps and disputeSlashBps at 0, set for the move to dispatcher-free mode. Mainnet additionally has suspensionThreshold raised to 1,000,000, so nothing suspends automatically there; testnet keeps the threshold at 3. Offences are still recorded at 0 bps and count once the rates and thresholds are restored. Testnet's ackTimeout is 119 s.
Read the live values before relying on the defaults quoted on this page:
CodeBASH

How a slash propagates

When any of the three offenses fires, the contracts execute the following sequence:
  1. JobRegistry calls one of WorkerRegistry's slash functions. They are onlyJobRegistry — no other contract or EOA can slash directly.
  2. The worker's stake is reduced by slashAmount. Nothing is burned: the amount goes to the slashedFunds pool in WorkerRegistry, minus a keeper bounty paid to the caller for timeouts (timeoutBountyBps, 10% today). Only the owner (the protocol Safe) can withdraw the pool, with withdrawSlashedFunds(), and it goes to the Treasury.
  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 network's perspective: its claimSession transactions revert (see dispatcher-free mode).

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 default rates, at least 3,000 LCAI of stake (three offenses at 2%); nothing while the rates are 0.

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, recover it in three steps: wait out the cooldown, top up the stake if a slash took it under the minimum, then reinstate. The lightchain-worker CLI has a command for each of the last two, from release v0.0.2 on. Each one checks the on-chain state first and sends nothing when the call would fail:
CodeBASH
Both read the worker's settings from the environment, as set up in Run a Worker on Testnet with the CLI. The same calls can be sent by hand with cast send against WorkerRegistry, which works with any worker version; the rest of this section shows that path. 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
getSuspendedUntil returns Unix seconds. The cooldown has expired once block.timestamp >= suspendedUntil. Compare against current chain time:
CodeBASH
You can also run preflight from the run-a-worker guide: its suspended line shows the offense count and when the cooldown ends. (status prints registration and keys only.)

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. Compare the current stake with the minimum, then top up the difference:
CodeBASH
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).
reinstate() has no fee — you only pay gas. Eligibility is read on-chain at claim time, so the worker can win sessions as soon as the transaction is mined.

4. Verify recovery

  1. Confirm WorkerReinstated is present in the transaction receipt:
    CodeBASH
  2. Re-run preflight from Run a Worker — Mainnet § Step 7: Preflight and confirm its line reads [PASS] suspended no.
  3. Tail the worker container logs and look for claimed session request 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.
  • The slash that reaches suspensionThreshold emits WorkerSuspended in the same transaction, before its own WorkerSlashed.
  • WorkerDeactivated is emitted per model only when a slash leaves the stake below minWorkerStake, also before WorkerSlashed. A suspension removes the worker from the eligible sets without it.
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 the owner of AIConfig (the protocol Safe).
ParameterDefaultNotes
minWorkerStake50,000 LCAI (5,000 on testnet)Required 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%)Setter bound: no single rate can exceed it (owner-adjustable up to 10,000)
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% (canary 60%); testnet 95% (canary 98%)Minimum cosine similarity for a consumer-filed dispute to clear
samplingRateBps500 (5%)Fraction of jobs sampled by the canary disputer
For the current values see Governance & Protocol Parameters.