A tool for forking live Substrate/Polkadot-SDK chains locally, built natively on polkadot-api.
Forklift is inspired by @acala-network/chopsticks, but it was built primarily for testing workflows that need multiple live branches of the chain, making it possible to simulate forks, reorgs, and pruned branches.
- Multiple live branches of the chain, including competing forks, reorgs, and pruned branches
- Immutable merkle-trie-backed storage with structural sharing between blocks
- Relay / parachain wiring helpers for local XCM testing
- Native
polkadot-apiimplementation withoutpolkadot-js - Based on the new chainHead_v1 / archive_v1 JSON-RPC methods
- YAML-based CLI config for single-chain and multi-chain setups
pnpm i @polkadot-api/forkliftThen run:
pnpm forklift --helpForklift can be started in two ways:
- Directly from a remote endpoint
- From a YAML config file
forklift <url> [options]Arguments:
| Argument | Description |
|---|---|
url |
WebSocket URL of the node to fork |
Options:
| Option | Description | Default |
|---|---|---|
-b, --block <block> |
Block number or block hash to fork from | latest finalized |
-p, --port <port> |
Preferred local WebSocket port | 3000 |
-c, --config <file> |
Load a YAML config instead of using direct mode | |
-l, --log-level <level> |
Log level: trace, debug, info, warn, error, fatal |
info |
Examples:
# Fork the latest finalized block
forklift wss://rpc.polkadot.io
# Fork a specific block number
forklift wss://rpc.polkadot.io --block 22000000
# Fork a specific block hash
forklift wss://rpc.polkadot.io --block 0xabc123...
# Prefer a specific local port
forklift wss://rpc.polkadot.io --port 9000The forklift CLI exposes a JSON-RPC WebSocket endpoint. In direct mode that is typically:
ws://localhost:3000If the requested port is already in use, forklift will try the next free port.
For anything beyond a single fork, the YAML config is the intended interface.
forklift --config forklift.ymlThe config supports either:
- a single chain at the root level
- multiple named chains under
chains:
endpoint: wss://rpc.polkadot.io
block: 22000000
port: 3000
options:
buildBlockMode:
timer: 100
finalizeMode:
timer: 2000
storage:
- key: 0x1234567890
value: nullchains:
relay:
endpoint: wss://rpc.polkadot.io
port: 3000
assetHub:
endpoint: wss://sys.ibp.network/asset-hub-polkadot
port: 3001
parachainOf: relay
bridgeHub:
endpoint: wss://sys.ibp.network/bridge-hub-polkadot
port: 3002
parachainOf: relayIn multi-chain mode:
- each entry under
chains:starts its own local fork parachainOf: <name>declares that a chain should be attached to another local chain as its relay- chains that share the same relay are also attached to each other as siblings
That makes the config suitable for relay/parachain and parachain/parachain XCM testing setups.
Each chain config supports the following fields:
| Field | Type | Description |
|---|---|---|
endpoint |
string | string[] |
Remote WebSocket endpoint or endpoints to fork from |
block |
number | string |
Optional block number or block hash to fork from |
port |
number |
Preferred local WebSocket port |
parachainOf |
string |
Name of the relay chain in a multi-chain config |
options |
object |
Forklift runtime options |
storage |
array |
Storage overrides applied after startup |
options maps closely to the programmatic ForkliftOptions.
options:
disableOnIdle: false
buildBlockMode:
timer: 100
finalizeMode:
timer: 2000Supported values:
-
disableOnIdle: booleanDisableson_idlehooks during block production. Some runtimes might perform actions that take a long time as they perform multiple serial storage queries. Setting this option totruedisables that hook, which can increase the speed blocks can be produced. -
buildBlockModeControls when new blocks are built after transactions arrive.Manual mode:
buildBlockMode: manual
Timer mode:
buildBlockMode: timer: 100
-
finalizeModeControls when built blocks are finalized.Manual mode:
finalizeMode: manual
Timer mode:
finalizeMode: timer: 2000
Notes:
manualmeans forklift only changes state when you explicitly drive it{ timer: 0 }is allowed and means immediate scheduling- if
portis omitted, forklift will choose a free port automatically
The storage section is applied after the local server has started and the initial block is available.
Forklift supports two storage override forms.
Use raw SCALE-encoded keys and values directly:
storage:
- key: 0x1234...
value: 0xabcd...
- key: 0x5678...
value: nullUse null to delete or clear a storage entry.
Use pallet / storage names and let the CLI encode the key and value from metadata:
storage:
- pallet: System
entry: Account
key:
- 14GjNs7Lw7nVbJrL8aL8m8m4vY2mQ2L9mQf8u2YpK9nQx7aD
value:
providers: 1
consumers: 0
sufficients: 0
data:
free: 100_0_000_000_000n
reserved: 0n
frozen: 0n
flags: 170141183460469231731687303715884105728nNotes:
keymust be an array in decoded form, even if the storage entry takes a single key- big integers can be written as strings ending in
n, for example1000000000000n - underscores are accepted in numeric strings for readability
- if a storage item, key, or value cannot be encoded against the chain metadata, forklift logs the error and skips that override
You can also create a chain from code:
import { forklift, wsSource } from "@polkadot-api/forklift";
import { Enum } from "polkadot-api";
const polkadot = forklift(
wsSource("wss://rpc.polkadot.io", {
atBlock: 22000000,
}),
{
buildBlockMode: Enum("timer", 100),
finalizeMode: Enum("timer", 2000),
disableOnIdle: false,
}
);The forklift instance then has a property serve which is a JsonRpcProvider - This is an unopinionated interface that serves JSON-RPC connections, and can be plugged directly into polkadot-api:
import { forklift } from "@polkadot-api/forklift";
import { createClient } from "polkadot-api";
const polkadot = forklift(/* … */);
const client = createClient(polkadot.serve);Or, given it's a simple interface, it's simple to expose that to a WS. For instance, using bun:
import { forklift } from "@polkadot-api/forklift";
const polkadot = forklift(/* … */);
Bun.serve({
fetch(req, server) {
// Al WS connections start with a HTTP request, we tell bun to upgrade the connection to a WS
const success = server.upgrade(req, { data: {} });
if (success) {
return undefined;
}
// handle HTTP request normally
return new Response("Nothing to see here, move along");
},
websocket: {
data: {} as any,
open(ws) {
// When the WS opens we call the JsonRpcProvider to open a connection, and wire up incoming messages from forklift to send them out to the WS
ws.data.connection = forklift.serve((msg) =>
ws.send(JSON.stringify(msg))
);
},
close(ws) {
// When it closes we just close the connection
ws.data.connection.disconnect();
},
async message(ws, message) {
// When we receive a message we just pass it down to forklift
ws.data.connection.send(JSON.parse(message as string));
},
},
});interface Forklift {
serve: JsonRpcProvider;
newBlock(opts?: Partial<NewBlockOptions>): Promise<HexString>;
changeBest(hash: HexString): Promise<void>;
changeFinalized(hash: HexString): Promise<void>;
setStorage(
hash: HexString,
changes: Record<string, Uint8Array>
): Promise<void>;
getStorageDiff(
hash: HexString,
baseHash?: HexString
): Promise<
Record<string, { value: Uint8Array | null; prev?: Uint8Array | null }>
>;
changeOptions(opts: Partial<ForkliftOptions>): void;
destroy(): void;
}buildBlockMode controls when new blocks are produced:
Enum("manual"): only explicitnewBlock()calls produce blocksEnum("timer", ms): automatically produce a block after a transaction arrives
finalizeMode controls when blocks are finalized:
Enum("manual"): only explicitchangeFinalized()calls finalize blocksEnum("timer", ms): automatically finalize a block after it is built
Pass a parent hash to branch from any existing block:
const base = await f.newBlock();
const forkA = await f.newBlock({ parent: base, type: "fork" });
const forkB = await f.newBlock({ parent: base, type: "fork" });await f.setStorage(hash, {
"0x...key": new Uint8Array([...value]),
});
const diff = await f.getStorageDiff(hash);Forklift serves a WebSocket JSON-RPC endpoint and currently includes methods in these groups:
archive_v1_*chainHead_v1_*chainSpec_v1_*transaction_v1_*dev_*forklift_xcm_*
Forklift is heavily inspired by @acala-network/chopsticks and reuses its WASM executor package, @acala-network/chopsticks-executor, for local runtime execution.