CIP-1: Actor Message Scheduler
Status: Draft for Internal Review
Type: Standards Track
Category: Core
Created: 2025-10-01
Type: Standards Track
Category: Core
Created: 2025-10-01
Abstract
This document specifies the Autonomous Actor Scheduler: the protocol-level mechanism that implements chain-native timers. It combines a tiered Calendar Queue for scalable O(1) event scheduling with an EIP-1559 timer-lane pricing model (basefee + priority tip) and a per-actor fairness weightW(actor) ∈ [1, 2]. Per-actor Gas Bidding Agent (GBA) contracts may override the protocol-supplied default GBA to compute bids from real-time block context, enabling actors to dynamically respond to network congestion and weigh the urgency of their own scheduled tasks.
The scheduler operates on top of CIP-5’s per-fire basefee model: every fire is a paid execution (fee_payer pre-charge plus refund). The EIP-1559 priority tip is additional to the per-fire max_cost pre-charge, not a replacement. Fees and metering align with CIP-3.
Implementation status: The inspected node runs CIP-5 FIFO scheduling and does not yet provide the complete auction contract below. Its cip1.auction.activation_height gate defaults to u64::MAX; this is an implementation limitation, not a required deployment mechanism. Remaining work includes the default-GBA median-tip estimator, Python bindings for both fee fields, schedule/fire validation and structured errors, and the tiered physical queue. The current fairness calculation uses the competing due-timer actors rather than a full-network median; simulation must validate that approximation against §8. These gaps must be resolved before claiming conformance.
1. Motivation
The Cowboy actor model’s reliance on timers requires a scheduler that is both scalable and economically intelligent. Network conditions are dynamic, and a scheduled task’s importance can change based on external events. A fixed pre-paid fee for a future transaction is insufficient. This design lets actors make real-time, economically rational decisions about the cost of their own execution, so high-priority tasks can aggressively compete for block space when it matters most. EIP-1559 pricing is preferred over a first-price-per-cycle auction for three structural reasons:- First-price-per-cycle is unstable in repeated knapsack settings. Programmatic bidders best-respond by underbidding the prior round’s clearing price, leading to oscillation and revenue collapse. Autonomous actors cannot easily run a complex bidding strategy or converge a Nash equilibrium against other agents. EIP-1559 eliminates the strategic-bidding component: the basefee is deterministic from the prior block’s utilisation, and the priority tip is a simple price-discovery channel for ordering within the lane.
- The default GBA collapses to ~2 lines. Under EIP-1559 the default bidding strategy reduces to
max_fee = 2 × basefee,max_priority_fee = previous_block_p50_tip— analogous to MetaMask’s default estimator on EVM. This removes the centralisation pressure that conservative ad-hoc defaults would otherwise impose on unsophisticated actors. - The invalid-bid attack class disappears structurally. There is no
bidfield;max_priority_fee_per_cycleis bounded bymax_fee_per_cycle − basefee(lossy clamping is structural), and the per-firemax_costpre-charge (CIP-5 §6.3) already caps the worst-case debit per fire. No drain attack survives.
2. Tiered Calendar Queue
The Actor Scheduler state is part of the global consensus state (σ) and is organized into a three-tier structure to manage timers across different time horizons.2.1 Tier 1 — Block Ring Buffer
Imminent timers.- Structure: A fixed-size ring buffer of
RING_BUFFER_SIZEbuckets, one per upcoming block height. - Function: A timer scheduled for block
His placed in bucketH % RING_BUFFER_SIZE. - Performance: O(1) enqueue and dequeue; the block producer accesses only the single bucket for the current height.
2.2 Tier 2 — Epoch Queue
Medium-term timers.- Structure: An array of buckets, one per future epoch (≈ one hour of blocks).
- Function: A timer scheduled for a block in epoch
Eis placed in bucketE. At each epoch rollover, the protocol redistributes that bucket’s timers into the appropriate Ring Buffer slots — an amortized maintenance cost.
2.3 Tier 3 — Overflow Sorted Set
Long-horizon timers.- Structure: A Merkleized balanced binary search tree ordered by block height.
- Function: Far-future timers are inserted here. Epoch maintenance also walks this tree and migrates any timers now within the Epoch Queue’s range.
3. Integration with the State Transition Function
Let σ be the global state, B a block, andH = height(B). The per-block sequence (canonical, matching CIP-5 §5.1 and the validator code path):
- Header / proposer. Determined by Simplex consensus and the previous QC.
- Epoch maintenance (if applicable). Redistribute timers from higher tiers into the Block Ring Buffer.
- Execute transactions (TX phase). Process the ordered transaction set Tᵢ. Calls to
schedule_timerMAY supply(max_fee_per_cycle, max_priority_fee_per_cycle); if omitted, the runtime supplies the default-GBA values (§7.1). - Collect due timers (end-of-block). Read the timer bucket for
Hand apply lifecycle classification per CIP-5 §5.4:- TTL expired or
balance(fee_payer) < max_cost→ self-destruct under the GC lane (§9). - Otherwise → enqueue for priority sort.
- TTL expired or
- Compute lane basefee. The Timer-lane basefee adjusts per block using EIP-1559 dynamics over the prior block’s timer-lane utilisation (§5).
- Compute effective priority for each enqueued timer:
where
W(actor) ∈ [1, 2]is the per-actor fairness weight (§8). - Sort and select. Order due timers by
effective_prioritydescending; tie-break by(timer_id, schedule_block). Greedily fillLANE_TIMER_CYCLES. Each selected timer is gated bycycles_consumed_so_far + gas_limit_per_fire ≤ LANE_TIMER_CYCLES − cycles_already_usedandgas_limit_per_fire ≤ MAX_CYCLES_PER_FIRE_AUCTION_PHASE(§10). Timers exceeding the per-timer cap are deferred without an attempt. - Settle. For each selected timer:
- Pre-charge
fee_payerper CIP-5 §6.3 with the priority-tip term added: - Execute the handler. On normal return, refund unused cycles ×
(basefee_lane_timer + priority_per_cycle). The tip portion goes to the block proposer (consistent with CIP-3 §2.4 tips routing); the basefee portion is burned. - On insufficient funds at any step → CIP-5 §5.4 path 2 (self-destruct without firing).
- Pre-charge
- Defer. Timers not selected remain in the bucket; on the next block they fall through the same flow. The 1,000-block fairness window naturally raises
W(actor)for actors whose timers are repeatedly deferred (§8). - Update fairness counters. Increment per-actor
recent_executions[actor] += 1for every timer fired; the 1,000-block rolling decay is applied at the start of the next end-of-block step (§8). - Resolve jobs, adjust basefees, mint rewards. As elsewhere defined.
4. Same-Block Prohibition
Timers created within the current block’s transactions MUST NOT execute in the same block. This avoids reentrancy via timer scheduling and contextual ambiguity around the basefees, congestion signals, and balances passed to GBAs. Consensus-critical.5. Timer-Lane EIP-1559 Pricing
The Timer-lane basefee adjusts per block over the prior block’s timer-lane utilisation:- Target utilisation: 0.5 (50%) of the 8,800,000-cycle Timer lane.
- Max basefee adjustment per block: ±12.5%.
- 100% of the basefee is burned (consistent with CIP-3 §2.4 and WP §6 “100% basefee burn”). The lane basefee replaces the CIP-3 cycle-basefee meter for timer fires — timer cycles are metered against
basefee_lane_timeronly and are not double-charged against the global cycle basefee. The two meters are tracked separately (the timer lane runs its own EIP-1559 utilisation curve and its own burn tally) to preserve per-lane burn telemetry. - Per-lane fee multiplier is pinned at
1.0×at launch (CIP-3 §2.2.3 + WP §6 / §17.9; no subsidy). Tier-0 governance-tunable.
max_priority_fee_per_cycle = 0 still fires when the lane is uncongested, paying only the lane basefee. Under congestion, only timers whose effective_priority (§3 step 6) clears the marginal kicked-out timer make the cut; losers carry forward.
6. schedule_timer API
The PVM host API (node/execution/src/pvm_host.rs):
max_fee_per_cycle ≥ basefee_lane_timer→ elseTimerRejectedBelowBasefee(immediate failure; nothing escrowed).max_priority_fee_per_cycle ≤ max_fee_per_cycle − basefee_lane_timer→ else clamp at schedule time and emitTimerPriorityClampedAtSchedule(stated, clamped)for observability.- No escrow at scheduling. The per-fire
max_cost(CIP-5 §6.3) is pre-charged at execution time only, not at scheduling. - Scheduling cost remains a small fixed cycle charge paid upfront by
schedule_timerto occupy the queue slot — independent of execution pricing.
bid is not a supported scheduling parameter. A caller supplying it MUST receive TimerArgDeprecated; there is no silent-ignore behavior.
7. Gas Bidding Agents (GBAs)
A GBA is an actor-owned contract responsible for pricing one or more of the actor’s timers. The protocol callsgetGasBid(context) read-only at fire time. The runtime supplies a default GBA when an actor doesn’t provide one; custom GBAs remain useful for actors that need dynamic, context-sensitive bidding (DeFi liquidation actors, oracle-pushers, MEV-aware schedulers).
7.1 Default GBA (normative)
2 × basefeeheadroom absorbs up to ~5 blocks of basefee growth at the ±12.5% per-block clamp (1.125^5 ≈ 1.80) before hitting the cap — sufficient for normal congestion swings.p50 priority tipis the prior-block median priority fee paid by fired timers; falls back to0if no timers fired in the prior block (avoids an undefined estimator on cold start).
7.2 GBA interface and context
Custom GBAs implement:context carries the data needed for real-time pricing decisions:
trigger_block_height(u64) — the height the timer was originally scheduled for.current_block_height(u64) — current height; lets the GBA compute lateness.basefee_cycle(u128) — current compute-cycle basefee (CIP-3).basefee_cell(u128) — current storage/byte basefee (CIP-3).basefee_lane_timer(u128) — current Timer-lane basefee (§5).last_block_cycle_usage(u64) — total cycles used in the previous block (congestion signal).previous_block_p50_priority_tip_per_cycle(u128) — prior-block median fired-timer tip.owner_actor_balance(u128) — current CBY balance of the owning actor.
7.3 SDK convenience: priority_tier_hint
The cowboy-py SDK MAY expose a high-level enum for callers who don’t want to construct a GBA:
0x09 under system:gov:param:cip1.priority_tier_multipliers.{economy,standard,fast,urgent} (the system:gov:param: prefix is the on-chain gov_param_key convention — node/types/src/constants.rs GOV_PARAM_KEY_PREFIX) and are Tier-0 governance-tunable (consistent with the lane fee multipliers in CIP-3 §2.2.3 — both are multiplicative scalars on fee components, neither redirects revenue across recipient classes). CIP-12 §5.1 Tier-0 scope already references “any governance-tunable parameter (see genesis defaults and CIP-1/3/5/9/10)”; the priority-tier multipliers are picked up automatically by that clause.
8. Per-Actor Fairness Weight W(actor)
Formula:
- An actor with no recent fires (
ratio = 0) getsW = 2(maximum boost); an actor at or above the network median (ratio ≥ 1) getsW = 1(no boost). Linear interpolation forratioin[0, 1]. - Window length
FAIRNESS_WINDOW_BLOCKS = 1_000(~16.7 min at 1s blocks), Tier-2 governance-tunable. Stored at0x09undersystem:gov:param:cip1.fairness_window_blocks. ratiois clipped to[0, 2]before subtraction, so a brand-new actor with zero recent fires getsW = 2rather than infinity.
actor carries a 1,000-element ring buffer of fire-counts per block in the Actor Scheduler state. Memory cost: ~8 KiB per actively-firing actor. Eviction: actors with zero fires in the entire 1,000-block window are pruned from the fairness map (next fire re-creates a fresh entry).
Mutability of formula structure. Tier-2 (governance changes the inclusion ordering, which affects fee revenue distribution). The numeric FAIRNESS_WINDOW_BLOCKS and the [1, 2] clip bounds are Tier-0.
Known limitations (Simulation deferred — §16):
- Fragmentation attack. A sophisticated developer can deploy 100 actor instances and treat each as a separate “quiet” actor to bypass per-actor weight. Mitigation candidate: per-deployer weight (using actor-creation trace) instead of per-actor — requires deeper actor metadata than CIP-2 currently exposes. Simulation MUST size the attack magnitude before this spec ships; if material, per-deployer weight is added in a follow-up revision.
- Median moves under shock. When a large actor enters/exits and shifts the network median sharply, incumbents are briefly disadvantaged for ~1,000 blocks. EMA-smoothing the median (analogous to the HHI smoothing in CIP-2 §5 amend) is a candidate follow-up addition.
9. Lane Budgets and the Three-Path Timer Lifecycle
Per CIP-5 §6.5, the timer subsystem has two independent per-block budgets:
The auction operates only on
LANE_TIMER_CYCLES. GC draws from TIMER_GC_CYCLES, so a resume-after-outage storm of expired timers cannot starve live timer execution.
The three-path lifecycle for any due timer (CIP-5 §5.4), plus explicit cancellation:
- Natural fire.
fee_payeris solvent and TTL has not expired. Pre-charge, execute, refund unused. - TTL expiry.
expires_athas passed. Self-destruct under the GC lane. - Insufficient funds.
balance(fee_payer) < max_cost. Self-destruct under the GC lane and emitTimerCancelledInsufficientFunds. - Explicit cancellation. Actor-self cancel (host syscall) or validator-set emergency cancel via
SYS_CANCEL_TIMER(§12).
10. Per-Timer Cycle Cap
system:gov:param:cip1.auction.max_cycles_per_fire (read at 0x09 via gov_param_key, using the governance parameter lookup in node/storage/src/speculative.rs).
11. DoS Mitigation and Congestion Handling
The combination of a bounded lane budget, deterministic basefee growth, and per-actor fairness weight defends against scheduler-targeted DoS:- Bounded execution.
LANE_TIMER_CYCLEScaps timer-driven work per block, so timers cannot crowd out user transactions. - Economic prioritisation. Instead of plain FIFO, the lane is sorted by
effective_priority: timers whose owners bid higher tips run first. Low-priority spam timers are priced out during congestion as the basefee ratchets up. - Best-effort delivery with fairness. Losers carry over to the next bucket; the per-actor fairness weight
W(actor)raises priority for actors who have fired less than the network median in the last 1,000 blocks, preserving liveness without amplifying timer-spam. - GC isolation. TTL-expiry storms and insufficient-funds destruction are routed through
TIMER_GC_CYCLES, an independent budget. - Structural invalid-bid resistance. There is no
bidfield to game;max_priority_fee_per_cycleis bounded bymax_fee_per_cycle − basefee_lane_timer, and the per-firemax_costpre-charge (CIP-5 §6.3) caps the worst-case debit per fire.
12. System Instructions
This spec introduces no new system-instruction opcodes. Theschedule_timer extension is a PVM host-API change to existing syscalls (schedule_timer / schedule_timer_ex / cancel_timer / extend_timer at node/execution/src/pvm_host.rs); it adds optional parameters but allocates no opcodes.
The supporting timer-management system instructions already live in code at the following opcodes (node/types/src/execution.rs):
The canonical opcode allocation table is maintained in CIP-13.
13. Funding
Each scheduling actor MUST fund itself or a designatedfee_payer with at least one max_cost reserve per pending timer. Long-running heartbeat actors SHOULD use schedule_timer_ex with an explicit fee_payer (default actor_self) and monitor its balance. Actors and watchtowers SHOULD subscribe to TimerCancelledInsufficientFunds. TTL-protected abandoned timers are cleaned up automatically.
14. Integration Requirements
- CIP-5 owns timer creation, cancellation, expiry, and the per-fire payer model. This CIP owns auction ordering and fairness; implementations MUST use §§3–11 rather than CIP-5 §9’s alternative auction description.
- System instruction opcodes 48, 49, and 50 retain the functions in §12.
- The priority tip extends the per-fire
max_costformula while retaining pre-charge and refund accounting. - Block ordering is transactions before timers. A timer created in a block MUST NOT fire in that block.
- Omitted fee fields select the default GBA estimator. Python bindings MUST expose the explicit fee fields in §6.
- Conformance requires auction ordering, timer-lane basefee dynamics, default-GBA estimation, the §10 cap, and §8 fairness to operate together. Test these through the scheduler and SDK caller paths.
15. Governance Parameters
Economic defaults and parameter changes follow their stated governance tiers under CIP-12. Thepriority_tier_multipliers are Tier-0 parameters (§7.3). The scheduler contract does not require a timed conversion of an existing network or a temporary argument-compatibility path.
16. Simulation Requirements
FAIRNESS_WINDOW_BLOCKScalibration. 1,000 blocks (~16.7 min) is the launch default; longer windows smooth more but lag more on actor identity changes.- Per-deployer vs per-actor weight (fragmentation-attack threat magnitude, §8 limitation 1).
- Median EMA-smoothing (§8 limitation 2) — evaluate against empirical workload shocks.
- Priority-tier multipliers
{0.8, 1.0, 1.5, 2.5}— empirical tuning against actual congestion patterns. - Lane fee multiplier — pinned at 1.0× at launch (CIP-3 §2.2.3); Simulation MAY recommend a 0.8× Timer-lane subsidy if the lane is structurally under-utilised post-mainnet.

