diff --git a/Makefile b/Makefile index b5a7f61d890..c528ba23786 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,10 @@ op-supernode: ## Builds op-supernode binary just $(JUSTFLAGS) ./op-supernode/op-supernode .PHONY: op-supernode +op-interop-filter: ## Builds op-interop-filter binary + just $(JUSTFLAGS) ./op-interop-filter/op-interop-filter +.PHONY: op-interop-filter + op-program: ## Builds op-program binary make -C ./op-program op-program .PHONY: op-program diff --git a/docker-bake.hcl b/docker-bake.hcl index 7a96988365f..f2b8fec9ff8 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -69,6 +69,10 @@ variable "OP_SUPERNODE_VERSION" { default = "${GIT_VERSION}" } +variable "OP_INTEROP_FILTER_VERSION" { + default = "${GIT_VERSION}" +} + variable "OP_TEST_SEQUENCER_VERSION" { default = "${GIT_VERSION}" } @@ -228,6 +232,19 @@ target "op-supernode" { tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-supernode:${tag}"] } +target "op-interop-filter" { + dockerfile = "ops/docker/op-stack-go/Dockerfile" + context = "." + args = { + GIT_COMMIT = "${GIT_COMMIT}" + GIT_DATE = "${GIT_DATE}" + OP_INTEROP_FILTER_VERSION = "${OP_INTEROP_FILTER_VERSION}" + } + target = "op-interop-filter-target" + platforms = split(",", PLATFORMS) + tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-interop-filter:${tag}"] +} + target "op-test-sequencer" { dockerfile = "ops/docker/op-stack-go/Dockerfile" context = "." diff --git a/op-interop-filter/Makefile b/op-interop-filter/Makefile new file mode 100644 index 00000000000..9d1abda16ca --- /dev/null +++ b/op-interop-filter/Makefile @@ -0,0 +1,3 @@ +DEPRECATED_TARGETS := op-interop-filter clean test + +include ../justfiles/deprecated.mk diff --git a/op-interop-filter/README.md b/op-interop-filter/README.md new file mode 100644 index 00000000000..3b3f32eee8b --- /dev/null +++ b/op-interop-filter/README.md @@ -0,0 +1,26 @@ +# op-interop-filter + +A lightweight service that validates interop executing messages for op-geth or op-reth transaction filtering. + +Any reorg will trigger the failsafe which disables all interop transactions. + +## Usage + +### Build from source + +```bash +just op-interop-filter +./bin/op-interop-filter --help +``` + +### Run from source + +```bash +go run ./cmd --help +``` + +### Build docker image + +```bash +docker buildx bake op-interop-filter +``` diff --git a/op-interop-filter/cmd/main.go b/op-interop-filter/cmd/main.go new file mode 100644 index 00000000000..d04dc4b95af --- /dev/null +++ b/op-interop-filter/cmd/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "os" + + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-service/metrics/doc" + + "github.com/ethereum-optimism/optimism/op-interop-filter/filter" + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" +) + +var ( + Version = "v0.0.0" + GitCommit = "" + GitDate = "" +) + +func main() { + oplog.SetupDefaults() + + app := cli.NewApp() + app.Flags = cliapp.ProtectFlags(flags.Flags) + app.Version = opservice.FormatVersion(Version, GitCommit, GitDate, "") + app.Name = "op-interop-filter" + app.Usage = "Interop transaction filter service" + app.Description = "Validates interop executing messages for transaction filtering" + app.Action = cliapp.LifecycleCmd(filter.Main(app.Version)) + app.Commands = []*cli.Command{ + { + Name: "doc", + Subcommands: doc.NewSubcommands(metrics.NewMetrics("default")), + }, + } + + ctx := ctxinterrupt.WithSignalWaiterMain(context.Background()) + err := app.RunContext(ctx, os.Args) + if err != nil { + log.Crit("Application failed", "message", err) + } +} diff --git a/op-interop-filter/filter/backend.go b/op-interop-filter/filter/backend.go new file mode 100644 index 00000000000..c98a197f8f6 --- /dev/null +++ b/op-interop-filter/filter/backend.go @@ -0,0 +1,56 @@ +package filter + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// Backend coordinates chain ingesters and handles the failsafe state. +// This is a stub implementation - the actual logic will be added in a follow-up PR. +type Backend struct { + log log.Logger + metrics metrics.Metricer + cfg *Config +} + +// NewBackend creates a new Backend instance +func NewBackend(ctx context.Context, logger log.Logger, m metrics.Metricer, cfg *Config) (*Backend, error) { + b := &Backend{ + log: logger, + metrics: m, + cfg: cfg, + } + logger.Info("Created backend", "chains", len(cfg.L2RPCs)) + return b, nil +} + +// Start starts the backend +func (b *Backend) Start(ctx context.Context) error { + b.log.Info("Starting backend (stub)") + return nil +} + +// Stop stops the backend +func (b *Backend) Stop(ctx context.Context) error { + b.log.Info("Stopping backend (stub)") + return nil +} + +// FailsafeEnabled returns whether failsafe is enabled +func (b *Backend) FailsafeEnabled() bool { + return false +} + +// CheckAccessList validates the given access list entries. +// This is a stub implementation that always returns ErrUninitialized. +func (b *Backend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, execDescriptor types.ExecutingDescriptor) error { + + b.metrics.RecordCheckAccessList(false) + return types.ErrUninitialized +} diff --git a/op-interop-filter/filter/config.go b/op-interop-filter/filter/config.go new file mode 100644 index 00000000000..2fee5c6283d --- /dev/null +++ b/op-interop-filter/filter/config.go @@ -0,0 +1,62 @@ +package filter + +import ( + "errors" + "fmt" + "time" + + "github.com/urfave/cli/v2" + + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" +) + +type Config struct { + L2RPCs []string + DataDir string + BackfillDuration time.Duration + JWTSecretPath string + Version string + + LogConfig oplog.CLIConfig + MetricsConfig opmetrics.CLIConfig + PprofConfig oppprof.CLIConfig + RPC oprpc.CLIConfig +} + +func (c *Config) Check() error { + var result error + if len(c.L2RPCs) == 0 { + result = errors.Join(result, errors.New("at least one L2 RPC is required")) + } + // Admin API requires JWT authentication + if c.RPC.EnableAdmin && c.JWTSecretPath == "" { + result = errors.Join(result, errors.New("admin RPC requires JWT setup, but no JWT path was specified")) + } + result = errors.Join(result, c.MetricsConfig.Check()) + result = errors.Join(result, c.PprofConfig.Check()) + result = errors.Join(result, c.RPC.Check()) + return result +} + +func NewConfig(ctx *cli.Context, version string) (*Config, error) { + backfillDuration, err := time.ParseDuration(ctx.String(flags.BackfillDurationFlag.Name)) + if err != nil { + return nil, fmt.Errorf("invalid backfill-duration: %w", err) + } + + return &Config{ + L2RPCs: ctx.StringSlice(flags.L2RPCsFlag.Name), + DataDir: ctx.String(flags.DataDirFlag.Name), + BackfillDuration: backfillDuration, + JWTSecretPath: ctx.String(flags.JWTSecretFlag.Name), + Version: version, + LogConfig: oplog.ReadCLIConfig(ctx), + MetricsConfig: opmetrics.ReadCLIConfig(ctx), + PprofConfig: oppprof.ReadCLIConfig(ctx), + RPC: oprpc.ReadCLIConfig(ctx), + }, nil +} diff --git a/op-interop-filter/filter/frontend.go b/op-interop-filter/filter/frontend.go new file mode 100644 index 00000000000..7e5a1ee6624 --- /dev/null +++ b/op-interop-filter/filter/frontend.go @@ -0,0 +1,52 @@ +package filter + +import ( + "context" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// QueryFrontend handles supervisor query RPC methods +type QueryFrontend struct { + backend *Backend +} + +// CheckAccessList validates interop executing messages +func (f *QueryFrontend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + + err := f.backend.CheckAccessList(ctx, inboxEntries, minSafety, executingDescriptor) + if err != nil { + return &rpc.JsonError{ + Code: types.GetErrorCode(err), + Message: err.Error(), + } + } + return nil +} + +// AdminFrontend handles admin RPC methods +type AdminFrontend struct { + backend *Backend +} + +// GetFailsafeEnabled returns whether failsafe is enabled +func (a *AdminFrontend) GetFailsafeEnabled(ctx context.Context) (bool, error) { + return a.backend.FailsafeEnabled(), nil +} + +// SetFailsafeEnabled enables or disables failsafe mode (TODO: implement) +func (a *AdminFrontend) SetFailsafeEnabled(ctx context.Context, enabled bool) error { + return errors.New("SetFailsafeEnabled not yet implemented") +} + +// Rewind rewinds chain state to a specific block (TODO: implement) +// This can be used to recover from reorg-induced stuck states. +func (a *AdminFrontend) Rewind(ctx context.Context, chain eth.ChainID, block eth.BlockID) error { + return errors.New("Rewind not yet implemented") +} diff --git a/op-interop-filter/filter/service.go b/op-interop-filter/filter/service.go new file mode 100644 index 00000000000..77449649b52 --- /dev/null +++ b/op-interop-filter/filter/service.go @@ -0,0 +1,242 @@ +package filter + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/httputil" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" + + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" +) + +// Service is the main op-interop-filter service +type Service struct { + log log.Logger + metrics metrics.Metricer + version string + + pprofService *oppprof.Service + metricsSrv *httputil.HTTPServer + rpcServer *oprpc.Server + + backend *Backend + + stopped atomic.Bool +} + +var _ cliapp.Lifecycle = (*Service)(nil) + +// Main returns the main entrypoint for the service +func Main(version string) cliapp.LifecycleAction { + return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) { + if err := flags.CheckRequired(cliCtx); err != nil { + return nil, err + } + + cfg, err := NewConfig(cliCtx, version) + if err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + if err := cfg.Check(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + l := oplog.NewLogger(oplog.AppOut(cliCtx), cfg.LogConfig) + oplog.SetGlobalLogHandler(l.Handler()) + opservice.ValidateEnvVars(flags.EnvVarPrefix, flags.Flags, l) + + l.Info("Initializing op-interop-filter", "version", version) + return NewService(cliCtx.Context, cfg, l) + } +} + +// NewService creates a new Service instance +func NewService(ctx context.Context, cfg *Config, logger log.Logger) (*Service, error) { + s := &Service{ + log: logger, + version: cfg.Version, + } + if err := s.init(ctx, cfg); err != nil { + return nil, errors.Join(err, s.Stop(ctx)) + } + return s, nil +} + +func (s *Service) init(ctx context.Context, cfg *Config) error { + s.initMetrics(cfg) + + if err := s.initPProf(cfg); err != nil { + return fmt.Errorf("failed to init pprof: %w", err) + } + if err := s.initMetricsServer(cfg); err != nil { + return fmt.Errorf("failed to init metrics server: %w", err) + } + if err := s.initBackend(ctx, cfg); err != nil { + return fmt.Errorf("failed to init backend: %w", err) + } + if err := s.initRPCServer(cfg); err != nil { + return fmt.Errorf("failed to init RPC server: %w", err) + } + return nil +} + +func (s *Service) initMetrics(cfg *Config) { + if cfg.MetricsConfig.Enabled { + s.metrics = metrics.NewMetrics("default") + s.metrics.RecordInfo(s.version) + } else { + s.metrics = metrics.NoopMetrics + } +} + +func (s *Service) initPProf(cfg *Config) error { + s.pprofService = oppprof.New( + cfg.PprofConfig.ListenEnabled, + cfg.PprofConfig.ListenAddr, + cfg.PprofConfig.ListenPort, + cfg.PprofConfig.ProfileType, + cfg.PprofConfig.ProfileDir, + cfg.PprofConfig.ProfileFilename, + ) + if err := s.pprofService.Start(); err != nil { + return fmt.Errorf("failed to start pprof: %w", err) + } + return nil +} + +func (s *Service) initMetricsServer(cfg *Config) error { + if !cfg.MetricsConfig.Enabled { + s.log.Info("Metrics disabled") + return nil + } + m, ok := s.metrics.(opmetrics.RegistryMetricer) + if !ok { + return fmt.Errorf("metrics do not expose registry") + } + metricsSrv, err := opmetrics.StartServer(m.Registry(), cfg.MetricsConfig.ListenAddr, cfg.MetricsConfig.ListenPort) + if err != nil { + return fmt.Errorf("failed to start metrics server: %w", err) + } + s.log.Info("Started metrics server", "addr", metricsSrv.Addr()) + s.metricsSrv = metricsSrv + return nil +} + +func (s *Service) initBackend(ctx context.Context, cfg *Config) error { + backend, err := NewBackend(ctx, s.log, s.metrics, cfg) + if err != nil { + return err + } + s.backend = backend + return nil +} + +func (s *Service) initRPCServer(cfg *Config) error { + opts := []oprpc.Option{ + oprpc.WithLogger(s.log), + } + + // Load JWT secret if path is provided (generates new secret if file is empty) + if cfg.JWTSecretPath != "" { + secret, err := oprpc.ObtainJWTSecret(s.log, cfg.JWTSecretPath, true) + if err != nil { + return fmt.Errorf("failed to obtain JWT secret: %w", err) + } + opts = append(opts, oprpc.WithJWTSecret(secret[:])) + } + + server := oprpc.NewServer( + cfg.RPC.ListenAddr, + cfg.RPC.ListenPort, + s.version, + opts..., + ) + + // Register supervisor query API + server.AddAPI(rpc.API{ + Namespace: "supervisor", + Service: &QueryFrontend{backend: s.backend}, + Authenticated: false, + }) + + // Register admin API (opt-in) + if cfg.RPC.EnableAdmin { + s.log.Info("Admin RPC enabled") + server.AddAPI(rpc.API{ + Namespace: "admin", + Service: &AdminFrontend{backend: s.backend}, + Authenticated: true, + }) + } + + s.rpcServer = server + return nil +} + +// Start starts the service +func (s *Service) Start(ctx context.Context) error { + s.log.Info("Starting op-interop-filter") + + // Start backend (begins block ingestion) + if err := s.backend.Start(ctx); err != nil { + return fmt.Errorf("failed to start backend: %w", err) + } + + // Start RPC server + if err := s.rpcServer.Start(); err != nil { + return fmt.Errorf("failed to start RPC server: %w", err) + } + s.log.Info("RPC server started", "endpoint", s.rpcServer.Endpoint()) + + s.metrics.RecordUp() + return nil +} + +// Stop stops the service +func (s *Service) Stop(ctx context.Context) error { + if !s.stopped.CompareAndSwap(false, true) { + return nil + } + s.log.Info("Stopping op-interop-filter") + + var result error + if s.rpcServer != nil { + if err := s.rpcServer.Stop(); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop RPC: %w", err)) + } + } + if s.backend != nil { + if err := s.backend.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop backend: %w", err)) + } + } + if s.pprofService != nil { + if err := s.pprofService.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop pprof: %w", err)) + } + } + if s.metricsSrv != nil { + if err := s.metricsSrv.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop metrics: %w", err)) + } + } + return result +} + +// Stopped returns true if the service has been stopped +func (s *Service) Stopped() bool { + return s.stopped.Load() +} diff --git a/op-interop-filter/flags/flags.go b/op-interop-filter/flags/flags.go new file mode 100644 index 00000000000..465aa3d031d --- /dev/null +++ b/op-interop-filter/flags/flags.go @@ -0,0 +1,79 @@ +package flags + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" +) + +const EnvVarPrefix = "OP_INTEROP_FILTER" + +func prefixEnvVars(name string) []string { + return opservice.PrefixEnvVar(EnvVarPrefix, name) +} + +var ( + L2RPCsFlag = &cli.StringSliceFlag{ + Name: "l2-rpcs", + Usage: "L2 RPC endpoints to connect to (chain ID is queried from each endpoint)", + EnvVars: prefixEnvVars("L2_RPCS"), + } + DataDirFlag = &cli.StringFlag{ + Name: "data-dir", + Usage: "Directory for LogsDB storage. If empty, uses in-memory storage", + EnvVars: prefixEnvVars("DATA_DIR"), + Value: "", + } + BackfillDurationFlag = &cli.StringFlag{ + Name: "backfill-duration", + Usage: "Duration to backfill on startup (e.g., 24h, 30m, 1h30m)", + EnvVars: prefixEnvVars("BACKFILL_DURATION"), + Value: "24h", + } + JWTSecretFlag = &cli.StringFlag{ + Name: "rpc.jwt-secret", + Usage: "Path to JWT secret key for RPC authentication. " + + "Keys are 32 bytes, hex encoded in a file. " + + "A new key will be generated if the file is empty.", + EnvVars: prefixEnvVars("RPC_JWT_SECRET"), + Value: "", + TakesFile: true, + } +) + +var requiredFlags = []cli.Flag{ + L2RPCsFlag, +} + +var optionalFlags = []cli.Flag{ + DataDirFlag, + BackfillDurationFlag, + JWTSecretFlag, +} + +func init() { + optionalFlags = append(optionalFlags, oprpc.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, oplog.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, opmetrics.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, oppprof.CLIFlags(EnvVarPrefix)...) + + Flags = append(requiredFlags, optionalFlags...) +} + +var Flags []cli.Flag + +func CheckRequired(ctx *cli.Context) error { + for _, f := range requiredFlags { + name := f.Names()[0] + if !ctx.IsSet(name) { + return fmt.Errorf("flag %s is required", name) + } + } + return nil +} diff --git a/op-interop-filter/justfile b/op-interop-filter/justfile new file mode 100644 index 00000000000..f8333843443 --- /dev/null +++ b/op-interop-filter/justfile @@ -0,0 +1,20 @@ +import '../justfiles/go.just' + +# Build ldflags string +_LDFLAGSSTRING := "'" + trim( + "-X main.GitCommit=" + GITCOMMIT + " " + \ + "-X main.GitDate=" + GITDATE + " " + \ + "-X main.Version=" + VERSION + " " + \ + "") + "'" + +BINARY := "./bin/op-interop-filter" + +# Build op-interop-filter binary +op-interop-filter: (go_build BINARY "./cmd" "-ldflags" _LDFLAGSSTRING) + +# Clean build artifacts +clean: + rm -f {{BINARY}} + +# Run tests +test: (go_test "./...") diff --git a/op-interop-filter/metrics/metrics.go b/op-interop-filter/metrics/metrics.go new file mode 100644 index 00000000000..c46f28cac59 --- /dev/null +++ b/op-interop-filter/metrics/metrics.go @@ -0,0 +1,127 @@ +package metrics + +import ( + "strconv" + + "github.com/prometheus/client_golang/prometheus" + + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" +) + +const Namespace = "op_interop_filter" + +type Metricer interface { + RecordInfo(version string) + RecordUp() + RecordFailsafeEnabled(enabled bool) + RecordChainHead(chainID uint64, blockNum uint64) + RecordCheckAccessList(success bool) +} + +type Metrics struct { + ns string + registry *prometheus.Registry + factory opmetrics.Factory + + info *prometheus.GaugeVec + up prometheus.Gauge + failsafeEnabled prometheus.Gauge + chainHead *prometheus.GaugeVec + checkAccessTotal *prometheus.CounterVec +} + +var _ Metricer = (*Metrics)(nil) +var _ opmetrics.RegistryMetricer = (*Metrics)(nil) + +func NewMetrics(procName string) *Metrics { + if procName == "" { + procName = "default" + } + ns := Namespace + "_" + procName + + registry := opmetrics.NewRegistry() + factory := opmetrics.With(registry) + + return &Metrics{ + ns: ns, + registry: registry, + factory: factory, + + info: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Name: "info", + Help: "Service info", + }, []string{"version"}), + + up: factory.NewGauge(prometheus.GaugeOpts{ + Namespace: ns, + Name: "up", + Help: "1 if service is up", + }), + + failsafeEnabled: factory.NewGauge(prometheus.GaugeOpts{ + Namespace: ns, + Name: "failsafe_enabled", + Help: "1 if failsafe is enabled", + }), + + chainHead: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Name: "chain_head", + Help: "Latest ingested block number", + }, []string{"chain_id"}), + + checkAccessTotal: factory.NewCounterVec(prometheus.CounterOpts{ + Namespace: ns, + Name: "check_access_list_total", + Help: "Total checkAccessList requests", + }, []string{"success"}), + } +} + +func (m *Metrics) Registry() *prometheus.Registry { + return m.registry +} + +func (m *Metrics) Document() []opmetrics.DocumentedMetric { + return m.factory.Document() +} + +func (m *Metrics) RecordInfo(version string) { + m.info.WithLabelValues(version).Set(1) +} + +func (m *Metrics) RecordUp() { + m.up.Set(1) +} + +func (m *Metrics) RecordFailsafeEnabled(enabled bool) { + if enabled { + m.failsafeEnabled.Set(1) + } else { + m.failsafeEnabled.Set(0) + } +} + +func (m *Metrics) RecordChainHead(chainID uint64, blockNum uint64) { + m.chainHead.WithLabelValues(strconv.FormatUint(chainID, 10)).Set(float64(blockNum)) +} + +func (m *Metrics) RecordCheckAccessList(success bool) { + label := "false" + if success { + label = "true" + } + m.checkAccessTotal.WithLabelValues(label).Inc() +} + +// NoopMetrics is a no-op implementation for testing +var NoopMetrics Metricer = &noopMetrics{} + +type noopMetrics struct{} + +func (n *noopMetrics) RecordInfo(version string) {} +func (n *noopMetrics) RecordUp() {} +func (n *noopMetrics) RecordFailsafeEnabled(enabled bool) {} +func (n *noopMetrics) RecordChainHead(chainID uint64, blockNum uint64) {} +func (n *noopMetrics) RecordCheckAccessList(success bool) {} diff --git a/ops/docker/op-stack-go/Dockerfile b/ops/docker/op-stack-go/Dockerfile index 57052e7101b..f07beb6b4d8 100644 --- a/ops/docker/op-stack-go/Dockerfile +++ b/ops/docker/op-stack-go/Dockerfile @@ -170,6 +170,11 @@ ARG OP_SUPERNODE_VERSION=v0.0.0 RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-supernode && make op-supernode \ GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$OP_SUPERNODE_VERSION" +FROM --platform=$BUILDPLATFORM builder AS op-interop-filter-builder +ARG OP_INTEROP_FILTER_VERSION=v0.0.0 +RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-interop-filter && make op-interop-filter \ + GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$OP_INTEROP_FILTER_VERSION" + FROM --platform=$BUILDPLATFORM builder AS op-test-sequencer-builder ARG OP_TEST_SEQUENCER_VERSION=v0.0.0 RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-test-sequencer && make op-test-sequencer \ @@ -262,6 +267,10 @@ FROM $TARGET_BASE_IMAGE AS op-supernode-target COPY --from=op-supernode-builder /app/op-supernode/bin/op-supernode /usr/local/bin/ CMD ["op-supernode"] +FROM $TARGET_BASE_IMAGE AS op-interop-filter-target +COPY --from=op-interop-filter-builder /app/op-interop-filter/bin/op-interop-filter /usr/local/bin/ +CMD ["op-interop-filter"] + FROM $TARGET_BASE_IMAGE AS op-test-sequencer-target COPY --from=op-test-sequencer-builder /app/op-test-sequencer/bin/op-test-sequencer /usr/local/bin/ CMD ["op-test-sequencer"] diff --git a/ops/docker/op-stack-go/Dockerfile.dockerignore b/ops/docker/op-stack-go/Dockerfile.dockerignore index 280b1603456..db630bdb08b 100644 --- a/ops/docker/op-stack-go/Dockerfile.dockerignore +++ b/ops/docker/op-stack-go/Dockerfile.dockerignore @@ -20,6 +20,7 @@ !/op-service !/op-supervisor !/op-supernode +!/op-interop-filter !/op-test-sequencer !/op-wheel !/op-alt-da