Scheduler system contract at 0x00000000000000000000000000000000000000fe
runs calls at a future slot (a block timestamp in seconds), a block height, or
after an oracle condition is met. The account that calls schedule, named
payer in the API, deposits the gas token into escrow to fund execution.
Every block, the proposer derives the due entries from the scheduler’s
storage and prepends one system transaction per entry. Every node re-derives
the same set and rejects a block whose leading transactions differ. Each run
produces a transaction receipt and logs. These system transactions have sender
0xFE and type byte 0x7e. Only block execution creates them.
Entries support these options:
- Open entries carry their calls in plaintext.
- Sealed entries keep their calls encrypted until the corresponding beacon key becomes available.
- Conditional entries become eligible after an oracle write satisfies their condition. Their calls can be open or sealed.
Policy
The network sets the policy when it installs the scheduler. Each field has a public getter:
Once the scheduler activates, a block’s timestamp may be at most 2 seconds ahead
of a validator’s clock and 60 seconds behind it.
Scheduling an entry
approve(0xFE, amount)on the scheduler’s gas token, thendeposit(amount).- If the entry uses a token reserve,
approve(0xFE, reserveAmount)onreserveTokenbefore scheduling it. - Call
schedule(Params). It reservescost * gasPricefrom the available escrow balance, places any token reserve on hold, and returns the entry ID.
calls: one to eight(target, gasLimit, data)calls, run in order and atomically. If any call fails, every earlier call reverts. No target may be the zero address, the scheduler, the gas token, or a MIP20 token. The ABI-encoded calls may take at most 4096 bytes.slot(strictly after the current block’s timestamp) orheight(strictly after the current block number): exactly one of them, within the horizon. A height trigger becomes due at that height and runs when its dependencies and the block’s capacity permit. Height triggers have no lateness deadline and cannot recur.gasLimit: the gas limit for executing the calls. It covers each call’s own limit, the overhead of making each call, and capturing any reserve, including receive policy screening and possible quarantine writes.gasPrice: at leastminGasPrice.class:0Guaranteed or1BestEffort.lateExecute: pastslot + maxLateSlots, run anyway (catch up after a halt) instead of expiring.maxRetries,backoffSlots: retries after a failed run.periodSlots,remaining,pauseOnFail: a recurrence.reserveToken,reserveAmount: an optional reserve. Use both zero for none.dependsOn: up to eight earlier entries, each(id, finished). The dependency must have succeeded, or withfinishedmerely ended (a paused series counts as ended). A recurring entry cannot have dependencies.
Cost
An entry is budgeted atcost(gasLimit, encodedCalls.length, dependencies, recurring, reserved):
cost must fit gasPerBlock. The amount reserved for execution is
cost * gasPrice. A run pays this amount in full to the block’s proposer,
regardless of the gas the target calls use.
Capacity
schedulerejects a slot or height that already holdsmaxEntriesPerBlockentries.- A Guaranteed entry reserves its
costfrom the slot’s or height’sguaranteedGasPerSlotbudget.schedulerejects the entry if the remaining budget is too small. Each period of a Guaranteed series tries to reserve again. A recurrence whose next slot lacks Guaranteed capacity runs that period as BestEffort. - A retry, a recurrence, or a moved entry spills to the first of the next 8 slots with room. When none has room, the retry fails or the recurrence starves.
- A Scheduled entry holds its place in the queue and its Guaranteed capacity
until it runs, is canceled, or expires. Holding that place locks
cost * gasPriceof escrow at the entry’s own gas price, which is at leastminGasPrice.
Reserves
A reserve holds the payment amount until the entry executes.reserveToken
must be a MIP20 token other than the scheduler’s gas token. This keeps the hold
from locking the gas-token balance needed to pay fees for cancel or withdraw.
scheduleholdsreserveAmountout of thepayer’s spendable balance (MIP20.holdFrom).- The scheduler captures the hold into the first call’s target, then executes the
calls atomically. A plain payment is one call with
targetset to the recipient and emptydata. - A failed run reverts the capture, so the hold stays through retries. A
final failure,
cancel, orexpirereleases it in full. - A recurrence holds the next period’s amount when it re-enqueues, so approve enough for every period. If the hold fails, the recurrence starves and its reserved execution fee returns to the available escrow balance.
REVERT. If the subsequent calls succeed, the
entry becomes Executed and consumes its reserve while the intended recipient
remains unpaid. Verify the reserve token’s exact Transfer from the payer to
the target before recording payment. For quarantine, save the matching
quarantine receipt and transfer from the payer to ReceivePolicy. Record the
reserve as consumed and resolve recovery before authorizing another payment.
A later target revert rolls back both the capture and its quarantine receipt.
When an entry runs
An entry becomes due when the block’s timestamp reachesslot or its number
reaches height. Execution also depends on available capacity, completed
dependencies and, for a sealed entry, its decryption key.
A block’s budget goes to due Guaranteed entries before BestEffort ones, and
to BestEffort entries in descending gasPrice order. All selected entries must
fit the gas budget. Guaranteed entries count toward maxEntriesPerBlock when
selecting BestEffort entries, but that count does not limit Guaranteed entries.
An entry waits when it exceeds the remaining budget or has a dependency still
waiting to execute.
A waiting BestEffort entry or an entry delayed by a dependency moves to the
first of the next 8 slots or heights with room, keeping its deadline. This lets
the queue cursor advance past its original position. A Guaranteed entry delayed
by a dependency becomes BestEffort when it moves, as a retry does. These entries
keep their position:
- a sealed entry, which waits for its decryption key.
- a Guaranteed entry waiting for budget.
- an entry that finds no room.
msg.sender == 0xFE and must check the authorization supplied
in the calldata. The scheduler marks an entry Executed before making its
calls, which prevents a target from canceling, expiring or rerunning that entry
during execution.
Failures, retries, and recurrence
A reverting call fails its run. An unfinished dependency delays the entry until the dependency finishes. A failed entry can retry up tomaxRetries times as a
time-triggered BestEffort entry. Each retry reserves another execution fee from
the available escrow balance and targets now + backoffSlots, or the next
slot when backoffSlots is zero. A dependency that was required to succeed but
failed, was canceled, expired or paused causes the entry to fail immediately.
A recurrence schedules another period after the previous one succeeds or ends
in failure. Its timing is calculated from the previous due slot. The series
stops when remaining reaches zero. It emits RecurrenceStarved when it cannot
fund the execution fee or token reserve for another period, or find room in
the 8 slots starting at its due slot.
With pauseOnFail, a failed period pauses the series (Paused) until the
payer calls resume(id) or cancel(id). Resuming requires enough funds for
the execution fee and token reserve, plus capacity in one of those 8 slots.
If these requirements are unmet, resume reverts and the series stays paused.
Cancelling, expiring, and withdrawing
Before execution, thepayer can call cancel(id) to recover
cancelRefundBps of the reserved execution fee. Anyone can call expire(id)
for a full refund when an entry meets one of these conditions:
- It has passed its deadline with
lateExecutedisabled. - A queue cursor (
cursorSlotorcursorHeight) has passed its key while the entry remainsScheduled. - It is a conditional entry whose item has left the oracle catalog.
Scheduled after its system transaction reverts, expire
recovers its reserved amount, as described in
What a run looks like. Calling expire during a
scheduled entry’s execution reverts with Running.
Cancellation and expiry release the token reserve in full and free the entry’s
queue position and Guaranteed capacity. Expiry also removes conditional entries
from the watch list. To recover available escrow funds, the payer calls
withdraw(amount).
For a withdrawal, verify a finalized Transfer(scheduler, payer, amount) from
the gas token before recording that the payer received the tokens. If its
receive policy refuses the credit in REVERT mode, withdrawal reverts and
preserves the available escrow balance. In QUARANTINE mode, withdrawal
succeeds and emits Withdrawn, while the funds go to ReceivePolicy. Save the
full quarantine receipt and follow the
recovery procedure.
Conditional entries
A conditional entry waits for an oracle write that satisfies its condition. Setwhen = (item, kind, operand) and set slot to the earliest timestamp at
which a write may trigger it. item is the catalog item’s ID,
keccak256(name). The item must be listed in the oracle catalog and have at
least one recorded write, or scheduling reverts with BadCondition.
- The entry watches its item (
watchersOf(item), at most 256 per item) under the keyWATCH_KEY(bit 62). It is never due and never expires while it watches, unless its item leaves the catalog. - Each block evaluates the watchers of every item written since they were
last evaluated, normally the items the parent block wrote.
watchedHeight(item)is the height of the write they last saw. - An entry fires when its predicate holds on the item’s latest state and the write’s timestamp reaches the entry’s not-before slot. It moves into the first of the next 8 slots with room and follows the rules for time-triggered entries, including deadlines, retries and dependencies. If all 8 slots are full, it keeps watching until the item’s next write, including a heartbeat.
- If the value already satisfies the predicate when the entry is scheduled, the next qualifying write triggers it.
- A halt or resume through a control item changes the item’s flags.
WHEN_NOT_HALTEDfires at the first write after a resume. - Conditional entries are BestEffort only, with no height trigger or recurrence.
- An entry leaves the watch list when it fires, is canceled, or expires.
- Each block evaluates at most 1024 watchers, apart from the budget for due
entries, starting from an item that rotates with the block number. It
reads only an entry’s predicate (a few words, never its calls). An item
past that budget is evaluated in a later block on its latest write then,
so a value that moved back across the threshold in between does not fire
it. Filling an item’s 256 places, or enough items’ places to spend the
budget, takes that many entries locked at
minGasPrice.
Call authority
A call carries0xFE’s authority over its escrow and the holds and allowances
granted by accounts that call schedule. The scheduler therefore rejects
MIP20 tokens as call targets, including targets that become MIP20 tokens after
scheduling. To relay an ERC-3009 authorization, target a relayer contract that
submits it.
What a run looks like
The system transaction’s hash is:gasLimit is the entry’s
budgeted cost, so the reserved amount is the transaction’s
gasLimit * gasPrice.
RPC presents the transaction using EIP-1559 fields, with 0xFE as sender, the
attempt as nonce and a zero signature. A single-call entry shows the target
address and its data. A batch shows 0xFE as the target and the ABI-encoded
calls as data. During execution, the system calls execute(id) on the
scheduler, which makes the target calls and records their outcome. The
transaction receipt’s status describes execution of execute(id).
A 0x7e receipt has one of three outcomes:
- Success.
execute(id)ran and paid the reserved amount to the proposer. Read its logs from0xFEto determine whether the target calls succeeded:Executed(id, attempt)records success, whileExecutionFailed(id, attempt, willRetry)records a failed call or dependency. Readstatus(id)for the entry’s resulting state. - Reverted, with gas used.
execute(id)reverted, so the execution fee remains in escrow. The entry staysScheduledbehind the queue cursors and is excluded from future execution. Dependencies treat it as expired, andexpire(id)returns its whole reserved amount. - Reverted, with zero gas used. The system skipped
execute(id)and charged no fee. Either an earlier target call canceled the entry, applying its cancellation refund, or escrow could not cover the execution fee. In the latter case, the entry staysScheduledand can be expired.
debug_trace*) replays a scheduled transaction as the same system
call block execution made, from 0xFE to itself. Pending-block RPC
("pending") does not include due entries.
Halts and catch-up
- Entries past their deadline run only with
lateExecute. The rest wait forexpire. - Catch-up after a halt scans at most 4096 slots (and 4096 heights) and reads at most 1024 queued entries per block, heights first, then slots, each oldest first. Each read costs a few words of the entry’s fixed fields. The calls are read, and decrypted when sealed, only for entries selected for execution.
- The cursors (
cursorSlot,cursorHeight) mark the first key still holding a waiting or unread entry. Each block clears the queues and Guaranteed reserves of the keys the cursors pass. When the scheduler activates on a running chain, the cursors start at the activation block’s next slot and height. - Scheduled runs count against the block gas limit like any transaction.
Interface
A finished entry (Executed, Failed, Cancelled, Expired) is pruned to its first word:status(id) and the first fields of entry(id) stay readable,
and dependents read its status. status is 0 None, 1 Scheduled, 2
Executed, 3 Failed, 4 Cancelled, 5 Expired, 6 Paused.
Transactions: deposit, withdraw, schedule, cancel, expire,
resume.
Reads: status(id), entry(id), queueAt(key), reservedGas(key),
balance(payer), cost(...), nextId(), cursorSlot(),
cursorHeight(), watchersOf(item), watchedHeight(item),
sealedPending(slot), and the policy getters. A height’s queue key is the
height with bit 63 (HEIGHT_KEY) set.
Events: Scheduled, Executed, ExecutionFailed, Recurred,
RecurrenceStarved, Paused, Cancelled, Expired, Deposited,
Withdrawn.
Errors: NotChain, NotPayer, NotScheduled, NotPaused, NotDue,
NotExpired, BadTarget, BadCalls, BadTrigger, BadDependency,
SlotNotInFuture, BeyondHorizon, GasLimitOutOfRange, ZeroGasPrice,
GasPriceTooLow, DataTooLarge, BadClass, SlotReserveFull,
SlotFull, InsufficientEscrow, TransferFailed, BadReserve,
ReserveIsGasToken, CallFailed, CannotRecur, BadSeal,
BadCondition, WatchFull, Running.
Encrypted payloads
Seal the calls as follows, withgroup_key = Beacon.groupKey() at 0xFF
and payer the account that will call schedule:
callsisabi.encode(Call[]), as the Scheduler stores an open entry’s calls, at most 4092 bytes.- Pad it to a size class:
body = u32_be(len(calls)) || calls, then zeros up to the next power of two, at least 256 and at most 4096 bytes. - Draw a random 32-byte
salt. The commitment iskeccak256(salt || body). - Draw a random 32-byte data key and
timelock-encrypt it
to the identity
u64_be(slot): 112 bytes. - Draw a random 12-byte nonce and encrypt
salt || bodyunder the data key with ChaCha20-Poly1305 (RFC 8439), with associated datau64_be(slot) || payer(8 + 20 bytes). Append the 16-byte tag to the encrypted bytes.
schedule in ciphertext and the commitment in commitment, with
calls empty. The salt keeps the commitment from confirming a guess at the
calls. schedule accepts a ciphertext longer than 172 and at most
4268 bytes. An entry that does not open to a well-formed body fails when the
entry runs.
Every entry at a slot opens with the same key. The associated data also binds
the payload to payer, so successful decryption requires the scheduling
account’s address to match the address used during encryption.
Rules
- A sealed entry requires a slot trigger and an installed beacon group key. It supports at most 4092 bytes of calls, with recurrence and token reserves disabled. A sealed conditional entry uses the slot as its not-before time.
- Everything else (gas, class, retries, dependencies) works as for an open entry. The reserved amount is priced on the ciphertext’s length, padding included.
- A sealed entry keeps its queue position while waiting for its slot or condition key.
What stays hidden
Thepayer, slot, gas limit and price, class, dependencies and size class are
public before execution. Targets, data and the calls’ exact length stay hidden
until the decryption key becomes available.
- A sealed entry pays through its calls, for example by submitting a signed ERC-3009 transfer through a relayer contract. The spending account must keep enough funds available until execution.
- A gas limit rounded up, to a power of two for example, hides how much work the calls do.
Opening
sealedPending(slot) counts a slot’s unrevealed entries. Proposers prioritize
beacon keys for slots with pending entries for 30 seconds after each slot ends
(GRACE_SLOTS). Block production can continue while a key remains unavailable,
and omitting a key does not make a block invalid.
The block that carries the slot’s key applies its beacon update first. It
decrypts the calls, checks the commitment and stores the revealed calls before
executing the entry as a system transaction. If the entry exceeds that block’s
remaining budget, it runs in a later block. Anyone can decrypt its calls once
the key is onchain.
An entry fails without retry if decryption, commitment verification or decoding
fails, or if it exceeds eight calls or names a forbidden target. A payload
encrypted under a group key the beacon no longer holds is revealed as empty
and fails, consuming its reserved amount.
A queued entry still sealed at block.timestamp > entry.key + 1 + 30 has
missed its decryption deadline. expire(id) refunds its reserved amount in
full, including when lateExecute is set. Here entry.key is the queued slot,
which ends at entry.key + 1, followed by a 30-second grace period. Conditional
entries enter this grace period once they leave the watch list for a slot.
Sealed conditional entries
A conditional entry can also be sealed: its calls stay hidden until the condition holds. Seal them to the condition instead of the slot:payer is the account that calls schedule, and slot is the entry’s
not-before slot. Seal as for a slot, with two changes: the data key is encrypted
to the identity condition (32 bytes), and the associated data is condition.
Pass the ciphertext and commitment as for a sealed entry, together with
when.
- The validators’ signers release a partial signature over
conditiononly for a finalized block whose state, proven against the block’s state root, holds an oracle write ofitemthat satisfies the predicate and is no older than the not-before slot: the same rule that fires the entry. Nodes ask for it only once a block has fired the entry, proving the state its watchers were evaluated on, so an entry left watching (no room, or past the watcher budget) keeps its calls hidden. - The required number of validator partial signatures reconstructs the
condition’s key. A proposer carries it in the block’s beacon update, which
accepts at most 32 condition keys per block. After inclusion,
Beacon.conditionKey(condition)returns the key. - The entry fires as an open conditional entry does, then waits for the condition’s key while holding its slot’s cursor. Once the key is included, the entry can open and run subject to the block’s capacity. It expires with a full refund if it is still sealed 30 seconds after the end of the slot it fired into.
- A
payer’s entries that share a condition share its key. Once one fires and its key lands, every payload thatpayersealed to that condition opens, including one still watching for room. Use different not-before slots when entries need independent disclosure times. The condition includespayer, so each scheduling account uses a distinct key. Once that key is public, anyone can decrypt the calls. sealedPendingcounts slot-sealed entries only. Sealed conditional entries use their condition keys and have no key-inclusion requirement for block validity.
Timelock key encoding
Encryption to a slot is Boneh-Franklin timelock encryption on BLS12-381 with the Fujisaki-Okamoto transform, public keys in G1 and signatures in G2. It encrypts a 32-byte messageM to an identity id under the group
key PK (groupKey()). A slot’s identity is u64_be(slot). A sealed
conditional entry’s is its 32-byte condition.
Q = hash_to_G2(varint(len(ns)) || ns || id)with the RFC 9380 DSTBLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_, wherens = "metal-beacon-v1" || uint256_be(chain_id) || genesis_hashandvarintis unsigned LEB128.keyAt(slot)is the group’s BLS signature onQ.- Draw a random 32-byte
sigma. ris the 48 bytes ofexpand_message_xmd(SHA-256, sigma || M, "TLE_BLS12381_XMD:SHA-256_SSWU_RO_H3_"), read big-endian and reduced modulo the group order.U = r * G1(48-byte compressed).g = e(r * PK, Q), serialized as 576 big-endian bytes in blst’sblst_bendian_from_fp12layout.V = sigma XOR sha256("h2" || g)andW = M XOR sha256("h4" || sigma).
U || V || W, 112 bytes. With the key K, decryption
computes g = e(U, K), recovers sigma from V and M from W, and
accepts only when U equals r * G1 recomputed from them.
Decryption requires the corresponding slot or condition key.
Read entry state
Use this interface to diagnose a pending entry at a chosen block. A finished entry is pruned, so save its parameters and relevant state before completion.key === (1n << 62n) means the entry is still waiting for an oracle condition.
A height queue key has bit 63 set. Other keys are queued timestamps. For a
fired condition, key is the slot into which it fired and deadlineSlot
is that slot plus the configured late window. dueSlot remains its original
not-before timestamp. Use key for an unrevealed entry’s 30-second grace
period, after excluding the watch-list sentinel. An entry remains sealed
when (flags & 16) != 0. Simulation of expire(id) checks complete eligibility,
including item removal and cursor position.