Sysvars: Clock, Rent, and Deterministic Globals
The previous page closed the loop on code calling code: a program can invoke another program and even sign for accounts it owns, all inside the sandbox. But a program often needs something more mundane than another program — it needs to know what time it is, or what a byte of on-chain storage costs. A vesting contract unlocks tokens “after March”; an auction closes “at slot 250,000,000”; an account-creation instruction must fund the new account with “enough to survive.” All of these are questions about the environment, not about any account the transaction named.
Here is the problem the whole part has been circling. Back in Syscalls and the Sandbox we established that a program has no host access: no files, no network, no gettimeofday. Those calls are banned precisely because they are nondeterministic — two validators calling the real clock would read two different times, execute two different branches, and produce two different states. A single global state machine cannot tolerate that. So how does a program that is forbidden from asking the OS for the time still get the time? The answer is the last piece of the runtime: sysvars.
The idea: environment as a read-only account
Section titled “The idea: environment as a read-only account”A sysvar is not a syscall to the outside world. It is an account — an ordinary entry in the account database, at a well-known fixed address — whose data buffer the runtime itself writes and keeps current. The program reads it exactly the way it reads any other account: bytes in, deserialize, use. The trick is who writes it and when.
BANNED path (nondeterministic) SYSVAR path (deterministic) ────────────────────────────── ─────────────────────────── program ──► syscall ──► OS clock runtime writes Clock account │ once per slot, identically ▼ on every validator each host answers │ differently ✗ ▼ program ──► reads Clock account same bytes everywhere ✓The environment data still flows through the account model, the same sandboxed, deterministic channel every other read uses. Nothing new is punched through the sandbox wall. The runtime pre-computes the values that every validator must agree on, stamps them into a fixed-address account before executing the block’s transactions, and the program simply reads state — as it always does. That is the entire elegance of the design: “the time” is just another account, and because every validator computes that account’s contents from the same consensus inputs, every validator reads the same time.
In our companion crate the account is already the universal container. Recall from the account model that an Account is nothing but a balance, a byte buffer, and an owner:
pub struct Account { pub lamports: u64, pub data: Vec<u8>, // a sysvar is just this buffer, written by the runtime pub owner: Pubkey,}A sysvar is that struct with two special properties: its address is a fixed, well-known key, and the only writer is the runtime (never a user program). Everything else is the machinery you already know.
The Clock sysvar: how a sandbox tells time
Section titled “The Clock sysvar: how a sandbox tells time”The Clock sysvar is how a program answers “when is now?” without touching a wall clock. On real Solana it carries roughly these fields:
pub struct Clock { pub slot: u64, // the current slot height pub epoch_start_timestamp: i64, // unix time the epoch began pub epoch: u64, // which epoch we are in pub leader_schedule_epoch: u64, // epoch the leader schedule is fixed for pub unix_timestamp: i64, // the network-agreed wall-clock time}Two of these fields are worth pulling apart, because they answer “time” in two different currencies.
slot is the honest, fully deterministic clock. A slot is a fixed window in which one leader may produce a block; slots advance monotonically. Because slot height is a direct consequence of Proof of History and consensus — not of anyone’s wristwatch — every validator agrees on it exactly. If your program’s logic can be expressed as “at or after slot N,” use slot. It is the safest notion of time on the chain: it never disagrees, never jumps backward, and cannot be argued about.
unix_timestamp is the convenient-but-negotiated clock. Humans think in dates, not slot numbers, so the network also maintains an approximate wall-clock time. But no single machine’s clock is authoritative — so where does one number come from? It is derived, not read: validators submit their local clock readings as part of voting, and the runtime combines them (a stake-weighted estimate, corrected against the expected slot rate) into a single value that every validator then writes identically into the Clock account. It is real calendar time, close to correct, but it is a negotiated estimate, not a precise oracle. Treat it as such: fine for “unlocks in March,” dangerous for “expires in exactly 30 seconds.”
Reading the Clock two ways
Section titled “Reading the Clock two ways”A program can obtain a sysvar through either of two doors, and understanding why both exist tells you a lot about the account model.
Door 1 — passed in as an account. The transaction lists the sysvar’s fixed address in its account list, exactly like any other account, and the runtime hands the program the current bytes. The program deserializes them. This is fully consistent with the Sealevel scheduler: the sysvar was declared up front, so the scheduler knew the transaction reads it. Sysvars are always read-only, so declaring one never causes a write-conflict — any number of transactions can read Clock in parallel, the same way our runtime treats a shared read-only account:
// read-only shared accounts never conflict — many txs can read Clock at onceif ma.key == mb.key && (ma.is_writable || mb.is_writable) { return true; // conflict ONLY if at least one side writes}Door 2 — a get() syscall. Real Solana also exposes Clock::get(), a syscall that returns the current sysvar without the caller having to list it in the transaction’s accounts. Under the hood this is a controlled syscall: the runtime already holds the value, so it can answer directly and cheaply. It is convenient — no account plumbing — but it is still the runtime handing back a value it computed deterministically, not the program reaching out to a host. Either door delivers the identical bytes on every validator; that identity is the whole point.
// Real Anchor / on-chain program — both of these read the SAME deterministic bytes:use solana_program::sysvar::clock::Clock;use solana_program::sysvar::Sysvar;
let clock = Clock::get()?; // door 2: syscall, nothing to declare// ...or, when passed in as an account:let clock = Clock::from_account_info(&clock_account_info)?; // door 1
if clock.slot < unlock_slot { return Err(ErrorCode::TooEarly.into()); // deterministic on every node}Whichever door you pick, the guarantee is the same: because the runtime wrote the value from consensus inputs, the branch above evaluates identically on every validator, and the state machine stays single-valued.
The Rent sysvar: pricing permanent state
Section titled “The Rent sysvar: pricing permanent state”The second sysvar every serious program touches is Rent, and it exists to solve an externality the earlier parts flagged and deferred. Recall the account model: state lives in accounts, and every account occupies real memory on every validator, forever. That is a cost the network bears collectively. If creating state were free, anyone could bloat every validator’s RAM with junk accounts at no price — the classic tragedy of a shared, permanent resource.
Rent is how Solana prices that externality. The rule today is simple to state: an account must hold a minimum lamport balance to persist. Hold at least that minimum and the account is rent-exempt — it lives indefinitely and is never charged. Fall below it and the account is subject to collection and can be purged. In practice essentially all accounts are created rent-exempt: you pre-pay a deposit sized to the account’s byte length, and that deposit is refunded in full when you close the account and reclaim its lamports.
The Rent sysvar exposes the parameters a program needs to compute that minimum:
pub struct Rent { pub lamports_per_byte_year: u64, // the base price of a byte-year pub exemption_threshold: f64, // years of rent you pre-pay to be exempt pub burn_percent: u8, // share of collected rent that is burned}The exemption minimum is a function of the account’s size, because bigger accounts cost every validator more memory:
min_balance(bytes) = (ACCOUNT_STORAGE_OVERHEAD + bytes) * lamports_per_byte_year * exemption_threshold
larger data buffer ─► larger minimum ─► you pay for what you occupyA program computes this with Rent::get()?.minimum_balance(data_len) and refuses to create an account underfunded — otherwise the account it just created could be swept away underneath it. Pricing storage by size is the direct economic answer to “how do you stop a shared, permanent state store from being spammed to death?” — the same question a chain-wide state machine must answer to survive at scale.
Under the hood — why sysvars, and not “just ask the runtime”
Section titled “Under the hood — why sysvars, and not “just ask the runtime””It is fair to ask why the environment is modeled as accounts at all, instead of a grab-bag of syscalls. The account framing buys three things at once.
- Determinism by construction. A sysvar’s value is written once per block, before execution, from consensus inputs. There is no live query, no clock read during execution that could differ across machines. The value is fixed for the whole block, identical everywhere.
- Schedulability. Because a sysvar is an account, the Sealevel scheduler already knows how to reason about it: it is read-only, so it never blocks parallelism, and a transaction that touches it declared it up front like everything else. No special case in the scheduler.
- Uniformity. There is exactly one way to get data into a program: read an account (or a
get()that returns the same account’s contents). “The time” and “the rent price” ride the same rails as a user’s token balance. Fewer mechanisms means fewer places for nondeterminism to leak in.
The get() syscalls (Clock::get, Rent::get) are a convenience layer on top of this model, not an escape from it — they hand back the exact bytes the runtime already stamped into the sysvar account.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a deterministic, sandboxed program is forbidden from reading a real clock or any host state, yet still needs the environment — the time, the epoch, the price of storage. Sysvars deliver that data through the one channel the sandbox permits: a read-only account.
- What problem does it solve? Nondeterminism. If each validator read its own clock, executions would diverge and the single global state machine would fork. Sysvars pre-compute the shared values from consensus inputs so every validator reads identical bytes and every execution converges.
- What are the trade-offs? Values are fixed per block, not live — you read “the time as of this slot,” not this microsecond. And
unix_timestampis a stake-weighted estimate, so it can drift; precision costs you the safety ofslot. Rent, meanwhile, forces every account creator to pre-fund a deposit, adding friction in exchange for bounded state growth. - When should I avoid it? Never avoid reading sysvars when you need environment data — there is no legitimate alternative in the sandbox. But avoid misusing
unix_timestampfor sub-minute precision or as a tamper-proof oracle; useslotfor exact, unarguable timing. - What breaks if I remove it? Programs lose all lawful access to time and economic parameters. They would either be blind to “now” (no vesting, no auctions, no deadlines) or forced toward the banned nondeterministic host calls — which would fork the chain. Remove rent and permanent state becomes free, inviting unbounded account spam that every validator must store forever.
Check your understanding
Section titled “Check your understanding”- A program cannot call
gettimeofday. So how does it get the current time, and why is that mechanism safe for a global state machine? - The
Clocksysvar offers bothslotandunix_timestamp. Which is fully deterministic, which is a negotiated estimate, and when would you choose each? - What are the two ways a program can obtain a sysvar’s value, and why does listing a sysvar in a transaction’s account list never cause a write-conflict in the scheduler?
- What externality does rent price, and what does it mean for an account to be rent-exempt?
- Restate, in one sentence, how sysvars “close the loop” of this part of the book.
Show answers
- It reads a sysvar — the
Clockaccount, a read-only account at a fixed address whose bytes the runtime writes once per block from consensus inputs. It is safe because every validator computes the same value before execution and the program merely reads state, so no nondeterministic host call is involved and all executions converge. slotis fully deterministic — it comes from PoH and consensus, so every validator agrees exactly; use it whenever correctness must be exact (“at or after slot N”).unix_timestampis a stake-weighted estimate combined from validators’ local clocks; it is convenient calendar time for human-scale, generously-bounded conditions (“after March”) but can drift, so avoid it for precise deadlines.- Either passed in as an account (declared in the transaction’s account list, deserialized like any account) or via a
get()syscall (e.g.Clock::get()) that returns the same runtime-computed bytes without declaring the account. Listing a sysvar never conflicts because sysvars are read-only, and the scheduler only serializes transactions when at least one side writes a shared account. - Rent prices the permanent-state externality: every account occupies memory on every validator forever, a shared cost that would otherwise be free to inflict. An account is rent-exempt when it holds at least a size-proportional minimum balance; it then persists indefinitely and is never charged, and the deposit is refunded when the account is closed.
- Even “environment” data — the time and the network’s economic parameters — is delivered through the same deterministic, sandboxed, read-only account model as everything else, so nothing in program execution ever depends on nondeterministic host access.