时间旅行

Surfpool 运行时提供了三个时间旅行辅助函数。每个函数均将本地时钟移动至绝对目标时间(而非相对偏移量),并返回更新后的 EpochInfo,以便测试用例验证运行时是否确实到达了请求的时间。

辅助函数目标返回值
time_travel_to_slot / timeTravelToSlot绝对 slot 编号包含新 absolute_slotEpochInfo
time_travel_to_epoch / timeTravelToEpoch绝对 epoch 编号包含新 epochEpochInfo
time_travel_to_timestamp / timeTravelToTimestamp毫秒级 Unix 时间戳反映对应 slot 的 EpochInfo

仅支持向前跳转

时间旅行只能将时钟向前拨动。目标时间若位于过去则会被拒绝而非忽略——Rust 辅助函数将返回 Err,JS 辅助函数将抛出异常。错误消息会指明哪个单位发生了回退,例如 Cannot travel to past slot: target=1000, current=2000

跳转至指定 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);

跳转至指定 Epoch

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

跳转至指定 Unix 时间戳

请以毫秒为单位传入 Unix 纪元以来的时间戳——与 JavaScript 的 Date.now() 单位相同——而非秒。运行时将计算该时间戳对应的最近 slot。

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

时钟返回的是秒,而非毫秒

从运行时读取回来的每个时间戳均以为单位——包括链上 Clock sysvar 的 unix_timestamp,以及 systemClockUpdated 事件携带的 ClockValue。若将这些值直接传入时间旅行辅助函数,会被解析为 1970 年初的某个时刻,导致调用因目标时间在过去而被拒绝。请先乘以 1000。

// 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);

暂停与恢复时钟

时间旅行跳转到目标时间;暂停则完全停止时钟的推进。surfnet_pauseClock 会停止 slot 生产和时间推进,直到 surfnet_resumeClock 运行——当测试需要 slot 和时间戳在多个断言期间保持不变时,可使用此功能。

Rust SDK 和 @solana/surfpool 中的 Surfnet 类均未封装这两个作弊码。请通过 Kit 插件调用它们,或直接通过 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();

两个作弊码均返回一个 EpochInfo——分别对应暂停时刻和恢复后的时钟状态。请注意该结构体包含以下两项信息:

  • 无时间戳。 EpochInfo 没有时间字段。请改从 Clock sysvar 读取模拟的 Unix 时间:对 SysvarC1ock11111111111111111111111111111111 使用 jsonParsed 编码调用 getAccountInfo——同样以秒为单位。
  • 无暂停标志。 没有任何 RPC 方法能报告时钟当前是否处于暂停状态。在 SDK 层面,可监听 clockUpdate 事件,每当执行暂停、恢复或间隔变更操作时该事件会携带 clockCommand 触发。否则,请在测试中自行跟踪状态。

常见使用模式

测试锁定期或归属窗口

以下示例仅为示意——assertWithdrawFailsassertWithdrawSucceeds 是占位符,代表您的测试套件中用于断言 RPC 行为的客户端辅助函数。

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();

驱动多 epoch 场景

中间 slot 将被跳过

时间旅行会直接跳转到目标位置 —— 从 epoch 1 跳转到 epoch 5 会跳过中间的所有 slot。如果您的程序需要触发每个中间 epoch 的边界事件(例如,用于计算每 epoch 的奖励),请在每个 epoch 之间 携带所需交易调用一次 time_travel_to_epoch

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

EpochInfo 结构

两个 SDK 均返回一个 EpochInfo 结构的对象,包含以下字段:

字段RustJS
绝对 slotabsolute_slot: u64absoluteSlot: number
epoch 内的 slotslot_index: u64slotIndex: number
每 epoch 的 slot 数slots_in_epoch: u64slotsInEpoch: number
epoch 编号epoch: u64epoch: number
区块高度block_height: u64blockHeight: number
交易数量transaction_count: Option<u64>transactionCount?: number

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有