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.
| Helper | Target | Returns |
|---|---|---|
time_travel_to_slot / timeTravelToSlot | Absolute slot number | EpochInfo with the new absolute_slot |
time_travel_to_epoch / timeTravelToEpoch | Absolute epoch number | EpochInfo with the new epoch |
time_travel_to_timestamp / timeTravelToTimestamp | Unix timestamp in milliseconds | EpochInfo 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:00Zlet 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.
EpochInfohas no time field. Read the simulated Unix time from theClocksysvar instead:getAccountInfoonSysvarC1ock11111111111111111111111111111111withjsonParsedencoding — again, in seconds. - No paused flag. No RPC method reports whether the clock is currently
paused. From the SDKs, watch the
clockUpdateevent, which fires with aclockCommandwhenever 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:
| Field | Rust | JS |
|---|---|---|
| Absolute slot | absolute_slot: u64 | absoluteSlot: number |
| Slot within epoch | slot_index: u64 | slotIndex: number |
| Slots per epoch | slots_in_epoch: u64 | slotsInEpoch: number |
| Epoch number | epoch: u64 | epoch: number |
| Block height | block_height: u64 | blockHeight: number |
| Transaction count | transaction_count: Option<u64> | transactionCount?: number |
Is this page helpful?