All articles
solanasurfpooltestinglocal-devjito

Surfpool v1.4.0: Jito bundle simulation, slot subscriptions, and snapshot exports

Surfpool v1.4.0 ships simulateBundle for local Jito bundle testing, slotUpdatesSubscribe for real-time slot streaming, and filtered snapshot exports. Plus four bug fixes that affect snapshot loading and getSignaturesForAddress.

Share

Surfpool v1.4.0 dropped on June 20th with three new features and four bug fixes. The headline is simulateBundle — local execution of Jito bundles, which makes Surfpool useful for the MEV and bundle-aware part of the Solana ecosystem that previously had no clean local testing path.

If you're new to Surfpool, start with the overview. This article covers what's new in this release.

simulateBundle: test Jito bundles locally

Jito bundles are groups of up to five transactions that execute atomically — all succeed or all fail, in order, with no other transactions interleaved. They're how MEV bots and sophisticated protocols guarantee ordering and atomicity on Solana. Until now, testing bundle logic meant either deploying to mainnet or building a bespoke simulation harness.

v1.4.0 adds simulateBundle directly to Surfpool's RPC surface. Pass an array of transactions; they execute against your local (possibly mainnet-forked) state in atomic sequence, returning per-transaction results and logs:

typescript
import { Connection, Transaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"

const connection = new Connection("http://localhost:8899")

// Build two transactions that must execute atomically
const tx1 = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: recipient1,
    lamports: 1 * LAMPORTS_PER_SOL,
  })
)

const tx2 = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: recipient2,
    lamports: 0.5 * LAMPORTS_PER_SOL,
  })
)

// Simulate as an atomic Jito bundle
const result = await connection.rpcRequest("simulateBundle", [
  {
    transactions: [
      tx1.serialize({ requireAllSignatures: false }).toString("base64"),
      tx2.serialize({ requireAllSignatures: false }).toString("base64"),
    ],
  },
  { encoding: "base64" },
])

// result.value contains per-tx simulation results
for (const txResult of result.value.transactionResults) {
  console.log("err:", txResult.err)
  console.log("logs:", txResult.logs)
  console.log("units consumed:", txResult.unitsConsumed)
}

The practical use case: test your arbitrage logic, liquidation bundle, or any atomicity-dependent flow against real mainnet account state (fork it first with surfpool start --fork mainnet) before submitting a single bundle to a Jito block engine.

slotUpdatesSubscribe: WebSocket slot streaming

v1.4.0 implements the slotUpdatesSubscribe WebSocket method — part of the standard Solana RPC WebSocket spec but previously missing from Surfpool. It pushes granular slot lifecycle events in real time: firstShredReceived, completed, createdBank, frozen, dead, and optimisticallyConfirmed.

typescript
import WebSocket from "ws"

const ws = new WebSocket("ws://localhost:8900")

ws.on("open", () => {
  ws.send(JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "slotUpdatesSubscribe",
  }))
})

ws.on("message", (data) => {
  const msg = JSON.parse(data.toString())
  if (msg.method === "slotNotification") {
    const { slot, type, timestamp } = msg.params.result
    console.log(`slot ${slot}${type} at ${timestamp}`)
  }
})

This fills the last remaining gap between Surfpool's WebSocket surface and what a production RPC exposes. If your app uses slotUpdatesSubscribe to drive optimistic UI updates or slot-aware logic, it now works identically against the local validator.

surfnet_exportSnapshot with filters

surfnet_exportSnapshot already let you export current local state as a snapshot file. v1.4.0 adds two filter parameters:

  • Sysvar filter — include or exclude sysvar accounts (SysvarClock, SysvarEpochSchedule, etc.) from the export. Useful when you want a clean snapshot that doesn't bake in a specific timestamp or epoch.
  • Feature-gate filter — include or exclude feature-gate accounts. When importing a snapshot into a network with a different feature set, you typically want to strip these so the destination validator's own features take effect.
sh
# Export snapshot excluding sysvars and feature gates
curl -X POST http://localhost:8899 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "surfnet_exportSnapshot",
    "params": [{
      "excludeSysvars": true,
      "excludeFeatureGates": true
    }]
  }'

Bug fixes

Four fixes worth knowing about if you hit these edges before:

  • SlotHashes sysvar for initial slots. The first 31 slots after startup were missing SlotHashessysvar data, which caused programs that read slot history to fail or behave incorrectly immediately after boot. Fixed in PR #686.
  • Snapshot loading order. When loading a snapshot, programdata accounts were being processed after program accounts. This caused upgradeable programs to fail to load correctly from snapshots. Fixed in PR #687 — programdata now loads first.
  • getSignaturesForAddress memo and block_time. For transactions executed locally (not forked from mainnet), getSignaturesForAddress was returning null for bothmemo and blockTime. Fixed in PR #693 — both are now populated correctly. This affects any code that indexes or displays local transaction history.
  • Pinocchio in Framework::FromStr. The CLI framework parser now handles Pinocchio as a valid framework identifier (PR #689), replacing the previous todo!() panic with a proper unimplemented!() error. Relevant if you use Pinocchio and run into framework detection in the CLI.

Upgrade

sh
# Re-run the installer to get v1.4.0
curl -sL https://run.surfpool.run/ | bash

# Confirm version
surfpool --version
# surfpool 1.4.0

References

Keep reading

Surfpool v1.4.0: Jito bundle simulation, slot subscriptions, and snapshot exports | devrels.xyz