Commit 0d9e6015 authored by Adrian Sutton's avatar Adrian Sutton

Merge branch 'aj/create-cannon' of github.com:ethereum-optimism/optimism into aj/create-cannon

parents 22b0c43b 40bf36a5
......@@ -1854,6 +1854,16 @@ workflows:
context:
- oplabs-gcr
- slack
- docker-build:
name: contracts-bedrock-docker-publish
docker_name: contracts-bedrock
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
resource_class: xlarge
requires: [ 'chain-mon-docker-publish' ] # use the cached base image
publish: true
context:
- oplabs-gcr
- slack
- docker-build:
name: ufm-metamask-docker-publish
docker_name: ufm-metamask
......
......@@ -39,6 +39,15 @@ golang-docker:
op-node op-batcher op-proposer op-challenger
.PHONY: golang-docker
contracts-bedrock-docker:
IMAGE_TAGS=$$(git rev-parse HEAD),latest \
docker buildx bake \
--progress plain \
--load \
-f docker-bake.hcl \
contracts-bedrock
.PHONY: contracts-bedrock-docker
submodules:
git submodule update --init --recursive
.PHONY: submodules
......
......@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"os"
"path"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
)
......@@ -35,18 +34,16 @@ func writeJSON[X any](outputPath string, value X) error {
var out io.Writer
finish := func() error { return nil }
if outputPath != "-" {
// Write to a tmp file but reserve the file extension if present
tmpPath := outputPath + "-tmp" + path.Ext(outputPath)
f, err := ioutil.OpenCompressed(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
f, err := ioutil.NewAtomicWriterCompressed(outputPath, 0755)
if err != nil {
return fmt.Errorf("failed to open output file: %w", err)
}
// Ensure we close the stream even if failures occur.
defer f.Close()
out = f
finish = func() error {
// Rename the file into place as atomically as the OS will allow
return os.Rename(tmpPath, outputPath)
}
// Closing the file causes it to be renamed to the final destination
// so make sure we handle any errors it returns
finish = f.Close
} else {
out = os.Stdout
}
......
......@@ -180,4 +180,10 @@ target "ci-builder" {
tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/ci-builder:${tag}"]
}
target "contracts-bedrock" {
dockerfile = "./ops/docker/Dockerfile.packages"
context = "."
target = "contracts-bedrock"
platforms = split(",", PLATFORMS)
tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/contracts-bedrock:${tag}"]
}
......@@ -103,7 +103,7 @@ func (c *coordinator) createJob(game types.GameMetadata) (*job, error) {
c.states[game.Proxy] = state
}
if state.inflight {
c.logger.Debug("Not rescheduling already in-flight game", "game", game)
c.logger.Debug("Not rescheduling already in-flight game", "game", game.Proxy)
return nil, nil
}
// Create the player separately to the state so we retry creating it if it fails on the first attempt.
......@@ -117,7 +117,7 @@ func (c *coordinator) createJob(game types.GameMetadata) (*job, error) {
}
state.inflight = true
if state.status != types.GameStatusInProgress {
c.logger.Debug("Not rescheduling resolved game", "game", game, "status", state.status)
c.logger.Debug("Not rescheduling resolved game", "game", game.Proxy, "status", state.status)
return nil, nil
}
return &job{addr: game.Proxy, player: state.player, status: state.status}, nil
......
......@@ -56,18 +56,22 @@ func (f fakeTxMgr) Send(_ context.Context, _ txmgr.TxCandidate) (*types.Receipt,
}
func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Client, rollupCl *sources.RollupClient) *L2Proposer {
proposerCfg := proposer.Config{
L2OutputOracleAddr: cfg.OutputOracleAddr,
proposerConfig := proposer.ProposerConfig{
PollInterval: time.Second,
NetworkTimeout: time.Second,
L1Client: l1,
RollupClient: rollupCl,
L2OutputOracleAddr: cfg.OutputOracleAddr,
AllowNonFinalized: cfg.AllowNonFinalized,
// We use custom signing here instead of using the transaction manager.
TxManager: fakeTxMgr{from: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey)},
}
driverSetup := proposer.DriverSetup{
Log: log,
Metr: metrics.NoopMetrics,
Cfg: proposerConfig,
Txmgr: fakeTxMgr{from: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey)},
L1Client: l1,
RollupClient: rollupCl,
}
dr, err := proposer.NewL2OutputSubmitter(proposerCfg, log, metrics.NoopMetrics)
dr, err := proposer.NewL2OutputSubmitter(driverSetup)
require.NoError(t, err)
contract, err := bindings.NewL2OutputOracleCaller(cfg.OutputOracleAddr, l1)
require.NoError(t, err)
......
......@@ -531,8 +531,8 @@ func setupDisputeGameForInvalidOutputRoot(t *testing.T, outputRoot common.Hash)
// Wait for one valid output root to be submitted
l2oo.WaitForProposals(ctx, 1)
// Stop the honest output submitter so we can publish invalid outputs
sys.L2OutputSubmitter.Stop()
err := sys.L2OutputSubmitter.Driver().StopL2OutputSubmitting()
require.NoError(t, err)
sys.L2OutputSubmitter = nil
// Submit an invalid output root
......
......@@ -52,7 +52,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
proposermetrics "github.com/ethereum-optimism/optimism/op-proposer/metrics"
l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/clock"
......@@ -260,7 +259,7 @@ type System struct {
Clients map[string]*ethclient.Client
RawClients map[string]*rpc.Client
RollupNodes map[string]*rollupNode.OpNode
L2OutputSubmitter *l2os.L2OutputSubmitter
L2OutputSubmitter *l2os.ProposerService
BatchSubmitter *bss.BatcherService
Mocknet mocknet.Mocknet
......@@ -283,7 +282,7 @@ func (sys *System) Close() {
postCancel() // immediate shutdown, no allowance for idling
if sys.L2OutputSubmitter != nil {
sys.L2OutputSubmitter.Stop()
_ = sys.L2OutputSubmitter.Kill()
}
if sys.BatchSubmitter != nil {
_ = sys.BatchSubmitter.Kill()
......@@ -679,7 +678,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
}
// L2Output Submitter
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitterFromCLIConfig(l2os.CLIConfig{
proposerCLIConfig := &l2os.CLIConfig{
L1EthRpc: sys.EthInstances["l1"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
L2OOAddress: config.L1Deployments.L2OutputOracleProxy.Hex(),
......@@ -690,14 +689,15 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
Level: log.LvlInfo,
Format: oplog.FormatText,
},
}, sys.cfg.Loggers["proposer"], proposermetrics.NoopMetrics)
}
proposer, err := l2os.ProposerServiceFromCLIConfig(context.Background(), "0.0.1", proposerCLIConfig, sys.cfg.Loggers["proposer"])
if err != nil {
return nil, fmt.Errorf("unable to setup l2 output submitter: %w", err)
}
if err := sys.L2OutputSubmitter.Start(); err != nil {
if err := proposer.Start(context.Background()); err != nil {
return nil, fmt.Errorf("unable to start l2 output submitter: %w", err)
}
sys.L2OutputSubmitter = proposer
var batchType uint = derive.SingularBatchType
if cfg.DeployConfig.L2GenesisSpanBatchTimeOffset != nil && *cfg.DeployConfig.L2GenesisSpanBatchTimeOffset == hexutil.Uint64(0) {
......
......@@ -290,7 +290,8 @@ func testFaultProofProgramScenario(t *testing.T, ctx context.Context, sys *Syste
t.Log("Shutting down network")
// Shutdown the nodes from the actual chain. Should now be able to run using only the pre-fetched data.
require.NoError(t, sys.BatchSubmitter.Kill())
sys.L2OutputSubmitter.Stop()
err = sys.L2OutputSubmitter.Driver().StopL2OutputSubmitting()
require.NoError(t, err)
sys.L2OutputSubmitter = nil
for _, node := range sys.EthInstances {
node.Close()
......
......@@ -30,7 +30,7 @@ func main() {
app.Name = "op-proposer"
app.Usage = "L2Output Submitter"
app.Description = "Service for generating and submitting L2 Output checkpoints to the L2OutputOracle contract"
app.Action = curryMain(Version)
app.Action = cliapp.LifecycleCmd(proposer.Main(Version))
app.Commands = []*cli.Command{
{
Name: "doc",
......@@ -43,11 +43,3 @@ func main() {
log.Crit("Application failed", "message", err)
}
}
// curryMain transforms the proposer.Main function into an app.Action
// This is done to capture the Version of the proposer.
func curryMain(version string) func(ctx *cli.Context) error {
return func(ctx *cli.Context) error {
return proposer.Main(version, ctx)
}
}
package metrics
import (
"context"
"io"
"github.com/prometheus/client_golang/prometheus"
......@@ -10,13 +10,15 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/httputil"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
txmetrics "github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
)
const Namespace = "op_proposer"
// implements the Registry getter, for metrics HTTP server to hook into
var _ opmetrics.RegistryMetricer = (*Metrics)(nil)
type Metricer interface {
RecordInfo(version string)
RecordUp()
......@@ -27,6 +29,10 @@ type Metricer interface {
// Record Tx metrics
txmetrics.TxMetricer
opmetrics.RPCMetricer
StartBalanceMetrics(l log.Logger, client *ethclient.Client, account common.Address) io.Closer
RecordL2BlocksProposed(l2ref eth.L2BlockRef)
}
......@@ -78,18 +84,12 @@ func NewMetrics(procName string) *Metrics {
}
}
func (m *Metrics) Start(host string, port int) (*httputil.HTTPServer, error) {
return opmetrics.StartServer(m.registry, host, port)
func (m *Metrics) Registry() *prometheus.Registry {
return m.registry
}
func (m *Metrics) StartBalanceMetrics(ctx context.Context,
l log.Logger, client *ethclient.Client, account common.Address) {
// TODO(7684): util was refactored to close, but ctx is still being used by caller for shutdown
balanceMetric := opmetrics.LaunchBalanceMetrics(l, m.registry, m.ns, client, account)
go func() {
<-ctx.Done()
_ = balanceMetric.Close()
}()
func (m *Metrics) StartBalanceMetrics(l log.Logger, client *ethclient.Client, account common.Address) io.Closer {
return opmetrics.LaunchBalanceMetrics(l, m.registry, m.ns, client, account)
}
// RecordInfo sets a pseudo-metric that contains versioning and
......
package metrics
import (
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-service/eth"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
txmetrics "github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
......@@ -9,6 +15,7 @@ import (
type noopMetrics struct {
opmetrics.NoopRefMetrics
txmetrics.NoopTxMetrics
opmetrics.NoopRPCMetrics
}
var NoopMetrics Metricer = new(noopMetrics)
......@@ -17,3 +24,7 @@ func (*noopMetrics) RecordInfo(version string) {}
func (*noopMetrics) RecordUp() {}
func (*noopMetrics) RecordL2BlocksProposed(l2ref eth.L2BlockRef) {}
func (*noopMetrics) StartBalanceMetrics(log.Logger, *ethclient.Client, common.Address) io.Closer {
return nil
}
......@@ -3,13 +3,9 @@ package proposer
import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/op-proposer/flags"
"github.com/ethereum-optimism/optimism/op-service/sources"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
......@@ -17,18 +13,6 @@ import (
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
// Config contains the well typed fields that are used to initialize the output submitter.
// It is intended for programmatic use.
type Config struct {
L2OutputOracleAddr common.Address
PollInterval time.Duration
NetworkTimeout time.Duration
TxManager txmgr.TxManager
L1Client *ethclient.Client
RollupClient *sources.RollupClient
AllowNonFinalized bool
}
// CLIConfig is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services.
// It is transformed into a `Config` before the L2 output submitter is started.
......@@ -63,7 +47,7 @@ type CLIConfig struct {
PprofConfig oppprof.CLIConfig
}
func (c CLIConfig) Check() error {
func (c *CLIConfig) Check() error {
if err := c.RPCConfig.Check(); err != nil {
return err
}
......@@ -80,8 +64,8 @@ func (c CLIConfig) Check() error {
}
// NewConfig parses the Config from the provided flags or environment variables.
func NewConfig(ctx *cli.Context) CLIConfig {
return CLIConfig{
func NewConfig(ctx *cli.Context) *CLIConfig {
return &CLIConfig{
// Required Flags
L1EthRpc: ctx.String(flags.L1EthRpcFlag.Name),
RollupRpc: ctx.String(flags.RollupRpcFlag.Name),
......
This diff is collapsed.
This diff is collapsed.
package rpc
import (
"context"
"github.com/ethereum/go-ethereum/log"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/rpc"
)
type ProposerDriver interface {
StartL2OutputSubmitting() error
StopL2OutputSubmitting() error
}
type adminAPI struct {
*rpc.CommonAdminAPI
b ProposerDriver
}
func NewAdminAPI(dr ProposerDriver, m metrics.RPCMetricer, log log.Logger) *adminAPI {
return &adminAPI{
CommonAdminAPI: rpc.NewCommonAdminAPI(m, log),
b: dr,
}
}
func GetAdminAPI(api *adminAPI) gethrpc.API {
return gethrpc.API{
Namespace: "admin",
Service: api,
}
}
func (a *adminAPI) StartProposer(_ context.Context) error {
return a.b.StartL2OutputSubmitting()
}
func (a *adminAPI) StopProposer(ctx context.Context) error {
return a.b.StopL2OutputSubmitting()
}
package proposer
import (
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"sync/atomic"
"time"
"github.com/ethereum-optimism/optimism/op-proposer/metrics"
"github.com/ethereum-optimism/optimism/op-proposer/proposer/rpc"
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/dial"
"github.com/ethereum-optimism/optimism/op-service/httputil"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
type ProposerConfig struct {
// How frequently to poll L2 for new finalized outputs
PollInterval time.Duration
NetworkTimeout time.Duration
L2OutputOracleAddr common.Address
// AllowNonFinalized enables the proposal of safe, but non-finalized L2 blocks.
// The L1 block-hash embedded in the proposal TX is checked and should ensure the proposal
// is never valid on an alternative L1 chain that would produce different L2 data.
// This option is not necessary when higher proposal latency is acceptable and L1 is healthy.
AllowNonFinalized bool
}
type ProposerService struct {
Log log.Logger
Metrics metrics.Metricer
ProposerConfig
TxManager txmgr.TxManager
L1Client *ethclient.Client
RollupClient *sources.RollupClient
driver *L2OutputSubmitter
Version string
pprofSrv *httputil.HTTPServer
metricsSrv *httputil.HTTPServer
rpcServer *oprpc.Server
balanceMetricer io.Closer
stopped atomic.Bool
}
// ProposerServiceFromCLIConfig creates a new ProposerService from a CLIConfig.
// The service components are fully started, except for the driver,
// which will not be submitting state (if it was configured to) until the Start part of the lifecycle.
func ProposerServiceFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger) (*ProposerService, error) {
var ps ProposerService
if err := ps.initFromCLIConfig(ctx, version, cfg, log); err != nil {
return nil, errors.Join(err, ps.Stop(ctx)) // try to clean up our failed initialization attempt
}
return &ps, nil
}
func (ps *ProposerService) initFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger) error {
ps.Version = version
ps.Log = log
ps.initMetrics(cfg)
ps.PollInterval = cfg.PollInterval
ps.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout
ps.AllowNonFinalized = cfg.AllowNonFinalized
if err := ps.initRPCClients(ctx, cfg); err != nil {
return err
}
if err := ps.initTxManager(cfg); err != nil {
return fmt.Errorf("failed to init Tx manager: %w", err)
}
ps.initBalanceMonitor(cfg)
if err := ps.initMetricsServer(cfg); err != nil {
return fmt.Errorf("failed to start metrics server: %w", err)
}
if err := ps.initPProf(cfg); err != nil {
return fmt.Errorf("failed to start pprof server: %w", err)
}
if err := ps.initL2ooAddress(cfg); err != nil {
return fmt.Errorf("failed to init L2ooAddress: %w", err)
}
if err := ps.initDriver(); err != nil {
return fmt.Errorf("failed to init Driver: %w", err)
}
if err := ps.initRPCServer(cfg); err != nil {
return fmt.Errorf("failed to start RPC server: %w", err)
}
ps.Metrics.RecordInfo(ps.Version)
ps.Metrics.RecordUp()
return nil
}
func (ps *ProposerService) initRPCClients(ctx context.Context, cfg *CLIConfig) error {
l1Client, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, ps.Log, cfg.L1EthRpc)
if err != nil {
return fmt.Errorf("failed to dial L1 RPC: %w", err)
}
ps.L1Client = l1Client
rollupClient, err := dial.DialRollupClientWithTimeout(ctx, dial.DefaultDialTimeout, ps.Log, cfg.RollupRpc)
if err != nil {
return fmt.Errorf("failed to dial L2 rollup-client RPC: %w", err)
}
ps.RollupClient = rollupClient
return nil
}
func (ps *ProposerService) initMetrics(cfg *CLIConfig) {
if cfg.MetricsConfig.Enabled {
procName := "default"
ps.Metrics = metrics.NewMetrics(procName)
} else {
ps.Metrics = metrics.NoopMetrics
}
}
// initBalanceMonitor depends on Metrics, L1Client and TxManager to start background-monitoring of the Proposer balance.
func (ps *ProposerService) initBalanceMonitor(cfg *CLIConfig) {
if cfg.MetricsConfig.Enabled {
ps.balanceMetricer = ps.Metrics.StartBalanceMetrics(ps.Log, ps.L1Client, ps.TxManager.From())
}
}
func (ps *ProposerService) initTxManager(cfg *CLIConfig) error {
txManager, err := txmgr.NewSimpleTxManager("proposer", ps.Log, ps.Metrics, cfg.TxMgrConfig)
if err != nil {
return err
}
ps.TxManager = txManager
return nil
}
func (ps *ProposerService) initPProf(cfg *CLIConfig) error {
if !cfg.PprofConfig.Enabled {
return nil
}
log.Debug("starting pprof server", "addr", net.JoinHostPort(cfg.PprofConfig.ListenAddr, strconv.Itoa(cfg.PprofConfig.ListenPort)))
srv, err := oppprof.StartServer(cfg.PprofConfig.ListenAddr, cfg.PprofConfig.ListenPort)
if err != nil {
return err
}
ps.pprofSrv = srv
log.Info("started pprof server", "addr", srv.Addr())
return nil
}
func (ps *ProposerService) initMetricsServer(cfg *CLIConfig) error {
if !cfg.MetricsConfig.Enabled {
ps.Log.Info("metrics disabled")
return nil
}
m, ok := ps.Metrics.(opmetrics.RegistryMetricer)
if !ok {
return fmt.Errorf("metrics were enabled, but metricer %T does not expose registry for metrics-server", ps.Metrics)
}
ps.Log.Debug("starting metrics server", "addr", cfg.MetricsConfig.ListenAddr, "port", cfg.MetricsConfig.ListenPort)
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)
}
ps.Log.Info("started metrics server", "addr", metricsSrv.Addr())
ps.metricsSrv = metricsSrv
return nil
}
func (ps *ProposerService) initL2ooAddress(cfg *CLIConfig) error {
l2ooAddress, err := opservice.ParseAddress(cfg.L2OOAddress)
if err != nil {
return nil
}
ps.L2OutputOracleAddr = l2ooAddress
return nil
}
func (ps *ProposerService) initDriver() error {
driver, err := NewL2OutputSubmitter(DriverSetup{
Log: ps.Log,
Metr: ps.Metrics,
Cfg: ps.ProposerConfig,
Txmgr: ps.TxManager,
L1Client: ps.L1Client,
RollupClient: ps.RollupClient,
})
if err != nil {
return err
}
ps.driver = driver
return nil
}
func (ps *ProposerService) initRPCServer(cfg *CLIConfig) error {
server := oprpc.NewServer(
cfg.RPCConfig.ListenAddr,
cfg.RPCConfig.ListenPort,
ps.Version,
oprpc.WithLogger(ps.Log),
)
if cfg.RPCConfig.EnableAdmin {
adminAPI := rpc.NewAdminAPI(ps.driver, ps.Metrics, ps.Log)
server.AddAPI(rpc.GetAdminAPI(adminAPI))
ps.Log.Info("Admin RPC enabled")
}
ps.Log.Info("Starting JSON-RPC server")
if err := server.Start(); err != nil {
return fmt.Errorf("unable to start RPC server: %w", err)
}
ps.rpcServer = server
return nil
}
// Start runs once upon start of the proposer lifecycle,
// and starts L2Output-submission work if the proposer is configured to start submit data on startup.
func (ps *ProposerService) Start(_ context.Context) error {
ps.driver.Log.Info("Starting Proposer")
return ps.driver.StartL2OutputSubmitting()
}
func (ps *ProposerService) Stopped() bool {
return ps.stopped.Load()
}
// Kill is a convenience method to forcefully, non-gracefully, stop the ProposerService.
func (ps *ProposerService) Kill() error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ps.Stop(ctx)
}
// Stop fully stops the L2Output-submitter and all its resources gracefully. After stopping, it cannot be restarted.
// See driver.StopL2OutputSubmitting to temporarily stop the L2Output submitter.
func (ps *ProposerService) Stop(ctx context.Context) error {
if ps.stopped.Load() {
return errors.New("already stopped")
}
ps.Log.Info("Stopping Proposer")
var result error
if ps.driver != nil {
if err := ps.driver.StopL2OutputSubmittingIfRunning(); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop L2Output submitting: %w", err))
}
}
if ps.rpcServer != nil {
// TODO(7685): the op-service RPC server is not built on top of op-service httputil Server, and has poor shutdown
if err := ps.rpcServer.Stop(); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop RPC server: %w", err))
}
}
if ps.pprofSrv != nil {
if err := ps.pprofSrv.Stop(ctx); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop PProf server: %w", err))
}
}
if ps.balanceMetricer != nil {
if err := ps.balanceMetricer.Close(); err != nil {
result = errors.Join(result, fmt.Errorf("failed to close balance metricer: %w", err))
}
}
if ps.metricsSrv != nil {
if err := ps.metricsSrv.Stop(ctx); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop metrics server: %w", err))
}
}
if ps.L1Client != nil {
ps.L1Client.Close()
}
if ps.RollupClient != nil {
ps.RollupClient.Close()
}
if result == nil {
ps.stopped.Store(true)
ps.Log.Info("L2Output Submitter stopped")
}
return result
}
var _ cliapp.Lifecycle = (*ProposerService)(nil)
// Driver returns the handler on the L2Output-submitter driver element,
// to start/stop/restart the L2Output-submission work, for use in testing.
func (ps *ProposerService) Driver() rpc.ProposerDriver {
return ps.driver
}
......@@ -48,12 +48,14 @@ var _ Lifecycle = (*fakeLifecycle)(nil)
func TestLifecycleCmd(t *testing.T) {
appSetup := func(t *testing.T, shareApp **fakeLifecycle) (signalCh chan struct{}, initCh, startCh, stopCh, resultCh chan error) {
appSetup := func(t *testing.T) (signalCh chan struct{}, initCh, startCh, stopCh, resultCh chan error, appCh chan *fakeLifecycle) {
signalCh = make(chan struct{})
initCh = make(chan error)
startCh = make(chan error)
stopCh = make(chan error)
resultCh = make(chan error)
// optional channel to retrieve the fakeLifecycle from, available some time after init, before start.
appCh = make(chan *fakeLifecycle, 1)
// mock an application that may fail at different stages of its lifecycle
mockAppFn := func(ctx *cli.Context, close context.CancelCauseFunc) (Lifecycle, error) {
......@@ -72,9 +74,7 @@ func TestLifecycleCmd(t *testing.T) {
stopped: false,
selfClose: close,
}
if shareApp != nil {
*shareApp = app
}
appCh <- app
return app, nil
}
......@@ -115,19 +115,20 @@ func TestLifecycleCmd(t *testing.T) {
close(startCh)
close(stopCh)
close(resultCh)
close(appCh)
})
return
}
t.Run("interrupt int", func(t *testing.T) {
signalCh, _, _, _, resultCh := appSetup(t, nil)
signalCh, _, _, _, resultCh, _ := appSetup(t)
signalCh <- struct{}{}
res := <-resultCh
require.ErrorIs(t, res, interruptErr)
require.ErrorContains(t, res, "failed to setup")
})
t.Run("failed init", func(t *testing.T) {
_, initCh, _, _, resultCh := appSetup(t, nil)
_, initCh, _, _, resultCh, _ := appSetup(t)
v := errors.New("TEST INIT ERRROR")
initCh <- v
res := <-resultCh
......@@ -135,9 +136,9 @@ func TestLifecycleCmd(t *testing.T) {
require.ErrorContains(t, res, "failed to setup")
})
t.Run("interrupt start", func(t *testing.T) {
var app *fakeLifecycle
signalCh, initCh, _, _, resultCh := appSetup(t, &app)
signalCh, initCh, _, _, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
signalCh <- struct{}{}
res := <-resultCh
......@@ -146,9 +147,9 @@ func TestLifecycleCmd(t *testing.T) {
require.True(t, app.Stopped())
})
t.Run("failed start", func(t *testing.T) {
var app *fakeLifecycle
_, initCh, startCh, _, resultCh := appSetup(t, &app)
_, initCh, startCh, _, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
v := errors.New("TEST START ERROR")
startCh <- v
......@@ -158,9 +159,9 @@ func TestLifecycleCmd(t *testing.T) {
require.True(t, app.Stopped())
})
t.Run("graceful shutdown", func(t *testing.T) {
var app *fakeLifecycle
signalCh, initCh, startCh, stopCh, resultCh := appSetup(t, &app)
signalCh, initCh, startCh, stopCh, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
startCh <- nil
signalCh <- struct{}{} // interrupt, but at an expected time
......@@ -169,9 +170,9 @@ func TestLifecycleCmd(t *testing.T) {
require.True(t, app.Stopped())
})
t.Run("interrupted shutdown", func(t *testing.T) {
var app *fakeLifecycle
signalCh, initCh, startCh, _, resultCh := appSetup(t, &app)
signalCh, initCh, startCh, _, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
startCh <- nil
signalCh <- struct{}{} // start graceful shutdown
......@@ -182,9 +183,9 @@ func TestLifecycleCmd(t *testing.T) {
require.True(t, app.Stopped()) // still fully closes, interrupts only accelerate shutdown where possible.
})
t.Run("failed shutdown", func(t *testing.T) {
var app *fakeLifecycle
signalCh, initCh, startCh, stopCh, resultCh := appSetup(t, &app)
signalCh, initCh, startCh, stopCh, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
startCh <- nil
signalCh <- struct{}{} // start graceful shutdown
......@@ -196,9 +197,9 @@ func TestLifecycleCmd(t *testing.T) {
require.True(t, app.Stopped())
})
t.Run("app self-close", func(t *testing.T) {
var app *fakeLifecycle
_, initCh, startCh, stopCh, resultCh := appSetup(t, &app)
_, initCh, startCh, stopCh, resultCh, appCh := appSetup(t)
initCh <- nil
app := <-appCh
require.False(t, app.Stopped())
startCh <- nil
v := errors.New("TEST SELF CLOSE ERROR")
......
package ioutil
import (
"io"
"os"
"path/filepath"
)
type atomicWriter struct {
dest string
temp string
out io.WriteCloser
}
// NewAtomicWriterCompressed creates a io.WriteCloser that performs an atomic write.
// The contents are initially written to a temporary file and only renamed into place when the writer is closed.
// NOTE: It's vital to check if an error is returned from Close() as it may indicate the file could not be renamed
// If path ends in .gz the contents written will be gzipped.
func NewAtomicWriterCompressed(path string, perm os.FileMode) (io.WriteCloser, error) {
f, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path))
if err != nil {
return nil, err
}
if err := f.Chmod(perm); err != nil {
_ = f.Close()
return nil, err
}
return &atomicWriter{
dest: path,
temp: f.Name(),
out: CompressByFileType(path, f),
}, nil
}
func (a *atomicWriter) Write(p []byte) (n int, err error) {
return a.out.Write(p)
}
func (a *atomicWriter) Close() error {
// Attempt to clean up the temp file even if it can't be renamed into place.
defer os.Remove(a.temp)
if err := a.out.Close(); err != nil {
return err
}
return os.Rename(a.temp, a.dest)
}
package ioutil
import (
"io"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestAtomicWriter_RenameOnClose(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
f, err := NewAtomicWriterCompressed(target, 0755)
require.NoError(t, err)
defer f.Close()
_, err = os.Stat(target)
require.ErrorIs(t, err, os.ErrNotExist, "should not create target file when created")
content := ([]byte)("Hello world")
n, err := f.Write(content)
require.NoError(t, err)
require.Equal(t, len(content), n)
_, err = os.Stat(target)
require.ErrorIs(t, err, os.ErrNotExist, "should not create target file when writing")
require.NoError(t, f.Close())
stat, err := os.Stat(target)
require.NoError(t, err, "should create target file when closed")
require.EqualValues(t, fs.FileMode(0755), stat.Mode())
files, err := os.ReadDir(dir)
require.NoError(t, err)
require.Len(t, files, 1, "should not leave temporary files behind")
}
func TestAtomicWriter_MultipleClose(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
f, err := NewAtomicWriterCompressed(target, 0755)
require.NoError(t, err)
require.NoError(t, f.Close())
require.ErrorIs(t, f.Close(), os.ErrClosed)
}
func TestAtomicWriter_ApplyGzip(t *testing.T) {
tests := []struct {
name string
filename string
compressed bool
}{
{"Uncompressed", "test.notgz", false},
{"Gzipped", "test.gz", true},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
data := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0}
dir := t.TempDir()
path := filepath.Join(dir, test.filename)
out, err := NewAtomicWriterCompressed(path, 0o644)
require.NoError(t, err)
defer out.Close()
_, err = out.Write(data)
require.NoError(t, err)
require.NoError(t, out.Close())
writtenData, err := os.ReadFile(path)
require.NoError(t, err)
if test.compressed {
require.NotEqual(t, data, writtenData, "should have compressed data on disk")
} else {
require.Equal(t, data, writtenData, "should not have compressed data on disk")
}
in, err := OpenDecompressed(path)
require.NoError(t, err)
readData, err := io.ReadAll(in)
require.NoError(t, err)
require.Equal(t, data, readData)
})
}
}
......@@ -33,10 +33,7 @@ func OpenCompressed(file string, flag int, perm os.FileMode) (io.WriteCloser, er
if err != nil {
return nil, err
}
if IsGzip(file) {
out = gzip.NewWriter(out)
}
return out, nil
return CompressByFileType(file, out), nil
}
// WriteCompressedJson writes the object to the specified file as a compressed json object
......@@ -58,3 +55,10 @@ func WriteCompressedJson(file string, obj any) error {
func IsGzip(path string) bool {
return strings.HasSuffix(path, ".gz")
}
func CompressByFileType(file string, out io.WriteCloser) io.WriteCloser {
if IsGzip(file) {
return gzip.NewWriter(out)
}
return out
}
......@@ -57,6 +57,10 @@ RUN apt-get update && apt-get install -y \
libudev-dev \
--no-install-recommends
COPY /ops/docker/oplabs.crt /usr/local/share/ca-certificates/oplabs.crt
RUN chmod 644 /usr/local/share/ca-certificates/oplabs.crt \
&& update-ca-certificates
RUN npm install pnpm --global
COPY --from=foundry /usr/local/bin/forge /usr/local/bin/forge
......@@ -114,4 +118,6 @@ FROM base as wd-mon
WORKDIR /opt/optimism/packages/chain-mon
CMD ["start:wd-mon"]
FROM base as contracts-bedrock
WORKDIR /opt/optimism/packages/contracts-bedrock
CMD ["deploy"]
-----BEGIN CERTIFICATE-----
MIICEDCCAZagAwIBAgIUALLKhe49OFLAGb5Zt+DZlvpKScswCgYIKoZIzj0EAwMw
KTEQMA4GA1UEChMHT1AgTGFiczEVMBMGA1UEAxMMb3BsYWJzLmNsb3VkMB4XDTIy
MTIxOTE3MDQwNFoXDTMyMTIxNjE3MDQwM1owKTEQMA4GA1UEChMHT1AgTGFiczEV
MBMGA1UEAxMMb3BsYWJzLmNsb3VkMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEyCco
PpvE5IRv5S0zqmpE2VsbhVzB+hiQjbQO3J6j5L+pAvWjXGvjZblGvNi5PIVBCxvm
5UofdFrOCAiOdRfevhadv3zLzGUmoJ52iXCTPL01dlkQt5KUsoT+AU7GPW4Ko38w
fTAOBgNVHQ8BAf8EBAMCAaYwEgYDVR0TAQH/BAgwBgEB/wIBCjAdBgNVHQ4EFgQU
2AVLbBUBZcBOPkg8QCAOvSMrdj0wHwYDVR0jBBgwFoAU2AVLbBUBZcBOPkg8QCAO
vSMrdj0wFwYDVR0RBBAwDoIMb3BsYWJzLmNsb3VkMAoGCCqGSM49BAMDA2gAMGUC
MBuERHbRkWDwXm97jqKEGANU4VDBqgmRicdF7FspDqA5Zcpj+r+rQVaDlH0qvtxH
SQIxAL3fjNoC1Kon4kKmPQdp5KNhvGzOaoQiqbb5JuL3+j6f3x0ucLVD1yWP/V/+
zZ/vlQ==
-----END CERTIFICATE-----
......@@ -35,7 +35,7 @@
},
"dependencies": {
"@eth-optimism/core-utils": "workspace:*",
"@sentry/node": "^7.80.0",
"@sentry/node": "^7.80.1",
"bcfg": "^0.2.1",
"body-parser": "^1.20.2",
"commander": "^11.1.0",
......@@ -55,7 +55,7 @@
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0",
"@types/express": "^4.17.19",
"@types/morgan": "^1.9.7",
"@types/morgan": "^1.9.9",
"@types/pino": "^7.0.5",
"@types/pino-multi-stream": "^5.1.5",
"chai": "^4.3.10",
......
......@@ -20,6 +20,7 @@
"test": "pnpm build:go-ffi && forge test",
"coverage": "pnpm build:go-ffi && forge coverage",
"coverage:lcov": "pnpm build:go-ffi && forge coverage --report lcov",
"deploy": "./scripts/deploy.sh",
"gas-snapshot:no-build": "forge snapshot --no-match-test 'testDiff|testFuzz|invariant|generateArtifact'",
"gas-snapshot": "pnpm build:go-ffi && pnpm gas-snapshot:no-build",
"storage-snapshot": "./scripts/storage-snapshot.sh",
......@@ -49,4 +50,4 @@
"tsx": "^4.1.1",
"typescript": "^5.2.2"
}
}
}
\ No newline at end of file
#!/usr/bin/env bash
set -euo pipefail
verify_flag=""
if [ -n "${DEPLOY_VERIFY:-}" ]; then
verify_flag="--verify"
fi
echo "> Deploying contracts"
forge script -vvv scripts/Deploy.s.sol:Deploy --rpc-url "$DEPLOY_ETH_RPC_URL" --broadcast --private-key "$DEPLOY_PRIVATE_KEY" $verify_flag
if [ -n "${DEPLOY_GENERATE_HARDHAT_ARTIFACTS:-}" ]; then
echo "> Generating hardhat artifacts"
forge script -vvv scripts/Deploy.s.sol:Deploy --sig 'sync()' --rpc-url "$DEPLOY_ETH_RPC_URL" --broadcast --private-key "$DEPLOY_PRIVATE_KEY"
fi
......@@ -185,5 +185,7 @@ contract Setup is Deploy {
vm.label(Predeploys.GAS_PRICE_ORACLE, "GasPriceOracle");
vm.label(Predeploys.LEGACY_MESSAGE_PASSER, "LegacyMessagePasser");
vm.label(Predeploys.GOVERNANCE_TOKEN, "GovernanceToken");
vm.label(Predeploys.EAS, "EAS");
vm.label(Predeploys.SCHEMA_REGISTRY, "SchemaRegistry");
}
}
......@@ -85,8 +85,8 @@ importers:
specifier: ^8.0.3
version: 8.0.3
lint-staged:
specifier: 15.0.2
version: 15.0.2
specifier: 15.1.0
version: 15.1.0
markdownlint:
specifier: ^0.32.0
version: 0.32.0
......@@ -192,8 +192,8 @@ importers:
specifier: workspace:*
version: link:../core-utils
'@sentry/node':
specifier: ^7.80.0
version: 7.80.0
specifier: ^7.80.1
version: 7.80.1
bcfg:
specifier: ^0.2.1
version: 0.2.1
......@@ -247,8 +247,8 @@ importers:
specifier: ^4.17.19
version: 4.17.19
'@types/morgan':
specifier: ^1.9.7
version: 1.9.7
specifier: ^1.9.9
version: 1.9.9
'@types/pino':
specifier: ^7.0.5
version: 7.0.5
......@@ -3248,13 +3248,13 @@ packages:
'@noble/hashes': 1.3.2
'@scure/base': 1.1.3
/@sentry-internal/tracing@7.80.0:
resolution: {integrity: sha512-P1Ab9gamHLsbH9D82i1HY8xfq9dP8runvc4g50AAd6OXRKaJ45f2KGRZUmnMEVqBQ7YoPYp2LFMkrhNYbcZEoQ==}
/@sentry-internal/tracing@7.80.1:
resolution: {integrity: sha512-5gZ4LPIj2vpQl2/dHBM4uXMi9OI5E0VlOhJQt0foiuN6JJeiOjdpJFcfVqJk69wrc0deVENTtgKKktxqMwVeWQ==}
engines: {node: '>=8'}
dependencies:
'@sentry/core': 7.80.0
'@sentry/types': 7.80.0
'@sentry/utils': 7.80.0
'@sentry/core': 7.80.1
'@sentry/types': 7.80.1
'@sentry/utils': 7.80.1
dev: false
/@sentry/core@5.30.0:
......@@ -3268,12 +3268,12 @@ packages:
tslib: 1.14.1
dev: true
/@sentry/core@7.80.0:
resolution: {integrity: sha512-nJiiymdTSEyI035/rdD3VOq6FlOZ2wWLR5bit9LK8a3rzHU3UXkwScvEo6zYgs0Xp1sC0yu1S9+0BEiYkmi29A==}
/@sentry/core@7.80.1:
resolution: {integrity: sha512-3Yh+O9Q86MxwIuJFYtuSSoUCpdx99P1xDAqL0FIPTJ+ekaVMiUJq9NmyaNh9uN2myPSmxvEXW6q3z37zta9ZHg==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.80.0
'@sentry/utils': 7.80.0
'@sentry/types': 7.80.1
'@sentry/utils': 7.80.1
dev: false
/@sentry/hub@5.30.0:
......@@ -3311,14 +3311,14 @@ packages:
- supports-color
dev: true
/@sentry/node@7.80.0:
resolution: {integrity: sha512-J35fqe8J5ac/17ZXT0ML3opYGTOclqYNE9Sybs1y9n6BqacHyzH8By72YrdI03F7JJDHwrcGw+/H8hGpkCwi0Q==}
/@sentry/node@7.80.1:
resolution: {integrity: sha512-0NWfcZMlyQphKWsvyzfhGm2dCBk5DUPqOGW/vGx18G4tCCYtFcAIj/mCp/4XOEcZRPQgb9vkm+sidGD6DnwWlA==}
engines: {node: '>=8'}
dependencies:
'@sentry-internal/tracing': 7.80.0
'@sentry/core': 7.80.0
'@sentry/types': 7.80.0
'@sentry/utils': 7.80.0
'@sentry-internal/tracing': 7.80.1
'@sentry/core': 7.80.1
'@sentry/types': 7.80.1
'@sentry/utils': 7.80.1
https-proxy-agent: 5.0.1
transitivePeerDependencies:
- supports-color
......@@ -3340,8 +3340,8 @@ packages:
engines: {node: '>=6'}
dev: true
/@sentry/types@7.80.0:
resolution: {integrity: sha512-4bpMO+2jWiWLDa8zbTASWWNLWe6yhjfPsa7/6VH5y9x1NGtL8oRbqUsTgsvjF3nmeHEMkHQsC8NHPaQ/ibFmZQ==}
/@sentry/types@7.80.1:
resolution: {integrity: sha512-CVu4uPVTOI3U9kYiOdA085R7jX5H1oVODbs9y+A8opJ0dtJTMueCXgZyE8oXQ0NjGVs6HEeaLkOuiV0mj8X3yw==}
engines: {node: '>=8'}
dev: false
......@@ -3353,11 +3353,11 @@ packages:
tslib: 1.14.1
dev: true
/@sentry/utils@7.80.0:
resolution: {integrity: sha512-XbBCEl6uLvE50ftKwrEo6XWdDaZXHXu+kkHXTPWQEcnbvfZKLuG9V0Hxtxxq3xQgyWmuF05OH1GcqYqiO+v5Yg==}
/@sentry/utils@7.80.1:
resolution: {integrity: sha512-bfFm2e/nEn+b9++QwjNEYCbS7EqmteT8uf0XUs7PljusSimIqqxDtK1pfD9zjynPgC8kW/fVBKv0pe2LufomeA==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.80.0
'@sentry/types': 7.80.1
dev: false
/@sinclair/typebox@0.27.8:
......@@ -3996,10 +3996,10 @@ packages:
resolution: {integrity: sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==}
dev: true
/@types/morgan@1.9.7:
resolution: {integrity: sha512-4sJFBUBrIZkP5EvMm1L6VCXp3SQe8dnXqlVpe1jsmTjS1JQVmSjnpMNs8DosQd6omBi/K7BSKJ6z/Mc3ki0K9g==}
/@types/morgan@1.9.9:
resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==}
dependencies:
'@types/node': 20.8.9
'@types/node': 20.9.0
dev: true
/@types/ms@0.7.31:
......@@ -10178,8 +10178,8 @@ packages:
uc.micro: 1.0.6
dev: true
/lint-staged@15.0.2:
resolution: {integrity: sha512-vnEy7pFTHyVuDmCAIFKR5QDO8XLVlPFQQyujQ/STOxe40ICWqJ6knS2wSJ/ffX/Lw0rz83luRDh+ET7toN+rOw==}
/lint-staged@15.1.0:
resolution: {integrity: sha512-ZPKXWHVlL7uwVpy8OZ7YQjYDAuO5X4kMh0XgZvPNxLcCCngd0PO5jKQyy3+s4TL2EnHoIXIzP1422f/l3nZKMw==}
engines: {node: '>=18.12.0'}
hasBin: true
dependencies:
......@@ -10192,7 +10192,7 @@ packages:
micromatch: 4.0.5
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.3.3
yaml: 2.3.4
transitivePeerDependencies:
- supports-color
dev: true
......@@ -11913,7 +11913,7 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
'@sentry/node': 7.80.0
'@sentry/node': 7.80.1
commander: 2.20.3
pumpify: 2.0.1
split2: 3.2.2
......@@ -12007,7 +12007,7 @@ packages:
optional: true
dependencies:
lilconfig: 2.1.0
yaml: 2.3.2
yaml: 2.3.3
dev: true
/postcss@8.4.27:
......@@ -15440,13 +15440,13 @@ packages:
engines: {node: '>= 14'}
dev: true
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
/yaml@2.3.3:
resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==}
engines: {node: '>= 14'}
dev: true
/yaml@2.3.3:
resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==}
/yaml@2.3.4:
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
engines: {node: '>= 14'}
dev: true
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment