CIP-13 v2
Versioning. This is v2 of CIP-13. v1 is the canonical documentcip-13-runner-delegation.md(preserved verbatim as Part I). v2 = v1 + the alignment revision (Part II). Conflict rule: Part II is canonical wherever it contradicts Part I. Summary of v2 changes
- Opcode renumbering (deferred to activation). v1 §3.3’s 40–44 collide with code (
UpdateSettlementConfigthroughUpgradeActor); earlier v2 drafts proposed 44–48 (still in code’s 44–51 band) and then 52–56 (now taken by CIP-8 Session). §1 below records the code-authoritative master table as of 2026-05-26 and lists CIP-13 delegation handlers as pending-renumber to the free≥ 87range. Concrete numbers will be pinned in the implementation PR that lands CIP-13 v2 handlers innode/types/src/execution.rs.- Explicit amendment to CIP-2 §5/§6 for VRF selection and max-job-value: both now use
effective_stake = registration.stake + delegation_totals.total_active. v1 §3.2 implies this; v2 states it as a normative cross-CIP amendment.- Slashing routing reuses CIP-3
SettlementConfig.slash_*_percent(governance-tunable) instead of hard-coding 50/50 treasury/burn. Per-tranche math is unchanged.- CIP-23 interaction: TEE eligibility is categorical (
measurement_binding.status), not stake-thresholded. Delegation increases VRF weight but does not confer or remove TEE eligibility.- Carries forward v1 §6.1 zero-vote-weight rule for runner-delegated CBY.
Part I — v1 Specification (verbatim from cip-13-runner-delegation.md)
Status: Draft
Type: Standards Track
Category: Core
Created: 2026-04-12
Requires: CIP-12 (Governance)
1. Abstract
This CIP adds stake delegation to the Cowboy runner marketplace. Any CBY holder can lock tokens on behalf of a registered runner, increasing the runner’s effective stake (VRF selection weight and maximum job value) in exchange for a runner-configured share of the 89% settlement payout. The protocol provides only the delegation hook — registration, payout splitting, slashing cascade, and unbonding. Higher-order products (liquid staking pools, yield tokens, fleet management vaults) are built by third-party actors on top of this primitive.2. Motivation
In a self-bonded protocol, a runner must holdmax(10,000 CBY, 1.5× declared_max_job_value) in its own account to register (whitepaper §1.9, §5). This creates two problems:
- Capital lock-out. The best GPU operators may not have large CBY positions. The largest CBY holders may not operate hardware. Self-bonding fuses capital and operations into one party, limiting both.
- No passive yield path. CBY holders who are not runners have no way to earn runner marketplace revenue.
2.1 Design philosophy
The protocol provides the minimal hook. It does not implement pools, share tokens, diversification strategies, or yield wrappers. Following the precedent:3. Specification
3.1 New data structures
DelegationConfig
Added toRunnerRegistration. Runners opt into delegation by setting this field. The config carries the fields required by both the update flow (cooldown) and the settlement flow (epoch-delayed commission changes). All fields MUST be present; there is no ambiguity about where cooldown or pending commission data lives.
commission_bps is the runner’s cut of delegator-attributed revenue. If a runner earns 100 CBY on a job and 60% of the runner’s effective stake is delegated, then 60 CBY is the delegator-attributed portion. The runner keeps 60 × commission_bps / 10000 and delegators share the remainder pro-rata.
Epoch-delayed commission changes. A runner submits RunnerUpdateDelegationConfig to queue a new commission. The update writes pending_commission_bps = new_value and pending_effective_epoch = current_epoch + 1. The old commission_bps remains authoritative for every settlement that finalizes in the current epoch. At the first settlement in epoch pending_effective_epoch or later, the runtime promotes the pending value into commission_bps and clears the pending fields (see §3.4 effective_commission_for_epoch). This ensures commission changes are deterministic, observable one full epoch in advance, and resolvable with only the state above — no auxiliary history table is required.
The non-commission fields (max_delegated_stake, min_delegation, accept_delegation) take effect immediately on update, because they only gate future transactions; no past settlement depends on them.
DelegationTranche
A delegator’s position toward a single runner consists of one or more tranches. A tranche is an indivisible unit of delegated stake with its own lifecycle, status, and (when unbonding) its own claim timestamp. Partial undelegation, top-ups, and slashing all operate on tranches — never on a monolithic per-pair record.Claimable state. Instead, claimability and slashability are computed as pure functions of status, claimable_at, and the current block height:
RunnerClaimUnbonded (§3.3). Because they depend only on values already stored on the tranche and the block height at the moment of the operation, there is no state transition to schedule, no queue that could be delayed, and no window in which a tranche’s slashability or claimability disagrees with its advertised claimable_at. This is a deliberate simplification over earlier drafts that stored Claimable as a persisted status; see §3.8 for the bookkeeping implications.
A delegator MAY hold multiple Active tranches against the same runner (each top-up creates a new tranche) and MAY simultaneously hold Unbonding tranches at various stages of maturity. All tranches are tracked separately so that amounts, timestamps, and slashing state remain unambiguous.
Storage layout
Within the0x01 Runner Registry system actor (see §9 of the whitepaper for system actor addresses):
DelegationDelegatorSummary caches the per-(runner, delegator) counts and totals required by O(1) precondition checks:
DelegationTotals on every mutation (Delegate, Increase, Undelegate, Claim, slashing). When it would otherwise be zero/empty, the summary key is deleted to bound storage. active_tranche_count is the sole authoritative counter used to enforce MAX_ACTIVE_TRANCHES_PER_DELEGATOR in preconditions (§3.3); no instruction needs to iterate delegation_delegator_index to determine the count.
Active-delegator counts (at the runner level) are tracked in DelegationTotals: a delegator is counted once in delegator_count so long as delegation_delegator_summary.active_tranche_count > 0.
DelegationTotals is a cached aggregate to avoid iterating all tranches on every job dispatch or settlement:
total_active is cached at the runner level because that is what VRF selection weight, max-job-value, and settlement all require. The slashable base is computed lazily by the slashing routine itself (§3.6), which must iterate tranches anyway to apply per-tranche slashes; adding a total_slashable cache would provide no speedup and would require block-height-dependent invariants that are hard to maintain correctly. Amounts in Unbonding status are recoverable by iterating delegation_runner_index when needed (indexer reads and deregistration only).
3.2 Effective stake
A runner’s effective stake replaces the currentregistration.stake in all protocol-level calculations:
registration.stake is the runner’s self-bonded stake (unchanged from current behavior).
VRF selection weight (dispatcher.rs): the existing weight function w = effective_stake · max(√(reputation / REPUTATION_NORMALIZER), w_min_floor) (CIP-2 §5.3, v3_runner_weight in dispatcher.rs) uses effective_stake instead of registration.stake. Weight is linear in effective stake; the √reputation term (not stake compression) dampens pure-stake dominance.
Maximum job value: a runner’s rate_card.max_job_value is bounded by effective_stake × STAKE_JOB_MULTIPLIER_DENOM / STAKE_JOB_MULTIPLIER_NUM (currently effective_stake / 1.5). Delegation directly increases a runner’s capacity to accept higher-value jobs.
Minimum self-bond: runners MUST maintain self_stake >= max(MIN_STAKE_CBY_WEI, effective_stake × MIN_SELF_BOND_BPS / 10000). This ensures the runner always has meaningful skin in the game. See §4.2 for the parameter value.
3.3 New system instructions
Five newSystemInstruction variants. Redelegation (moving stake atomically between runners) is intentionally deferred — it requires a dual-liability accounting model that is out of scope for v1 (see §9.5).
TODO (opcode reassignment): The opcodes below (40–44) collide with currently-assigned System instructions innode/types/src/execution.rs(40UpdateSettlementConfig, 41FundActor, 42KeyDelivery, 43UpgradeActor). These values MUST be reassigned to a free range (e.g. 44–48 or the next free segment) before implementation; the opcodes in the table are placeholders pending that reassignment. This does not affect the semantics specified elsewhere in this CIP.
RunnerUpdateDelegationConfig
DelegationConfig is treated as absent and the cooldown precondition is skipped.
Preconditions:
RunnerRegistrationexists forsender.- Let
prev = registration.delegation_config. Ifprevis present (i.e., the runner has configured delegation before),block_height >= prev.last_updated + DELEGATION_COOLDOWN_BLOCKS. MIN_COMMISSION_BPS <= commission_bps <= MAX_COMMISSION_BPS.min_delegation >= MIN_DELEGATION_AMOUNT.- If
max_delegated_stake > 0, thenmax_delegated_stake >= delegation_totals.total_active(cannot retroactively break an existing cap).
- Compute the commission transition:
- If
previs absent:new_current = commission_bps,new_pending = None. (First-use takes effect immediately; there is no prior revenue stream to protect.) - Otherwise if
commission_bps == prev.commission_bpsANDprev.pending_commission_bps != Some(commission_bps): clear any queued change —new_current = prev.commission_bps,new_pending = None. - Otherwise (a genuine commission change):
new_current = prev.commission_bps,new_pending = Some(commission_bps),new_pending_effective_epoch = current_epoch + 1.
- If
- Write the resulting
DelegationConfig: - Emit
DelegationConfigUpdated { runner, accept_delegation, current_commission_bps: new_current, pending_commission_bps: new_pending, pending_effective_epoch, max_delegated_stake, min_delegation, block_height }.
pending_effective_epoch to current_epoch + 1 — this cannot short-circuit the one-epoch delay.
Gas: UPDATE_DELEGATION_CONFIG_CYCLES (governance-tunable, default 15,000 cycles, 500 cells).
RunnerDelegateStake
RunnerIncreaseDelegation.
Let summary = delegation_delegator_summary:{runner, sender} (empty struct if key is absent).
Preconditions:
RunnerRegistrationexists forrunner,health ∈ {Healthy, Paused}, andregistration.delegation_config.accept_delegation == true.amount >= max(MIN_DELEGATION_AMOUNT, registration.delegation_config.min_delegation).- If
registration.delegation_config.max_delegated_stake > 0, thendelegation_totals.total_active + amount <= registration.delegation_config.max_delegated_stake. sender_account.balance >= amount.summary.active_tranche_count < MAX_ACTIVE_TRANCHES_PER_DELEGATOR.- If
summary.active_tranche_count == 0:delegation_totals.delegator_count < MAX_DELEGATORS_PER_RUNNER.
sender_account.balance -= amount.- Allocate
tranche_id := delegation_tranche_counter:{runner,sender}++. - Write
DelegationTranche { delegator: sender, runner, tranche_id, amount, created_at: block_height, status: Active, claimable_at: None }. - Append
tranche_idtodelegation_delegator_index:{runner, sender}. - Append
(sender, tranche_id)todelegation_runner_index:{runner}. - Update
delegation_delegator_summary:{runner, sender}:active_tranche_count += 1active_amount += amount
- Update
DelegationTotals:total_active += amount- If
summary.active_tranche_countwas 0 before this tx:delegator_count += 1.
- Emit
DelegationCreated { delegator, runner, tranche_id, amount, block_height }.
DELEGATE_STAKE_CYCLES (governance-tunable, default 25,000 cycles, 2,000 cells).
3.3.1 Relationship between Delegate and Increase
RunnerIncreaseDelegation is semantically DelegateStake for a delegator who already holds at least one Active tranche (i.e., summary.active_tranche_count > 0) for the target runner. It is a separate opcode purely to make wire-level intent explicit and to let execution skip the MAX_DELEGATORS_PER_RUNNER check and the delegator_count bump.
Both instructions MUST enforce MAX_ACTIVE_TRANCHES_PER_DELEGATOR — it is the cap that bounds per-delegator settlement cost in §3.4, and it is always enforced by reading summary.active_tranche_count.
RunnerIncreaseDelegation
summary = delegation_delegator_summary:{runner, sender}.
Preconditions:
- Preconditions (1)–(5) of
RunnerDelegateStakeapply unchanged. summary.active_tranche_count >= 1(the sender already has at least one Active tranche for this runner; otherwiseRunnerDelegateStakeis the correct instruction).
RunnerDelegateStake, this instruction does NOT check MAX_DELEGATORS_PER_RUNNER because the delegator already counts toward delegator_count.
Effects:
sender_account.balance -= amount.- Allocate a fresh
tranche_id(existing tranches are never mutated — top-ups always create a new Active tranche so that per-tranche accounting remains unambiguous). - Write the new tranche, update indices.
- Update
delegation_delegator_summary:active_tranche_count += 1,active_amount += amount. - Update
DelegationTotals:total_active += amount;delegator_countunchanged. - Emit
DelegationIncreased { delegator, runner, tranche_id, amount, block_height }.
INCREASE_DELEGATION_CYCLES (governance-tunable, default 20,000 cycles, 1,500 cells).
RunnerUndelegateStake
amount CBY from the sender’s delegation to runner. Amount is drawn from the sender’s Active tranches in FIFO order by created_at (oldest first; ties broken by ascending tranche_id), splitting the last tranche touched if necessary. Each tranche drawn from contributes either its full amount (that tranche transitions to Unbonding) or a partial amount (the tranche is split: the remaining Active portion keeps its original tranche_id; the split-off Unbonding portion receives a fresh tranche_id).
Let summary = delegation_delegator_summary:{runner, sender}.
Preconditions:
summary.active_tranche_count >= 1.1 <= amount <= summary.active_amount.- The residual Active balance
summary.active_amount - amountMUST be either0OR>= max(MIN_DELEGATION_AMOUNT, registration.delegation_config.min_delegation)(prevents dust Active balances).
- Walk the sender’s Active tranches in FIFO order. Let
r = amount. For each Active tranchetiterated:- If
r >= t.amount(consume the whole tranche): sett.status = Unbonding,t.created_at = block_height,t.claimable_at = block_height + UNBONDING_BLOCKS. Decrementr -= t.amountandsummary.active_tranche_count -= 1. - Otherwise (
r < t.amount, final partial split): reducet.amount -= r; the tranche keepsstatus == Activewith its originaltranche_id. Allocate a freshtranche_idand write a new trancheuwithamount = r,status = Unbonding,created_at = block_height,claimable_at = block_height + UNBONDING_BLOCKS; appendu.tranche_idto the delegator and runner indices. Setr = 0.summary.active_tranche_countis unchanged by the split.
- If
- Update
summary.active_amount -= amount. Ifsummary.active_tranche_count == 0, delete thedelegation_delegator_summarykey. - Update
DelegationTotals:total_active -= amount. If the sender’ssummary.active_tranche_countfell from>0to0in this tx,delegator_count -= 1. - Emit
UndelegationInitiated { delegator, runner, amount, tranche_ids, claimable_at }listing every tranche that transitioned to or was created as Unbonding.
claimable_at and the current block height (§3.1).
During unbonding, an Unbonding tranche:
- Does NOT count toward
effective_stakefor VRF selection or job value limits (§3.2). - Does NOT earn revenue share (§3.4 restricts payouts to Active tranches).
- IS slashable while
current_block < claimable_at, and ceases to be slashable at exactlyclaimable_at. This prevents slash-and-run.
UNDELEGATE_STAKE_BASE_CYCLES + UNDELEGATE_STAKE_PER_TRANCHE_CYCLES × tranches_touched (defaults: 20,000 base + 5,000 per tranche).
RunnerClaimUnbonded
1 <= tranche_ids.len() <= CLAIM_MAX_TRANCHES(default: 32).- For each
tranche_id:DelegationTranche { delegator: sender, runner, tranche_id, .. }exists.tranche.status == UnbondingANDblock_height >= tranche.claimable_at(equivalently:is_claimable(tranche, block_height)per §3.1).
- Let
total = Σ tranche.amountover the supplied tranches. sender_account.balance += total.- For each supplied tranche: delete the
DelegationTrancherecord, remove itstranche_idfromdelegation_delegator_index:{runner, sender}anddelegation_runner_index:{runner}. DelegationTotalsis unchanged (total_activewas decremented at undelegation time; there is no cachedtotal_unbondingto update).delegator_countis unchanged by this instruction (it tracks delegators withactive_tranche_count > 0, not delegators with any remaining tranche).delegation_delegator_summary:{runner, sender}is unchanged (summary tracks Active only).- Emit
DelegationClaimed { delegator, runner, tranche_ids, amount: total }.
CLAIM_UNBONDED_BASE_CYCLES + CLAIM_UNBONDED_PER_TRANCHE_CYCLES × tranche_ids.len() (defaults: 10,000 base + 3,000 per tranche).
Because claimability is a pure function of block height and tranche state, there is no scenario in which a tranche that has passed its claimable_at cannot be claimed — the instruction can be submitted at any block ≥ claimable_at without a separate pre-processing pass.
3.4 Settlement payout splitting
0x03) is modified. Each runner’s portion is split between the runner and their delegators. Revenue is attributed only to Active tranches; Unbonding tranches earn no revenue (per §3.3).
Resolving the effective commission. Because commission changes are epoch-delayed (§3.3 RunnerUpdateDelegationConfig), settlement reads the authoritative rate via:
commission_bps and clears the pending fields; subsequent settlements in the same block see the clean state. A settlement that finalizes in any epoch strictly before pending_effective_epoch reads cfg.commission_bps unchanged.
amount, not the per-delegator total. A delegator with multiple Active tranches is paid proportionally on each, which falls out of the sum-of-tranches equalling the per-delegator total.
Integer rounding: any remainder from the runner-vs-delegator split accrues to the runner. Any remainder from the per-tranche distribution accrues to the lowest tranche_id among Active tranches. This keeps the total exactly equal to runner_share_total with no stray dust.
Gas impact: with T Active tranches per runner and M consensus runners, settlement performs M × T additional balance writes. The per-runner tranche count is bounded by MAX_DELEGATORS_PER_RUNNER × MAX_ACTIVE_TRANCHES_PER_DELEGATOR (see §4.2). At the defaults (200 × 8 = 1,600 tranches per runner), a 5-runner consensus pays out 8,000 tranche writes in the worst case, well within a single block’s system lane budget (LANE_SYSTEM_CYCLES = 25,000,000).
3.5 Settlement events
The0x03 system actor MUST emit structured events on every job settlement. These events are the canonical data source for delegation yield tracking and downstream compute finance products.
Implementation status (2026-07). The settlement events below (JobSettled/DelegatorPayout) are not yet emitted — the Result Verifier settlement path is&selfand cannot push events;settle_delegated_splitreturns the payout data for a future event-wiring pass. The delegation-lifecycle events, by contrast, are emitted today under acip13.topic namespace:cip13.delegation.created/cip13.delegation.increased(DelegateStake / IncreaseDelegation),cip13.delegation_config.updated,cip13.undelegation.initiated, andcip13.delegation.claimed(node/execution/src/runner/delegation.rs). These topic names are consensus-visible (they enterreceipt_root) and differ from the bareJobSettled/DelegatorPayoutnames used in this section.
JobSettled event
Emitted once per job settlement:emit_event pricing).
DelegatorPayout event
Emitted once per delegator payment within a settlement:(delegator, amount) pairs when the delegator count exceeds DELEGATION_EVENT_BATCH_THRESHOLD (default: 20).
3.6 Slashing cascade
When a runner is slashed for dishonesty (fabricated results, wrong model under TEE), the slash is distributed proportionally across the runner’s self-stake and every slashable tranche under theis_slashable(T, current_block) predicate from §3.1. Slashable tranches are: all Active tranches, plus all Unbonding tranches whose claimable_at > current_block. Unbonding tranches that have already reached claimable_at are no longer slashable — they have aged out of the slashing window — even though the delegator has not yet submitted RunnerClaimUnbonded.
Because the slashable base depends on current_block (and matures lazily as blocks advance), there is no cached total_slashable. The slashing routine computes the base on the fly during its iteration over tranches:
current_block < claimable_at, which changes each block. Caching a total_slashable would require a per-block invariant maintenance pass (the exact thing the prior draft’s overflow problem was caused by) with no performance benefit — slashing already iterates all tranches to apply per-tranche reductions.
Why all slashable tranches, not just Active? A delegator who initiated unbonding at block B and sees the runner misbehave at block B+10 must not be able to front-run the resulting slash transaction. Unbonding tranches remain slashable until claimable_at — which is the full UNBONDING_BLOCKS window, same as advertised when the delegator initiated unbonding.
Rounding: per-tranche floor division always under-slashes slightly; the rounding residue remains with the delegator. Per CIP principle (“never over-slash under ambiguity”), this is the safe direction.
Per-epoch cap (MAX_DELEGATION_SLASH_PER_EPOCH_BPS, default 500 = 5%): the cap is a ceiling on cumulative delegator liability per epoch, not a deferred-slash queue. Runner self-stake is NOT subject to this cap — misconduct always fully slashes the self-bond; the cap exists only to bound cascading damage to passive capital. If a runner’s self-stake is exhausted and further slashing would exceed the delegator cap, the excess is not reapplied in a future epoch. This is a deliberate design choice: uncapped slashes across epochs create cascading-attack surface, and passive capital should have a predictable per-epoch floor.
3.7 Runner deregistration with active delegations
RunnerDeregister MUST handle delegations. There is no unbonding queue in this CIP (§3.8); deregistration coordinates solely by writing claimable_at values onto the relevant records.
- The runner MAY first set
accept_delegation = false(viaRunnerUpdateDelegationConfig) and wait for all delegators to undelegate voluntarily, OR - The runner MAY submit
RunnerDeregister, which force-initiates unbonding for all Active tranches on the runner. For each Active trancheTheld against the runner:- Set
T.status = Unbonding,T.created_at = block_height,T.claimable_at = block_height + UNBONDING_BLOCKS. - Decrement
total_activebyT.amountand update each affected delegator’sDelegationDelegatorSummary(active_tranche_count,active_amount). - When a delegator’s
active_tranche_countdrops to zero, decrementDelegationTotals.delegator_countand delete the summary key.
- Set
- The runner’s self-stake enters its own unbonding window by writing
registration.self_stake_unbonding_claimable_at = block_height + UNBONDING_BLOCKSon theRunnerRegistration. The self-stake amount remains in the registration (not on the runner’s balance) until claimed; it is slashable whileblock_height < self_stake_unbonding_claimable_atper the same rule that applies to tranches (§3.1). registration.healthtransitions toDeregisteredimmediately. The runner is excluded from VRF selection and receives no further settlements.- Delegators claim their matured tranches via
RunnerClaimUnbondedat any block≥ T.claimable_at. The runner claims self-stake via the separate runner registration lifecycle flow at any block≥ self_stake_unbonding_claimable_at.
3.8 Unbonding maturity (no state transition required)
Earlier drafts of this CIP scheduled an end-of-block pass that mutated Unbonding tranches into a separateClaimable state and maintained a global unbonding_queue for this purpose. That design created an overflow hazard: if the pass could not process every matured tranche in a single block’s cycle budget, some tranches would remain Unbonding past their advertised claimable_at, extending their slashability window beyond what was promised and blocking claims until the queue caught up.
This CIP resolves the hazard by removing the scheduled transition entirely. A tranche’s claimable_at is the authoritative moment at which it ceases to be slashable (§3.6) and becomes claimable (RunnerClaimUnbonded, §3.3). Both properties are pure functions of current_block and tranche.claimable_at — they flip atomically at the block boundary with no processing required, no queue to drain, no budget to exhaust. There is no unbonding_queue storage key.
Consequences:
- No window where
claimable_atis passed but claiming is blocked. TheRunnerClaimUnbondedprecondition is satisfied by any block≥ claimable_at. - No window where a tranche is slashable past
claimable_at. §3.6’sis_slashablepredicate returnsfalseat exactlyclaimable_at. - Runner deregistration force-unbonding (§3.7) sets
claimable_at = block_height + UNBONDING_BLOCKSon each affected tranche; nothing else needs to be scheduled. - Indexers SHOULD index tranches by
(runner, claimable_at)to answer “what is claimable now?” queries efficiently, but this is an indexer concern, not a consensus concern.
RunnerDeregister) is bookkept with the runner’s registration record rather than as a tranche. Its maturity is governed by the same block-height-derived rule: self-stake is slashable while block_height < runner.self_stake_unbonding_claimable_at and claimable at or after that block.
4. Parameters
All parameters are governance-tunable via CIP-12 Tier 0 proposals.4.1 Timing
4.2 Stake limits
4.3 Slashing
4.4 Revenue
5. Entitlement interaction
Cowboy runners are heterogeneous. A runner supporting TEE + Llama 405B earns revenue only from jobs requesting those capabilities. Delegators are implicitly betting on the demand profile of the runner’s entitlement set. The protocol does not resolve this. Delegators SHOULD evaluate a runner’s capabilities, historical job volume, and entitlement coverage before delegating. The settlement events (§3.5) provide the data for this evaluation. Higher-order products may be created that address the entitlement problem:- Fleet-as-a-Fund actors (third-party): accept CBY deposits, delegate across a diversified set of runners spanning multiple entitlement classes, issue CIP-20 share tokens. The actor operator manages fleet allocation.
- Entitlement-specific pools (third-party): pools scoped to a single entitlement class (“TEE H100 Pool”). Delegators choose which compute segment to back.
- Settlement event dashboards (third-party or Watchtower): per-entitlement-class demand aggregations help delegators assess demand before committing capital.
6. Interaction with CIP-12 governance
6.1 Vote weight of runner-delegated stake (v1: zero)
CIP-12 §6.2 defines stake-chamber vote weight as CBY staked to validators (self-stake and validator delegation both count). Validator staking is an operational, consensus-layer commitment; runner delegation is an economic, compute-market commitment. These are distinct systems, and CIP-12’s voting design is not set up to mix them. For v1 of this CIP, runner-delegated CBY has zero governance vote weight. The CBY is locked and economically productive, but it carries no political voice while it is attributed to a runner. A CBY holder who wants both governance voice and runner yield must split their holdings between the two systems. This resolves cleanly against CIP-12’s “staked CBY has weight, unstaked has zero” rule: runner-delegated CBY is neither validator-staked nor unstaked; it simply occupies a third category with no voting rights in v1. A future CIP MAY extend CIP-12 to grant vote weight to runner-delegated stake (voted directly by the delegator, not inherited by the runner). That extension is deliberately out of scope here to keep the governance surface stable.6.2 Governance-tunable parameters
All parameters in §4 are registered in the0x09 Governance actor’s params store at genesis. Changes require a Tier 0 proposal (CIP-12 §5.1).
The settlement config (runner_percent, burn_percent, treasury_percent) is already governance-tunable per CIP-12; this CIP does not alter it.
7. Implementation notes
7.1 Changes to existing code
node/runner/src/types.rs:
- Add
delegation_config: Option<DelegationConfig>field toRunnerRegistration - Add
DelegationTranche,DelegationTotals,DelegationConfig,TrancheStatustypes - Serde implementations for all new types
node/types/src/execution.rs:
- Add
SystemInstructionvariants for opcodes 40–44 (§3.3) - Codec (Encode/Decode) implementations for new instructions
node/execution/src/runner/registry.rs:
handle_runner_update_delegation_config: opcode TBD (see §1 master table; v1 draft 40 collides with code’sUpdateSettlementConfig, must take a free slot ≥ 87); validate cooldown, commission bounds, min_delegation floor, cap non-regressionhandle_runner_delegate_stake: opcode TBD (v1 draft 41 collides withFundActor); validate preconditions (includingMAX_DELEGATORS_PER_RUNNERcheck), lock CBY, allocate tranche_id, write tranche, update indices and totalshandle_runner_increase_delegation: opcode TBD (v1 draft 42 collides withKeyDelivery); same as delegate but skipsdelegator_countbump and theMAX_DELEGATORS_PER_RUNNERcheckhandle_runner_undelegate_stake: opcode TBD (v1 draft 43 collides withUpgradeActor); FIFO consume Active tranches, transition or split to Unbonding, schedule in unbonding queuehandle_runner_claim_unbonded: opcode TBD (v1 draft 44 collides withUpdateBasefeeConfig); validatetranche.status == Unbonding && block_height >= tranche.claimable_atfor every suppliedtranche_id, release CBY, remove tranches from indices- Modify
handle_runner_registerto initialize emptyDelegationTotalsand tranche counter namespace on registration - Implement
handle_runner_deregister(currentlyUnsupportedInstruction) with force-unbonding of all Active tranches per §3.7
node/execution/src/runner/dispatcher.rs:
- Modify VRF weight calculation to use
effective_stake(self_stake + delegation_totals.total_active) - Modify max job value check to use
effective_stake
node/execution/src/runner/verifier.rs:
- Modify settlement to read delegation records and split payouts per §3.4
- Add
emit_eventcalls forJobSettledandDelegatorPayoutevents per §3.5 - Modify
slash_runnerto cascade to delegators per §3.6
node/execution/src/gas.rs:
- Add gas costs for new instructions
node/storage/src/:
- Delegation state lives entirely under the
0x01 Runner Registryactor. No new storage subsystem is introduced. There is no unbonding queue: §3.8 specifies that unbonding maturity is a pure function of block height and per-trancheclaimable_at, so no scheduled processing pass is required.
RunnerClaimUnbonded validates maturity on-demand from block_height and tranche.claimable_at; slash_runner_with_delegation recomputes the slashable base from the current block during its iteration. This is an explicit design choice to avoid the overflow/timing hazards of a scheduled transition pass.
7.2 RPC additions
The indexer (node/indexer/) SHOULD expose:
GET /runners/{address}/delegations— list all tranches (all statuses) grouped by delegator for a runnerGET /accounts/{address}/delegations— list all tranches (all statuses) grouped by runner for a delegator, including each tranche’stranche_id,amount,status, andclaimable_at(required for clients buildingRunnerClaimUnbondedtransactions)GET /runners/{address}/delegation_stats— totals, APY estimate, effective commissionGET /delegations/unbonding— global unbonding queue (useful for liquid staking protocols)
8. Security considerations
8.1 Slash-and-run prevention
Unbonding tranches are slashable per §3.1’sis_slashable predicate: a tranche remains in the slashing base while current_block < tranche.claimable_at. Because slashability is derived from block height rather than from a scheduled state transition, there is no processing-queue latency and no window in which a tranche is promoted out of the slashable base earlier than its advertised claimable_at. A delegator cannot front-run a slash by undelegating.
8.2 Storage griefing via delegation
A malicious actor could split a position into many small tranches to inflate storage, or spray small delegations across many runners. Mitigated by:MIN_DELEGATION_AMOUNTprotocol floor (1,000 CBY) enforced inRunnerDelegateStake/RunnerIncreaseDelegationpreconditionsdelegation_config.min_delegationrunner-set floorMAX_ACTIVE_TRANCHES_PER_DELEGATORcap (default 8) bounds per-delegator tranche count for a single runnerMAX_DELEGATORS_PER_RUNNERcap (default 200) bounds delegator count per runner- Gas costs for delegation instructions (delegate, increase, undelegate) that scale with tranche touches
8.3 Commission manipulation
A runner could set commission to 0%, attract delegators, then raise commission to 100%. Mitigated by:DELEGATION_COOLDOWN_BLOCKSbetween config changes- Commission updates take effect at epoch boundary, not immediately
- Delegators can monitor
RunnerUpdateDelegationConfigevents and undelegate before the new rate applies
8.4 Concentration risk
VRF weight is linear in effective stake (CIP-2 §5.3), so delegation scales selection probability proportionally; the √reputation factor — not stake compression — tempers concentration (a 4× reputation gap yields only a 2× weight gap).8.5 Cascading slashes in multi-runner pools
A third-party pool actor that delegates across multiple runners faces correlated slash risk if multiple runners misbehave in the same epoch. The per-epoch cap (§3.6) limits exposure per runner, but pool-level risk management is the pool operator’s responsibility, not the protocol’s.9. Future work
9.1 Liquid staking token (stCBY)
A reference pool actor that accepts CBY, delegates across a diversified runner set, and mints a CIP-20 share token (stCBY) is a natural first product built on this CIP. The protocol does not specify this — it is an actor-layer concern.9.2 Prepaid compute forwards
CIP-13 delegation enables the supply side of compute forwards: a runner (or runner pool) with sufficient effective stake can commit capacity for forward delivery. The buyer deposits CBY into a forward contract actor; the runner accepts, backing the commitment with delegated + self-bonded stake. See the compute finance roadmap for design details.9.3 Compute demand indices
TheJobSettled events (§3.5) provide the raw data for any downstream compute demand index or oracle product. Third-party Watchtower feed actors can aggregate these events into per-epoch indices by entitlement class, model, or TEE status. The DelegatorPayout events enable yield tracking products. This CIP provides the data; aggregation is an actor-layer concern.
9.4 Delegation marketplace
A future CIP or actor may provide an on-chain marketplace where runners advertise delegation terms and delegators can compare runners by commission, entitlements, historical yield, uptime, and slashing history. This is analogous to validator explorer dashboards on Ethereum/Solana.9.5 Atomic redelegation
An earlier draft of this CIP included aRunnerRedelegateStake instruction that atomically moved stake from one runner to another without a full unbonding cycle. It was removed before merging because a correct implementation requires dual liability accounting: until the source runner’s slash-window expires, the moved stake must be simultaneously slashable by both the source runner (for misconduct that occurred before the redelegation) and the destination runner (for future misconduct) — while the underlying CBY exists only once. Getting the state model, slashing math, and per-epoch cap semantics right for dual liability is a meaningful spec exercise on its own and would bloat v1.
For v1, delegators who wish to move stake between runners MUST undelegate, wait UNBONDING_BLOCKS (~24h), and then delegate to the new runner. Liquid staking pool actors can smooth this over for end users by maintaining a reserve buffer. A future CIP MAY introduce atomic redelegation with a proper dual-liability model (Cosmos’s “redelegation hop” is the nearest precedent).
9.6 Governance vote weight for delegated stake
As noted in §6.1, runner-delegated CBY has zero governance vote weight in v1. A future CIP may extend CIP-12 to grant vote weight to runner-delegated stake (voted directly by the delegator, not inherited by the runner), after live operation of CIP-13 clarifies whether such weight is desired and how to weigh it against validator-delegated stake.10. Rationale
Why runner-configurable commission? A protocol-fixed rate would either overpay runners (discouraging delegation) or underpay them (discouraging runner operations). The marketplace approach (io.net, Fluence) lets supply and demand set the rate. TheMIN_COMMISSION_BPS floor prevents a race-to-zero that would harm runner sustainability.
Why 24-hour unbonding (not 7 days)? Compute marketplaces are faster-moving than PoS consensus. Runner capabilities change as hardware is added or removed, demand shifts between entitlement classes, and runners can deregister on short notice. A 7-day unbonding (standard in PoS) would lock delegator capital through multiple demand regime changes. 24 hours provides sufficient slash protection (the dispute window is 15 minutes per whitepaper §5) while keeping capital responsive. This parameter is governance-tunable and can be increased if slashing dynamics require it.
Why 10% minimum self-bond? This ensures runners always have meaningful skin in the game. A runner operating purely on delegated capital (0% self-bond) has no personal loss from misbehavior — only reputation damage. Governance can adjust as the market matures.
Why no automatic commission from the protocol for delegators finding runners? Introducing a “delegation fee” split would add complexity without clear benefit. The commission is between runner and delegator. If a pool operator charges an additional management fee, that’s between the pool and its depositors — an actor-layer concern.
Why batch DelegatorPayout events above threshold? Individual events per delegator per job settlement create O(runners × delegators) events per job. With 5 runners and 200 delegators each, that’s 1,000 events per settlement. Batching above a threshold (20) keeps events useful for small delegator sets (direct delegators can see per-job payouts) while bounding gas for large pools.
Why tranches instead of one record per (runner, delegator) pair? Partial undelegation creates an active remainder and a separate unbonding position — with distinct amounts, timestamps, and claim states. A single record per pair cannot represent this correctly. Tranches also make the slashing math, the unbonding queue, and top-ups trivially correct: each tranche has one status, one amount, one timestamp, and is the unit of bookkeeping throughout the system. The cost is modest storage growth, bounded by MAX_ACTIVE_TRANCHES_PER_DELEGATOR × MAX_DELEGATORS_PER_RUNNER.
Why is the slashable base computed lazily instead of cached? An Unbonding tranche leaves the slashable base the moment current_block >= claimable_at — a transition that happens implicitly at the block boundary, per §3.1. Caching that sum would require either a scheduled maintenance pass (the approach used in an earlier draft, which introduced an overflow hazard) or a block-height-dependent invariant that is easy to get subtly wrong. Slashing already iterates every slashable tranche to apply per-tranche reductions, so computing the base in the same pass is free.
Why no atomic redelegation in v1? See §9.5. The short answer: dual liability accounting is a meaningful spec on its own, and delegators who need to move between runners can undelegate and redelegate in ~24h, or use liquid staking pools that absorb the delay.
Part II — v2 Revision (canonical; opcode renumbering + explicit amendments)
0. What this revision does
CIP-13 v1 §3.3 carries an explicit TODO admitting opcodes 40–44 collide with currently-assignedSystemInstruction opcodes (40 UpdateSettlementConfig, 41 FundActor, 42 KeyDelivery, 43 UpgradeActor). CIP-13 v1 also implicitly amends CIP-2 §5/§6 (VRF weight + max job value formulas) but does not state this as a normative cross-CIP amendment. v2 resolves both.
1. Master opcode allocation table (rewritten 2026-05-26 against code reality)
History note. The original CIP-13 v1 / v2 drafts of this section described an aspirational table that did not matchLive opcodes (in code):node/types/src/execution.rs. Earlier v2 drafts of CIP-13 / CIP-23 / CIP-10 / CIP-14 / CIP-16 each proposed overlapping slot ranges that none of them implemented. The table below is rewritten to be the authoritative record of what the chain actually accepts on the wire (as ofnode/types/src/execution.rs:591-699and theRead<E>impl at line ~1870), with the still-aspirational v2 allocations called out separately at the end.
Aspirational allocations (NOT in code; need renumbering against the table above before activation):
Reserved pending network activation (no renumbering required):
CIP-29 (Event Subscription) uses neither a SystemInstruction opcode nor a deployed actor. Instead,
pvm_host::call_actor intercepts calls targeting the dedicated address EVENT_SUBSCRIPTION_SYSTEM_ACTOR = 0x1D and dispatches to event_sub_system_actor::dispatch_rpc. No master-table allocation is required.
Note on this CIP’s own opcodes. CIP-13 v2’s delegation handlers (originally numbered 52–56 in the v2 draft) are not yet in code. Activation work MUST pick fresh slots from the ≥ 87 free range and amend this table; the 52–56 numbering is historical and must not be cited as authoritative.
2. Explicit amendment to CIP-2 §5 (VRF selection)
CIP-2 §5.4 defines VRF selection usingweights[i] = stake_to_weight(candidates[i].stake). CIP-13 v2 amends:
Amended:weights[i] = stake_to_weight(effective_stake(candidates[i]))whereeffective_stake(R) = R.registration.stake + R.delegation_totals.total_active. The weight function is unchanged from CIP-2 §5.3:w = effective_stake · max(√(reputation / REPUTATION_NORMALIZER), w_min_floor)— linear ineffective_stake. This applies to every dispatcher selection that occurs at or after CIP-13 activation.
3. Explicit amendment to CIP-2 §6 (max job value)
Amended:rate_card.max_job_value <= effective_stake × STAKE_JOB_MULTIPLIER_DENOM / STAKE_JOB_MULTIPLIER_NUM(default 1.5×).effective_stakeincludes Active delegated tranches per §2 above.
4. Slashing routing reuses CIP-3 SettlementConfig
CIP-13 v1 §3.6 slash_runner_with_delegation ends with route_to_treasury(slashed_total / 2); route_to_burn(slashed_total - slashed_total / 2). v2 clarifies: the 50/50 split is the default, and runtime SHOULD read from system:settlement_config.slash_treasury_percent (CIP-3 / SettlementConfig) so governance can tune slashing routing alongside other settlement routing.
The per-tranche slashing math (proportional reduction by T.amount / delegated_slashable) is independent of the routing split and unchanged.
5. Vote weight in CIP-12 (carry-forward, no change)
CIP-13 v1 §6.1: runner-delegated CBY has zero governance vote weight in v1. v2 carries this forward without change. Validator-delegated stake retains full vote weight per CIP-12 §6.2; runner-delegated stake remains a third category with no governance voice. A future CIP MAY revisit; deliberately deferred.6. Interaction with CIP-23 (TEE delegation)
A runner with a TEEmeasurement_binding (CIP-23 §3.7) MAY accept delegations. Properties:
- Delegated stake counts toward the runner’s
effective_stakefor VRF weight (§2 above) and for max job value (§3 above). - Delegated stake does NOT affect TEE eligibility. Eligibility is determined solely by
measurement_binding.status == Active && expires_at > submission_blockper CIP-23 §3.8 — a categorical capability check, not a stake threshold. - Slashing of a TEE runner under CIP-23 (e.g., for forged CompositeAttestation) cascades to delegators per the v1 §3.6 algorithm, capped at
MAX_DELEGATION_SLASH_PER_EPOCH_BPS = 500(5%). The cap protects passive capital from acute TEE-related risk concentration. Self-stake is uncapped and fully slashable for TEE forgery (CIP-23 v2 Part II §3).

