Skip to content
Merged
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
230 changes: 230 additions & 0 deletions devnet-sdk/proofs/prestate/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package prestate

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"
)

const (
InteropDepSetName = "depsets.json"
)

// PrestateManifest maps prestate identifiers to their hashes
type PrestateManifest map[string]string

// PrestateBuilderClient is a client for the prestate builder service
type PrestateBuilderClient struct {
url string
client *http.Client
}

// NewPrestateBuilderClient creates a new client for the prestate builder service
func NewPrestateBuilderClient(url string) *PrestateBuilderClient {
return &PrestateBuilderClient{
url: url,
client: &http.Client{},
}
}

// FileInput represents a file to be used in the build process
type FileInput struct {
Name string // Name of the file (used for identification)
Content io.Reader // Content of the file
Type string // Type information (e.g., "rollup-config", "genesis-config", "interop")
}

// buildContext holds all the inputs for a build operation
type buildContext struct {
chains []string
files []FileInput
generatedInteropDepSet bool
}

// PrestateBuilderOption is a functional option for configuring a build
type PrestateBuilderOption func(*buildContext)

// WithInteropDepSet adds an interop dependency set file to the build
func WithInteropDepSet(content io.Reader) PrestateBuilderOption {
return func(c *buildContext) {
c.files = append(c.files, FileInput{
Name: InteropDepSetName,
Content: content,
Type: "interop",
})
}
}

type dependency struct {
ChainIndex uint32 `json:"chainIndex"`
ActivationTime uint64 `json:"activationTime"`
HistoryMinTime uint64 `json:"historyMinTime"`
}

type dependencySet struct {
Dependencies map[string]dependency `json:"dependencies"`
}

func generateInteropDepSet(chains []string) ([]byte, error) {
deps := dependencySet{
Dependencies: make(map[string]dependency),
}

for i, chain := range chains {
deps.Dependencies[chain] = dependency{
ChainIndex: uint32(i),
ActivationTime: 0,
HistoryMinTime: 0,
}
}

json, err := json.Marshal(deps)
if err != nil {
return nil, err
}
return json, nil
}

func WithGeneratedInteropDepSet() PrestateBuilderOption {
return func(c *buildContext) {
c.generatedInteropDepSet = true
}
}

// WithChainConfig adds a pair of rollup and genesis config files to the build
func WithChainConfig(chainId string, rollupContent io.Reader, genesisContent io.Reader) PrestateBuilderOption {
return func(c *buildContext) {
c.chains = append(c.chains, chainId)
c.files = append(c.files,
FileInput{
Name: chainId + "-rollup.json",
Content: rollupContent,
Type: "rollup-config",
},
FileInput{
Name: chainId + "-genesis.json",
Content: genesisContent,
Type: "genesis-config",
},
)
}
}

// BuildPrestate sends the files to the prestate builder service and returns a manifest of the built prestates
func (c *PrestateBuilderClient) BuildPrestate(ctx context.Context, opts ...PrestateBuilderOption) (PrestateManifest, error) {
fmt.Println("Starting prestate build...")

// Apply options to build context
bc := &buildContext{
files: []FileInput{},
}

for _, opt := range opts {
opt(bc)
}

if bc.generatedInteropDepSet {
depSet, err := generateInteropDepSet(bc.chains)
if err != nil {
return nil, fmt.Errorf("failed to generate interop dependency set: %w", err)
}
bc.files = append(bc.files, FileInput{
Name: InteropDepSetName,
Content: bytes.NewReader(depSet),
Type: "interop",
})
}

fmt.Printf("Preparing to upload %d files\n", len(bc.files))

// Create a multipart form
var b bytes.Buffer
w := multipart.NewWriter(&b)

// Add each file to the form
for _, file := range bc.files {
fmt.Printf("Adding file to form: %s (type: %s)\n", file.Name, file.Type)
// Create a form file with the file's name
fw, err := w.CreateFormFile("files[]", filepath.Base(file.Name))
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}

// Copy the file content to the form
if _, err := io.Copy(fw, file.Content); err != nil {
return nil, fmt.Errorf("failed to copy file content: %w", err)
}
}

// Close the multipart writer
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}

fmt.Printf("Sending build request to %s\n", c.url)

// Create the HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", c.url, &b)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

// Set the content type
req.Header.Set("Content-Type", w.FormDataContentType())

// Send the request
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

// Check the response status
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotModified {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}

fmt.Println("Build request successful, fetching build manifest...")

// If the build was successful, get the info.json file
infoURL := c.url
if infoURL[len(infoURL)-1] != '/' {
infoURL += "/"
}
infoURL += "info.json"

fmt.Printf("Requesting manifest from %s\n", infoURL)

infoReq, err := http.NewRequestWithContext(ctx, "GET", infoURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create info request: %w", err)
}

infoResp, err := c.client.Do(infoReq)
if err != nil {
return nil, fmt.Errorf("failed to get info.json: %w", err)
}
defer infoResp.Body.Close()

if infoResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(infoResp.Body)
return nil, fmt.Errorf("unexpected info.json status code: %d, body: %s", infoResp.StatusCode, string(body))
}

// Parse the JSON response
var manifest PrestateManifest
if err := json.NewDecoder(infoResp.Body).Decode(&manifest); err != nil {
return nil, fmt.Errorf("failed to decode info.json response: %w", err)
}

fmt.Printf("Build complete. Generated %d prestate entries\n", len(manifest))

return manifest, nil

}
99 changes: 99 additions & 0 deletions devnet-sdk/proofs/prestate/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"strings"

"github.com/ethereum-optimism/optimism/devnet-sdk/proofs/prestate"
)

type chainConfig struct {
id string
rollupConfig string
genesisConfig string
}

func parseChainFlag(s string) (*chainConfig, error) {
parts := strings.Split(s, ",")
if len(parts) != 3 {
return nil, fmt.Errorf("chain flag must contain exactly 1 id and 2 files separated by comma")
}
return &chainConfig{
id: strings.TrimSpace(parts[0]),
rollupConfig: strings.TrimSpace(parts[1]),
genesisConfig: strings.TrimSpace(parts[2]),
}, nil
}

func main() {
var (
clientURL = flag.String("url", "http://localhost:8080", "URL of the prestate builder service")
interop = flag.Bool("interop", false, "Generate interop dependency set")
chains = make(chainConfigList, 0)
)

flag.Var(&chains, "chain", "Chain configuration files in format: rollup-config.json,genesis-config.json (can be specified multiple times)")
flag.Parse()

client := prestate.NewPrestateBuilderClient(*clientURL)
ctx := context.Background()

// Build options list
opts := make([]prestate.PrestateBuilderOption, 0)

if *interop {
opts = append(opts, prestate.WithGeneratedInteropDepSet())
}

// Add chain configs
for i, chain := range chains {
rollupFile, err := os.Open(chain.rollupConfig)
if err != nil {
log.Fatalf("Failed to open rollup config file for chain %d: %v", i, err)
}
defer rollupFile.Close()

genesisFile, err := os.Open(chain.genesisConfig)
if err != nil {
log.Fatalf("Failed to open genesis config file for chain %d: %v", i, err)
}
defer genesisFile.Close()

opts = append(opts, prestate.WithChainConfig(
chain.id,
rollupFile,
genesisFile,
))
}

// Build prestate
manifest, err := client.BuildPrestate(ctx, opts...)
if err != nil {
log.Fatalf("Failed to build prestate: %v", err)
}

// Print manifest
for id, hash := range manifest {
fmt.Printf("%s: %s\n", id, hash)
}
}

// chainConfigList implements flag.Value interface for repeated chain flags
type chainConfigList []*chainConfig

func (c *chainConfigList) String() string {
return fmt.Sprintf("%v", *c)
}

func (c *chainConfigList) Set(value string) error {
config, err := parseChainFlag(value)
if err != nil {
return err
}
*c = append(*c, config)
return nil
}