Time Travel

The Surfpool runtime exposes three time-travel helpers. Each one moves the local clock to an absolute target (not a relative offset) and returns the updated EpochInfo so tests can assert that the runtime actually reached the requested time.

HelperTargetReturns
time_travel_to_slot / timeTravelToSlotAbsolute slot numberEpochInfo with the new absolute_slot
time_travel_to_epoch / timeTravelToEpochAbsolute epoch numberEpochInfo with the new epoch
time_travel_to_timestamp / timeTravelToTimestampUnix timestamp in millisecondsEpochInfo reflecting the implied slot

Forward only

Time travel can only move the clock forward. A target in the past is rejected rather than ignored — the Rust helpers return an Err and the JS helpers throw. The message names whichever unit went backwards, for example Cannot travel to past slot: target=1000, current=2000.

Jump To A Slot

use surfpool_sdk::Surfnet;
let surfnet = Surfnet::start().await?;
let cheats = surfnet.cheatcodes();
let info = cheats.time_travel_to_slot(1_000_000)?;
assert!(info.absolute_slot >= 1_000_000);

Jump To An Epoch

let info = cheats.time_travel_to_epoch(420)?;
assert_eq!(info.epoch, 420);

Jump To A Unix Timestamp

Pass the timestamp in milliseconds since the Unix epoch — the same unit as JavaScript's Date.now() — not seconds. The runtime computes the closest slot at that timestamp.

// 2030-01-01T00:00:00Z
let info = cheats.time_travel_to_timestamp(1_893_456_000_000)?;

The clock reports seconds, not milliseconds

Every timestamp you read back from the runtime is in seconds — the on-chain Clock sysvar's unix_timestamp, and the ClockValue carried by the systemClockUpdated event. Feeding one of those values straight back into a time-travel helper resolves to a moment in early 1970, so the call is rejected as a past target. Multiply by 1000 first.

// WRONG — `unixTimestamp` is in seconds, so this is a past target.
surfnet.timeTravelToTimestamp(clock.unixTimestamp + 3600);
// RIGHT — convert to milliseconds.
surfnet.timeTravelToTimestamp((clock.unixTimestamp + 3600) * 1000);

Pause And Resume The Clock

Time travel jumps to a target; pausing stops the clock from advancing at all. surfnet_pauseClock halts slot production and time progression until surfnet_resumeClock runs — reach for it when a test needs the slot and timestamp to hold still across several assertions.

Neither the Rust SDK nor the Surfnet class in @solana/surfpool wraps these two cheatcodes. Call them through the Kit plugin, or directly over JSON-RPC.

const paused = await client.cheatcodes.pauseClock().send();
// Nothing advances until the clock is resumed, so this is the only thing that
// moves the slot.
await client.cheatcodes
.timeTravel({ absoluteSlot: paused.absoluteSlot + 1_000n })
.send();
await client.cheatcodes.resumeClock().send();

Both cheatcodes return an EpochInfo — the clock state at the moment of the pause, and after resuming. Note the two things that shape does not carry:

  • No timestamp. EpochInfo has no time field. Read the simulated Unix time from the Clock sysvar instead: getAccountInfo on SysvarC1ock11111111111111111111111111111111 with jsonParsed encoding — again, in seconds.
  • No paused flag. No RPC method reports whether the clock is currently paused. From the SDKs, watch the clockUpdate event, which fires with a clockCommand whenever a pause, resume, or interval change runs. Otherwise, track the state in your test.

Common Patterns

Test A Lockup Or Vesting Window

The example below is a sketch — assertWithdrawFails and assertWithdrawSucceeds are placeholders for whatever client-side helpers your test suite uses to assert on RPC behavior.

import { Surfnet } from "@solana/surfpool";
const surfnet = Surfnet.start();
const beneficiary = Surfnet.newKeypair();
// 1. Set up a vesting account that unlocks at slot 1,000,000.
surfnet.setAccount(/* ...lockup program state... */);
// 2. Verify withdrawal fails before unlock.
await assertWithdrawFails(surfnet.rpcUrl, beneficiary);
// 3. Travel past the unlock slot.
surfnet.timeTravelToSlot(1_000_001);
// 4. Verify withdrawal succeeds.
await assertWithdrawSucceeds(surfnet.rpcUrl, beneficiary);
surfnet.stop();

Drive A Multi-Epoch Scenario

Intermediate slots are skipped

Time travel jumps directly to the target — moving from epoch 1 to epoch 5 skips the slots in between. If your program needs each intermediate epoch boundary to fire (for example, to credit per-epoch rewards), call time_travel_to_epoch once per epoch with any required transactions in between.

for epoch in 2..=5 {
cheats.time_travel_to_epoch(epoch)?;
surfnet.rpc_client().send_transaction(&claim_rewards_tx)?;
}

EpochInfo Shape

Both SDKs return an EpochInfo-shaped object with these fields:

FieldRustJS
Absolute slotabsolute_slot: u64absoluteSlot: number
Slot within epochslot_index: u64slotIndex: number
Slots per epochslots_in_epoch: u64slotsInEpoch: number
Epoch numberepoch: u64epoch: number
Block heightblock_height: u64blockHeight: number
Transaction counttransaction_count: Option<u64>transactionCount?: number

Is this page helpful?

Table of Contents

Edit Page
© 2026 Solana Foundation. All rights reserved.