Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Flags:

- `--external-builder`: URL of an external builder to use (enables rollup-boost)
- `--enable-latest-fork` (int): Enables the latest fork (isthmus) at startup (0) or n blocks after genesis.
- `--predeploy-json` (string[]): One or more paths to JSON files describing L2 predeploy accounts to inject into the L2 genesis (e.g. EntryPoint, paymasters, custom system contracts).

### Example Commands

Expand Down
54 changes: 54 additions & 0 deletions playground/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ type ArtifactsBuilder struct {
genesisDelay uint64
applyLatestL2Fork *uint64
OpblockTime uint64

// JSON files describing additional L2 predeploy accounts to inject into
// the L2 genesis alloc (e.g. EntryPoint, paymasters, or other system contracts).
predeployJSONFiles []string
}

func NewArtifactsBuilder() *ArtifactsBuilder {
Expand Down Expand Up @@ -101,6 +105,14 @@ func (b *ArtifactsBuilder) OpBlockTime(blockTimeSeconds uint64) *ArtifactsBuilde
return b
}

// PredeployJSONFiles configures the builder with one or more JSON files
// that each describe a single L2 predeploy account to inject into the
// L2 genesis alloc.
func (b *ArtifactsBuilder) PredeployJSONFiles(files []string) *ArtifactsBuilder {
b.predeployJSONFiles = files
return b
}

type Artifacts struct {
Out *output
}
Expand Down Expand Up @@ -295,6 +307,27 @@ func (b *ArtifactsBuilder) Build() (*Artifacts, error) {
}
}

// Inject any additional L2 predeploy accounts provided via JSON files.
for _, path := range b.predeployJSONFiles {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read L2 predeploy JSON %q: %w", path, err)
}

genesisAllocMap, err := loadPredeployAlloc(data)
if err != nil {
return nil, fmt.Errorf("failed to load L2 predeploy from %q: %w", path, err)
}

Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple predeploy JSON files specify the same address, the later file will silently overwrite the earlier one without any warning. This could lead to unexpected behavior where one predeploy contract unintentionally replaces another.

Consider adding a check for duplicate addresses:

for _, path := range b.predeployJSONFiles {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read L2 predeploy JSON %q: %w", path, err)
    }

    addr, account, err := loadPredeployAlloc(data)
    if err != nil {
        return nil, fmt.Errorf("failed to load L2 predeploy from %q: %w", path, err)
    }

    if _, exists := allocs[addr]; exists {
        return nil, fmt.Errorf("duplicate predeploy address %s found in %q", addr, path)
    }

    allocs[addr] = account
}
Suggested change
if _, exists := allocs[addr]; exists {
return nil, fmt.Errorf("duplicate predeploy address %s found in %q", addr, path)
}

Copilot uses AI. Check for mistakes.
for addr, account := range genesisAllocMap {
if _, exists := allocs[addr.String()]; exists {
return nil, fmt.Errorf("L2 predeploy address %s already exists in allocs, cannot inject duplicate address", addr.String())
}

allocs[addr.String()] = account
}
}

newOpGenesis, err := overrideJSON(opGenesis, input)
if err != nil {
return nil, err
Expand Down Expand Up @@ -348,6 +381,27 @@ func (b *ArtifactsBuilder) Build() (*Artifacts, error) {
return &Artifacts{Out: out}, nil
}

// PredeployAlloc is a generic JSON schema for describing a single L2
// predeploy account to be injected into the L2 genesis alloc.

// loadPredeployAlloc parses a JSON blob describing a single L2 predeploy
// account and returns the address plus a genesis alloc entry.
func loadPredeployAlloc(raw []byte) (types.GenesisAlloc, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("predeploy JSON is empty")
}

var genesisAllocMap types.GenesisAlloc
err := genesisAllocMap.UnmarshalJSON(raw)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal predeploy JSON: %w", err)
}

// Validation helpers (kept local; could also live in utils.go)

return genesisAllocMap, nil
}

type OpGenesisTmplInput struct {
Timestamp uint64
LatestFork *uint64
Expand Down
14 changes: 14 additions & 0 deletions playground/recipe_opstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type OpRecipe struct {

// whether to enable websocket proxy
enableWebsocketProxy bool

// JSON files describing L2 predeploy accounts to inject into L2 genesis
// (e.g. ERC-4337 EntryPoint, paymasters, or other system contracts).
// Each file should define a single account in a simple JSON format.
l2PredeployJSON []string
}

func (o *OpRecipe) Name() string {
Expand All @@ -56,13 +61,22 @@ func (o *OpRecipe) Flags() *flag.FlagSet {
flags.BoolVar(&o.baseOverlay, "base-overlay", false, "Whether to use base implementation for flashblocks-rpc")
flags.StringVar(&o.flashblocksBuilderURL, "flashblocks-builder", "", "External URL of builder flashblocks stream")
flags.BoolVar(&o.enableWebsocketProxy, "enable-websocket-proxy", false, "Whether to enable websocket proxy")
flags.StringArrayVar(
&o.l2PredeployJSON,
"predeploy-json",
nil,
"Path(s) to JSON file(s) describing L2 predeploy accounts injected into L2 genesis (e.g. EntryPoint)",
)
return flags
}

func (o *OpRecipe) Artifacts() *ArtifactsBuilder {
builder := NewArtifactsBuilder()
builder.ApplyLatestL2Fork(o.enableLatestFork)
builder.OpBlockTime(o.blockTime)
if len(o.l2PredeployJSON) > 0 {
builder.PredeployJSONFiles(o.l2PredeployJSON)
}
return builder
}

Expand Down
9 changes: 9 additions & 0 deletions playground/utils/entrypoint_v07.json

Large diffs are not rendered by default.