Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supersim #29

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-elephants-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prool": patch
---

Added supersim instance support
6 changes: 6 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ jobs:
- name: Set up Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Set up Supersim
uses: jaxxstorm/[email protected]
with:
repo: ethereum-optimism/supersim
platform: linux

- name: Set up Rundler
uses: jaxxstorm/[email protected]
with:
Expand Down
33 changes: 32 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Prool is a library that provides programmatic HTTP testing instances for Ethereu

Prool contains a set of pre-configured instances that can be used to simulate Ethereum server environments, being:

- **Local Execution Nodes:** [`anvil`](#anvil-execution-node)
- **Local Execution Nodes:** [`anvil`](#anvil-execution-node), [`supersim`](#supersim-op-execution-nodes)
- **Bundler Nodes:** [`alto`](#alto-bundler-node), [`rundler`](#rundler-bundler-node), [`silius`](#silius-bundler-node), [`stackup`](#stackup-bundler-node)
- **Indexer Nodes:** `ponder`⚠️

Expand All @@ -51,6 +51,7 @@ You can also create your own custom instances by using the [`defineInstance` fun
- [Install](#install)
- [Getting Started](#getting-started)
- [Anvil (Execution Node)](#anvil-execution-node)
- [Supersim (OP Execution Nodes)](#supersim-op-execution-nodes)
- [Alto (Bundler Node)](#alto-bundler-node)
- [Rundler (Bundler Node)](#rundler-bundler-node)
- [Silius (Bundler Node)](#silius-bundler-node)
Expand Down Expand Up @@ -106,6 +107,36 @@ await server.start()

See [`AnvilParameters`](https://github.com/wevm/prool/blob/801ede06ded8b2cb2d59c95294aae795e548897c/src/instances/anvil.ts#L5).

### Supersim (OP Execution Nodes)

#### Requirements

- [Foundry](https://getfoundry.sh/) binary installed
- Download: `curl -L https://foundry.paradigm.xyz | bash`
- [Supersim](https://github.com/ethereum-optimism/supersim?tab=readme-ov-file#2-install-supersim) binary installed

#### Usage

```ts
import { createServer } from 'prool'
import { supersim } from 'prool/instances'

const server = createServer({
instance: supersim(),
})

await server.start()
// Instances accessible at:
// "http://localhost:8545/1"
// "http://localhost:8545/2"
// "http://localhost:8545/3"
// "http://localhost:8545/n"
```

#### Parameters

See [`SupersimParameters`](https://github.com/wevm/prool/blob/ae34711701d3f4316a5e85442ae618a6506f55e1/src/instances/supersim.ts#L5).

### Alto (Bundler Node)

#### Requirements
Expand Down
1 change: 1 addition & 0 deletions src/exports/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { anvil, type AnvilParameters } from '../instances/anvil.js'
export { rundler, type RundlerParameters } from '../instances/rundler.js'
export { silius, type SiliusParameters } from '../instances/silius.js'
export { stackup, type StackupParameters } from '../instances/stackup.js'
export { supersim, type SupersimParameters } from '../instances/supersim.js'
110 changes: 110 additions & 0 deletions src/instances/supersim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { afterEach, expect, test } from 'vitest'
import type { Instance } from '../instance.js'
import { type SupersimParameters, supersim } from './supersim.js'

const instances: Instance[] = []

const defineInstance = (parameters: SupersimParameters = {}) => {
const instance = supersim(parameters)
instances.push(instance)
return instance
}

afterEach(async () => {
for (const instance of instances) await instance.stop().catch(() => {})
})

test('default', async () => {
const messages: string[] = []
const stdouts: string[] = []

const instance = defineInstance()

instance.on('message', (m) => messages.push(m))
instance.on('stdout', (m) => stdouts.push(m))

expect(instance.messages.get()).toMatchInlineSnapshot('[]')

await instance.start()
expect(instance.status).toEqual('started')

expect(messages.join('')).toBeDefined()
expect(stdouts.join('')).toBeDefined()
expect(instance.messages.get().join('')).toBeDefined()

await instance.stop()
expect(instance.status).toEqual('stopped')

expect(messages.join('')).toBeDefined()
expect(stdouts.join('')).toBeDefined()
expect(instance.messages.get()).toMatchInlineSnapshot('[]')
})

test('behavior: start supersim in forked mode', async () => {
const messages: string[] = []

const instance = defineInstance({
fork: {
chains: ['base', 'op'],
},
})

instance.on('message', (m) => messages.push(m))

await instance.start()

expect(messages.join('')).toContain('chain.id=10')
expect(messages.join('')).toContain('chain.id=8453')
})

test('behavior: start supersim with different ports', async () => {
const instance = defineInstance({
l1Port: 9000,
l2StartingPort: 9001,
})
await instance.start()
})

test('behavior: start and stop multiple times', async () => {
const instance = defineInstance()

await instance.start()
await instance.stop()
await instance.start()
await instance.stop()
await instance.start()
await instance.stop()
await instance.start()
await instance.stop()
})

test('behavior: exit', async () => {
const instance = defineInstance()

let exitCode: number | null | undefined = undefined
instance.on('exit', (code) => {
exitCode = code
})

await instance.start()
expect(instance.status).toEqual('started')

instance._internal.process.kill()

await new Promise<void>((res) => setTimeout(res, 100))
expect(instance.status).toEqual('stopped')
expect(typeof exitCode !== 'undefined').toBeTruthy()
})

test('behavior: exit when status is starting', async () => {
const instance = defineInstance()

const promise = instance.start()
expect(instance.status).toEqual('starting')

instance._internal.process.kill()

await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to start process "supersim": exited]`,
)
})
97 changes: 97 additions & 0 deletions src/instances/supersim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { defineInstance } from '../instance.js'
import { execa } from '../processes/execa.js'
import { toArgs } from '../utils.js'

export type SupersimParameters = {
/** The host the server will listen on. */
host?: string

/** Listening port for the L1 instance. `0` binds to any available port */
l1Port?: number

/** Starting port to increment from for L2 chains. `0` binds each chain to any available port */
l2StartingPort?: number

/** Locally fork a network in the superchain registry */
fork?: {
/** L1 height to fork the superchain (bounds L2 time). `0` for latest */
l1ForkHeight?: number

/** chains to fork in the superchain, example mainnet options: [base, lyra, metal, mode, op, orderly, race, tbn, zora] */
chains: string[]

/** superchain network. example options: mainnet, sepolia, sepolia-dev-0 */
network?: string
}

interop?: {
/** Automatically relay messages sent to the L2ToL2CrossDomainMessenger using account 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 */
autorelay?: boolean
}
}

const DEFAULT_HOST = 'localhost'
const DEFAULT_PORT = 8545

/**
* Defines an Supersim instance.
*
* @example
* ```ts
* const instance = supersim()
* await instance.start()
* // ...
* await instance.stop()
* ```
*/
export const supersim = defineInstance((parameters?: SupersimParameters) => {
const binary = 'supersim'

const name = 'supersim'
const process = execa({ name })

const args = toArgs({
// ports
'l1.port': parameters?.l1Port,
'l2.starting.port': parameters?.l2StartingPort,

// interop
'interop.autorelay': parameters?.interop?.autorelay,

// fork
'l1.fork.height': parameters?.fork?.l1ForkHeight,
chains: parameters?.fork?.chains,
network: parameters?.fork?.network,
})

if (parameters?.fork) {
args.unshift('fork')
}

return {
_internal: {
parameters,
get process() {
return process._internal.process
},
},
host: parameters?.host ?? DEFAULT_HOST,
name,
port: parameters?.l1Port ?? DEFAULT_PORT,
async start(_, options) {
return await process.start(($) => $(binary, args), {
...options,
resolver({ process, reject, resolve }) {
process.stdout.on('data', (data) => {
const message = data.toString()
if (message.includes('supersim is ready')) resolve()
})
process.stderr.on('data', (data) => reject(data.toString()))
},
})
},
async stop() {
await process.stop()
},
}
})