run.go 10.3 KB
Newer Older
protolambda's avatar
protolambda committed
1 2 3
package cmd

import (
4
	"context"
protolambda's avatar
protolambda committed
5 6
	"fmt"
	"os"
7 8
	"os/exec"
	"time"
protolambda's avatar
protolambda committed
9 10 11 12 13 14

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/log"
	"github.com/urfave/cli/v2"

15 16
	"github.com/pkg/profile"

17
	"github.com/ethereum-optimism/optimism/cannon/mipsevm"
18
	preimage "github.com/ethereum-optimism/optimism/op-preimage"
protolambda's avatar
protolambda committed
19 20 21 22 23 24 25 26 27 28 29 30
)

var (
	RunInputFlag = &cli.PathFlag{
		Name:      "input",
		Usage:     "path of input JSON state. Stdin if left empty.",
		TakesFile: true,
		Value:     "state.json",
		Required:  true,
	}
	RunOutputFlag = &cli.PathFlag{
		Name:      "output",
31
		Usage:     "path of output JSON state. Not written if empty, use - to write to Stdout.",
protolambda's avatar
protolambda committed
32 33 34 35 36 37 38 39 40 41 42 43 44
		TakesFile: true,
		Value:     "out.json",
		Required:  false,
	}
	patternHelp    = "'never' (default), 'always', '=123' at exactly step 123, '%123' for every 123 steps"
	RunProofAtFlag = &cli.GenericFlag{
		Name:     "proof-at",
		Usage:    "step pattern to output proof at: " + patternHelp,
		Value:    new(StepMatcherFlag),
		Required: false,
	}
	RunProofFmtFlag = &cli.StringFlag{
		Name:     "proof-fmt",
45
		Usage:    "format for proof data output file names. Proof data is written to stdout if -.",
protolambda's avatar
protolambda committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
		Value:    "proof-%d.json",
		Required: false,
	}
	RunSnapshotAtFlag = &cli.GenericFlag{
		Name:     "snapshot-at",
		Usage:    "step pattern to output snapshots at: " + patternHelp,
		Value:    new(StepMatcherFlag),
		Required: false,
	}
	RunSnapshotFmtFlag = &cli.StringFlag{
		Name:     "snapshot-fmt",
		Usage:    "format for snapshot output file names.",
		Value:    "state-%d.json",
		Required: false,
	}
	RunStopAtFlag = &cli.GenericFlag{
		Name:     "stop-at",
		Usage:    "step pattern to stop at: " + patternHelp,
		Value:    new(StepMatcherFlag),
		Required: false,
	}
67 68
	RunMetaFlag = &cli.PathFlag{
		Name:     "meta",
69
		Usage:    "path to metadata file for symbol lookup for enhanced debugging info during execution.",
70 71 72
		Value:    "meta.json",
		Required: false,
	}
73 74 75
	RunInfoAtFlag = &cli.GenericFlag{
		Name:     "info-at",
		Usage:    "step pattern to print info at: " + patternHelp,
76
		Value:    MustStepMatcherFlag("%100000"),
77 78
		Required: false,
	}
protolambda's avatar
protolambda committed
79 80 81 82
	RunPProfCPU = &cli.BoolFlag{
		Name:  "pprof.cpu",
		Usage: "enable pprof cpu profiling",
	}
protolambda's avatar
protolambda committed
83 84 85 86 87 88 89 90
)

type Proof struct {
	Step uint64 `json:"step"`

	Pre  common.Hash `json:"pre"`
	Post common.Hash `json:"post"`

91 92 93
	StateData hexutil.Bytes `json:"state-data"`
	ProofData hexutil.Bytes `json:"proof-data"`

94 95 96
	OracleKey    hexutil.Bytes `json:"oracle-key,omitempty"`
	OracleValue  hexutil.Bytes `json:"oracle-value,omitempty"`
	OracleOffset uint32        `json:"oracle-offset,omitempty"`
protolambda's avatar
protolambda committed
97 98
}

99 100 101 102 103 104 105 106 107 108 109 110 111
type rawHint string

func (rh rawHint) Hint() string {
	return string(rh)
}

type rawKey [32]byte

func (rk rawKey) PreimageKey() [32]byte {
	return rk
}

type ProcessPreimageOracle struct {
112 113 114 115 116
	pCl      *preimage.OracleClient
	hCl      *preimage.HintWriter
	cmd      *exec.Cmd
	waitErr  chan error
	cancelIO context.CancelCauseFunc
117 118
}

119 120
const clientPollTimeout = time.Second * 15

121
func NewProcessPreimageOracle(name string, args []string) (*ProcessPreimageOracle, error) {
122
	if name == "" {
123
		return &ProcessPreimageOracle{}, nil
124 125
	}

inphi's avatar
inphi committed
126 127
	pClientRW, pOracleRW, err := preimage.CreateBidirectionalChannel()
	if err != nil {
128
		return nil, err
inphi's avatar
inphi committed
129 130 131
	}
	hClientRW, hOracleRW, err := preimage.CreateBidirectionalChannel()
	if err != nil {
132
		return nil, err
inphi's avatar
inphi committed
133
	}
134

protolambda's avatar
protolambda committed
135
	cmd := exec.Command(name, args...) // nosemgrep
136 137 138
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.ExtraFiles = []*os.File{
inphi's avatar
inphi committed
139 140 141 142
		hOracleRW.Reader(),
		hOracleRW.Writer(),
		pOracleRW.Reader(),
		pOracleRW.Writer(),
143
	}
144 145 146 147 148 149

	// Note that the client file descriptors are not closed when the pre-image server exits.
	// So we use the FilePoller to ensure that we don't get stuck in a blocking read/write.
	ctx, cancelIO := context.WithCancelCause(context.Background())
	preimageClientIO := preimage.NewFilePoller(ctx, pClientRW, clientPollTimeout)
	hostClientIO := preimage.NewFilePoller(ctx, hClientRW, clientPollTimeout)
150
	out := &ProcessPreimageOracle{
151 152 153 154 155
		pCl:      preimage.NewOracleClient(preimageClientIO),
		hCl:      preimage.NewHintWriter(hostClientIO),
		cmd:      cmd,
		waitErr:  make(chan error),
		cancelIO: cancelIO,
156
	}
157
	return out, nil
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
}

func (p *ProcessPreimageOracle) Hint(v []byte) {
	if p.hCl == nil { // no hint processor
		return
	}
	p.hCl.Hint(rawHint(v))
}

func (p *ProcessPreimageOracle) GetPreimage(k [32]byte) []byte {
	if p.pCl == nil {
		panic("no pre-image retriever available")
	}
	return p.pCl.Get(rawKey(k))
}

func (p *ProcessPreimageOracle) Start() error {
	if p.cmd == nil {
		return nil
	}
178 179 180
	err := p.cmd.Start()
	go p.wait()
	return err
181 182 183 184 185 186
}

func (p *ProcessPreimageOracle) Close() error {
	if p.cmd == nil {
		return nil
	}
inphi's avatar
inphi committed
187 188
	// Give the pre-image server time to exit cleanly before killing it.
	time.Sleep(time.Second * 1)
189
	_ = p.cmd.Process.Signal(os.Interrupt)
190 191 192 193
	return <-p.waitErr
}

func (p *ProcessPreimageOracle) wait() {
194
	err := p.cmd.Wait()
195 196 197
	var waitErr error
	if err, ok := err.(*exec.ExitError); !ok || !err.Success() {
		waitErr = err
198
	}
199 200 201
	p.cancelIO(fmt.Errorf("%w: pre-image server has exited", waitErr))
	p.waitErr <- waitErr
	close(p.waitErr)
202 203 204 205 206 207 208 209 210
}

type StepFn func(proof bool) (*mipsevm.StepWitness, error)

func Guard(proc *os.ProcessState, fn StepFn) StepFn {
	return func(proof bool) (*mipsevm.StepWitness, error) {
		wit, err := fn(proof)
		if err != nil {
			if proc.Exited() {
211
				return nil, fmt.Errorf("pre-image server exited with code %d, resulting in err %w", proc.ExitCode(), err)
212 213 214 215 216 217 218 219 220 221
			} else {
				return nil, err
			}
		}
		return wit, nil
	}
}

var _ mipsevm.PreimageOracle = (*ProcessPreimageOracle)(nil)

protolambda's avatar
protolambda committed
222
func Run(ctx *cli.Context) error {
protolambda's avatar
protolambda committed
223 224 225
	if ctx.Bool(RunPProfCPU.Name) {
		defer profile.Start(profile.NoShutdownHook, profile.ProfilePath("."), profile.CPUProfile).Stop()
	}
226

protolambda's avatar
protolambda committed
227 228 229 230
	state, err := loadJSON[mipsevm.State](ctx.Path(RunInputFlag.Name))
	if err != nil {
		return err
	}
231

protolambda's avatar
protolambda committed
232 233 234 235
	l := Logger(os.Stderr, log.LvlInfo)
	outLog := &mipsevm.LoggingWriter{Name: "program std-out", Log: l}
	errLog := &mipsevm.LoggingWriter{Name: "program std-err", Log: l}

236 237 238 239
	// split CLI args after first '--'
	args := ctx.Args().Slice()
	for i, arg := range args {
		if arg == "--" {
240
			args = args[i+1:]
241 242 243
			break
		}
	}
protolambda's avatar
protolambda committed
244 245 246
	if len(args) == 0 {
		args = []string{""}
	}
247

248 249 250 251
	po, err := NewProcessPreimageOracle(args[0], args[1:])
	if err != nil {
		return fmt.Errorf("failed to create pre-image oracle process: %w", err)
	}
252 253 254 255 256 257 258 259
	if err := po.Start(); err != nil {
		return fmt.Errorf("failed to start pre-image oracle server: %w", err)
	}
	defer func() {
		if err := po.Close(); err != nil {
			l.Error("failed to close pre-image server", "err", err)
		}
	}()
protolambda's avatar
protolambda committed
260 261 262 263

	stopAt := ctx.Generic(RunStopAtFlag.Name).(*StepMatcherFlag).Matcher()
	proofAt := ctx.Generic(RunProofAtFlag.Name).(*StepMatcherFlag).Matcher()
	snapshotAt := ctx.Generic(RunSnapshotAtFlag.Name).(*StepMatcherFlag).Matcher()
264
	infoAt := ctx.Generic(RunInfoAtFlag.Name).(*StepMatcherFlag).Matcher()
protolambda's avatar
protolambda committed
265

266 267 268 269 270 271 272 273 274 275 276 277
	var meta *mipsevm.Metadata
	if metaPath := ctx.Path(RunMetaFlag.Name); metaPath == "" {
		l.Info("no metadata file specified, defaulting to empty metadata")
		meta = &mipsevm.Metadata{Symbols: nil} // provide empty metadata by default
	} else {
		if m, err := loadJSON[mipsevm.Metadata](metaPath); err != nil {
			return fmt.Errorf("failed to load metadata: %w", err)
		} else {
			meta = m
		}
	}

278
	us := mipsevm.NewInstrumentedState(state, po, outLog, errLog)
protolambda's avatar
protolambda committed
279 280 281
	proofFmt := ctx.String(RunProofFmtFlag.Name)
	snapshotFmt := ctx.String(RunSnapshotFmtFlag.Name)

282
	stepFn := us.Step
283
	if po.cmd != nil {
284
		stepFn = Guard(po.cmd.ProcessState, stepFn)
285 286
	}

287 288 289
	start := time.Now()
	startStep := state.Step

290 291 292
	// avoid symbol lookups every instruction by preparing a matcher func
	sleepCheck := meta.SymbolMatcher("runtime.notesleep")

protolambda's avatar
protolambda committed
293
	for !state.Exited {
294 295 296 297
		if state.Step%100 == 0 { // don't do the ctx err check (includes lock) too often
			if err := ctx.Context.Err(); err != nil {
				return err
			}
298 299
		}

protolambda's avatar
protolambda committed
300 301
		step := state.Step

302
		if infoAt(state) {
303
			delta := time.Since(start)
304 305 306 307
			l.Info("processing",
				"step", step,
				"pc", mipsevm.HexU32(state.PC),
				"insn", mipsevm.HexU32(state.Memory.GetMemory(state.PC)),
308
				"ips", float64(step-startStep)/(float64(delta)/float64(time.Second)),
309
				"pages", state.Memory.PageCount(),
310
				"mem", state.Memory.Usage(),
311
				"name", meta.LookupSymbol(state.PC),
312 313
			)
		}
314 315

		if sleepCheck(state.PC) { // don't loop forever when we get stuck because of an unexpected bad program
316 317
			return fmt.Errorf("got stuck in Go sleep at step %d", step)
		}
318

protolambda's avatar
protolambda committed
319 320 321 322 323
		if stopAt(state) {
			break
		}

		if snapshotAt(state) {
324
			if err := writeJSON(fmt.Sprintf(snapshotFmt, step), state); err != nil {
protolambda's avatar
protolambda committed
325 326 327 328 329
				return fmt.Errorf("failed to write state snapshot: %w", err)
			}
		}

		if proofAt(state) {
330 331 332 333
			preStateHash, err := state.EncodeWitness().StateHash()
			if err != nil {
				return fmt.Errorf("failed to hash prestate witness: %w", err)
			}
334
			witness, err := stepFn(true)
335 336 337
			if err != nil {
				return fmt.Errorf("failed at proof-gen step %d (PC: %08x): %w", step, state.PC, err)
			}
338 339 340 341
			postStateHash, err := state.EncodeWitness().StateHash()
			if err != nil {
				return fmt.Errorf("failed to hash poststate witness: %w", err)
			}
protolambda's avatar
protolambda committed
342 343 344 345
			proof := &Proof{
				Step:      step,
				Pre:       preStateHash,
				Post:      postStateHash,
346 347
				StateData: witness.State,
				ProofData: witness.MemProof,
protolambda's avatar
protolambda committed
348 349
			}
			if witness.HasPreimage() {
350 351
				proof.OracleKey = witness.PreimageKey[:]
				proof.OracleValue = witness.PreimageValue
352
				proof.OracleOffset = witness.PreimageOffset
protolambda's avatar
protolambda committed
353
			}
354
			if err := writeJSON(fmt.Sprintf(proofFmt, step), proof); err != nil {
protolambda's avatar
protolambda committed
355 356 357
				return fmt.Errorf("failed to write proof data: %w", err)
			}
		} else {
358
			_, err = stepFn(false)
359 360 361
			if err != nil {
				return fmt.Errorf("failed at step %d (PC: %08x): %w", step, state.PC, err)
			}
protolambda's avatar
protolambda committed
362 363 364
		}
	}

365
	if err := writeJSON(ctx.Path(RunOutputFlag.Name), state); err != nil {
protolambda's avatar
protolambda committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
		return fmt.Errorf("failed to write state output: %w", err)
	}
	return nil
}

var RunCommand = &cli.Command{
	Name:        "run",
	Usage:       "Run VM step(s) and generate proof data to replicate onchain.",
	Description: "Run VM step(s) and generate proof data to replicate onchain. See flags to match when to output a proof, a snapshot, or to stop early.",
	Action:      Run,
	Flags: []cli.Flag{
		RunInputFlag,
		RunOutputFlag,
		RunProofAtFlag,
		RunProofFmtFlag,
		RunSnapshotAtFlag,
		RunSnapshotFmtFlag,
		RunStopAtFlag,
384
		RunMetaFlag,
385
		RunInfoAtFlag,
protolambda's avatar
protolambda committed
386
		RunPProfCPU,
protolambda's avatar
protolambda committed
387 388
	},
}