diff --git a/op-deployer/pkg/deployer/artifacts/downloader.go b/op-deployer/pkg/deployer/artifacts/downloader.go index 1303adbe86aa8..7e566952e09a1 100644 --- a/op-deployer/pkg/deployer/artifacts/downloader.go +++ b/op-deployer/pkg/deployer/artifacts/downloader.go @@ -3,8 +3,10 @@ package artifacts import ( "archive/tar" "bufio" + "bytes" "compress/gzip" "context" + "crypto/sha256" "errors" "fmt" "io" @@ -15,6 +17,8 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum/go-ethereum/log" @@ -41,15 +45,50 @@ func LogProgressor(lgr log.Logger) DownloadProgressor { func Download(ctx context.Context, loc *Locator, progress DownloadProgressor) (foundry.StatDirFs, CleanupFunc, error) { var u *url.URL var err error + var checker integrityChecker if loc.IsTag() { u, err = standard.ArtifactsURLForTag(loc.Tag) if err != nil { return nil, nil, fmt.Errorf("failed to get standard artifacts URL for tag %s: %w", loc.Tag, err) } + + hash, err := standard.ArtifactsHashForTag(loc.Tag) + if err != nil { + return nil, nil, fmt.Errorf("failed to get standard artifacts hash for tag %s: %w", loc.Tag, err) + } + + checker = &hashIntegrityChecker{hash: hash} } else { u = loc.URL + checker = &noopIntegrityChecker{} } + return downloadURL(ctx, u, progress, checker) +} + +type integrityChecker interface { + CheckIntegrity(data []byte) error +} + +type hashIntegrityChecker struct { + hash common.Hash +} + +func (h *hashIntegrityChecker) CheckIntegrity(data []byte) error { + hash := sha256.Sum256(data) + if hash != h.hash { + return fmt.Errorf("integrity check failed - expected: %x, got: %x", h.hash, hash) + } + return nil +} + +type noopIntegrityChecker struct{} + +func (noopIntegrityChecker) CheckIntegrity(data []byte) error { + return nil +} + +func downloadURL(ctx context.Context, u *url.URL, progress DownloadProgressor, checker integrityChecker) (foundry.StatDirFs, CleanupFunc, error) { switch u.Scheme { case "http", "https": req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) @@ -78,7 +117,16 @@ func Download(ctx context.Context, loc *Locator, progress DownloadProgressor) (f total: resp.ContentLength, } - gr, err := gzip.NewReader(pr) + data, err := io.ReadAll(pr) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + + if err := checker.CheckIntegrity(data); err != nil { + return nil, nil, fmt.Errorf("failed to check integrity: %w", err) + } + + gr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return nil, nil, fmt.Errorf("failed to create gzip reader: %w", err) } @@ -111,7 +159,6 @@ type progressReader struct { } func (pr *progressReader) Read(p []byte) (int, error) { - n, err := pr.r.Read(p) pr.curr += int64(n) if pr.progress != nil && time.Since(pr.lastPrint) > 1*time.Second { diff --git a/op-deployer/pkg/deployer/artifacts/downloader_test.go b/op-deployer/pkg/deployer/artifacts/downloader_test.go index e66b41f96a814..cf4ef4742c94f 100644 --- a/op-deployer/pkg/deployer/artifacts/downloader_test.go +++ b/op-deployer/pkg/deployer/artifacts/downloader_test.go @@ -9,10 +9,12 @@ import ( "os" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" ) -func TestDownloadArtifacts(t *testing.T) { +func TestDownloadArtifacts_MockArtifacts(t *testing.T) { f, err := os.OpenFile("testdata/artifacts.tar.gz", os.O_RDONLY, 0o644) require.NoError(t, err) defer f.Close() @@ -21,6 +23,9 @@ func TestDownloadArtifacts(t *testing.T) { w.WriteHeader(http.StatusOK) _, err := io.Copy(w, f) require.NoError(t, err) + // Seek to beginning of file for next request + _, err = f.Seek(0, 0) + require.NoError(t, err) })) defer ts.Close() @@ -31,14 +36,50 @@ func TestDownloadArtifacts(t *testing.T) { URL: artifactsURL, } - fs, cleanup, err := Download(ctx, loc, nil) - require.NoError(t, err) - require.NotNil(t, fs) - defer func() { - require.NoError(t, cleanup()) - }() + t.Run("success", func(t *testing.T) { + fs, cleanup, err := Download(ctx, loc, nil) + require.NoError(t, err) + require.NotNil(t, fs) + defer func() { + require.NoError(t, cleanup()) + }() - info, err := fs.Stat("WETH98.sol/WETH98.json") - require.NoError(t, err) - require.Greater(t, info.Size(), int64(0)) + info, err := fs.Stat("WETH98.sol/WETH98.json") + require.NoError(t, err) + require.Greater(t, info.Size(), int64(0)) + }) + + t.Run("bad integrity", func(t *testing.T) { + _, _, err := downloadURL(ctx, loc.URL, nil, &hashIntegrityChecker{ + hash: common.Hash{'B', 'A', 'D'}, + }) + require.Error(t, err) + require.ErrorContains(t, err, "integrity check failed") + }) + + t.Run("ok integrity", func(t *testing.T) { + _, _, err := downloadURL(ctx, loc.URL, nil, &hashIntegrityChecker{ + hash: common.HexToHash("0x0f814df0c4293aaaadd468ac37e6c92f0b40fd21df848076835cb2c21d2a516f"), + }) + require.NoError(t, err) + }) +} + +func TestDownloadArtifacts_TaggedVersions(t *testing.T) { + tags := []string{ + "op-contracts/v1.6.0", + "op-contracts/v1.7.0-beta.1+l2-contracts", + } + for _, tag := range tags { + t.Run(tag, func(t *testing.T) { + t.Parallel() + + loc := MustNewLocatorFromTag(tag) + _, cleanup, err := Download(context.Background(), loc, nil) + t.Cleanup(func() { + require.NoError(t, cleanup()) + }) + require.NoError(t, err) + }) + } } diff --git a/op-deployer/pkg/deployer/broadcaster/gas_estimator.go b/op-deployer/pkg/deployer/broadcaster/gas_estimator.go index abe76d027ec40..b04390fc8aa78 100644 --- a/op-deployer/pkg/deployer/broadcaster/gas_estimator.go +++ b/op-deployer/pkg/deployer/broadcaster/gas_estimator.go @@ -11,15 +11,20 @@ import ( var ( // baseFeePadFactor = 50% as a divisor baseFeePadFactor = big.NewInt(2) - // tipMulFactor = 20 as a multiplier - tipMulFactor = big.NewInt(20) + // tipMulFactor = 5 as a multiplier + tipMulFactor = big.NewInt(5) // dummyBlobFee is a dummy value for the blob fee. Since this gas estimator will never // post blobs, it's just set to 1. dummyBlobFee = big.NewInt(1) + // maxTip is the maximum tip that can be suggested by this estimator. + maxTip = big.NewInt(50 * 1e9) + // minTip is the minimum tip that can be suggested by this estimator. + minTip = big.NewInt(1 * 1e9) ) // DeployerGasPriceEstimator is a custom gas price estimator for use with op-deployer. -// It pads the base fee by 50% and multiplies the suggested tip by 20. +// It pads the base fee by 50% and multiplies the suggested tip by 5 up to a max of +// 50 gwei. func DeployerGasPriceEstimator(ctx context.Context, client txmgr.ETHBackend) (*big.Int, *big.Int, *big.Int, error) { chainHead, err := client.HeaderByNumber(ctx, nil) if err != nil { @@ -34,5 +39,14 @@ func DeployerGasPriceEstimator(ctx context.Context, client txmgr.ETHBackend) (*b baseFeePad := new(big.Int).Div(chainHead.BaseFee, baseFeePadFactor) paddedBaseFee := new(big.Int).Add(chainHead.BaseFee, baseFeePad) paddedTip := new(big.Int).Mul(tip, tipMulFactor) + + if paddedTip.Cmp(minTip) < 0 { + paddedTip.Set(minTip) + } + + if paddedTip.Cmp(maxTip) > 0 { + paddedTip.Set(maxTip) + } + return paddedTip, paddedBaseFee, dummyBlobFee, nil } diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 0de23866946c7..f977fbbb0c721 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -100,21 +100,38 @@ func TestEndToEndApply(t *testing.T) { l1Client, err := ethclient.Dial(rpcURL) require.NoError(t, err) - depKey := new(deployerKey) + pk, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + l1ChainID := new(big.Int).SetUint64(defaultL1ChainID) dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) require.NoError(t, err) - pk, err := dk.Secret(depKey) - require.NoError(t, err) l2ChainID1 := uint256.NewInt(1) l2ChainID2 := uint256.NewInt(2) loc, _ := testutil.LocalArtifacts(t) - intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc) - cg := ethClientCodeGetter(ctx, l1Client) - t.Run("initial chain", func(t *testing.T) { + t.Run("two chains one after another", func(t *testing.T) { + intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc) + cg := ethClientCodeGetter(ctx, l1Client) + + require.NoError(t, deployer.ApplyPipeline( + ctx, + deployer.ApplyPipelineOpts{ + L1RPCUrl: rpcURL, + DeployerPrivateKey: pk, + Intent: intent, + State: st, + Logger: lgr, + StateWriter: pipeline.NoopStateWriter(), + }, + )) + + // create a new environment with wiped state to ensure we can continue using the + // state from the previous deployment + intent.Chains = append(intent.Chains, newChainIntent(t, dk, l1ChainID, l2ChainID2)) + require.NoError(t, deployer.ApplyPipeline( ctx, deployer.ApplyPipelineOpts{ @@ -131,10 +148,12 @@ func TestEndToEndApply(t *testing.T) { validateOPChainDeployment(t, cg, st, intent) }) - t.Run("subsequent chain", func(t *testing.T) { - // create a new environment with wiped state to ensure we can continue using the - // state from the previous deployment - intent.Chains = append(intent.Chains, newChainIntent(t, dk, l1ChainID, l2ChainID2)) + t.Run("chain with tagged artifacts", func(t *testing.T) { + intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc) + cg := ethClientCodeGetter(ctx, l1Client) + + intent.L1ContractsLocator = artifacts.DefaultL1ContractsLocator + intent.L2ContractsLocator = artifacts.DefaultL2ContractsLocator require.NoError(t, deployer.ApplyPipeline( ctx, @@ -148,6 +167,7 @@ func TestEndToEndApply(t *testing.T) { }, )) + validateSuperchainDeployment(t, st, cg) validateOPChainDeployment(t, cg, st, intent) }) } @@ -245,9 +265,26 @@ func testApplyExistingOPCM(t *testing.T, l1ChainID uint64, forkRPCUrl string, ve {"DelayedWETH", releases.DelayedWETH.ImplementationAddress, st.ImplementationsDeployment.DelayedWETHImplAddress}, } for _, tt := range implTests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expAddr, tt.actAddr) - }) + require.Equal(t, tt.expAddr, tt.actAddr, "unexpected address for %s", tt.name) + } + + superchain, err := standard.SuperchainFor(l1ChainIDBig.Uint64()) + require.NoError(t, err) + + managerOwner, err := standard.ManagerOwnerAddrFor(l1ChainIDBig.Uint64()) + require.NoError(t, err) + + superchainTests := []struct { + name string + expAddr common.Address + actAddr common.Address + }{ + {"ProxyAdmin", managerOwner, st.SuperchainDeployment.ProxyAdminAddress}, + {"SuperchainConfig", common.Address(*superchain.Config.SuperchainConfigAddr), st.SuperchainDeployment.SuperchainConfigProxyAddress}, + {"ProtocolVersions", common.Address(*superchain.Config.ProtocolVersionsAddr), st.SuperchainDeployment.ProtocolVersionsProxyAddress}, + } + for _, tt := range superchainTests { + require.Equal(t, tt.expAddr, tt.actAddr, "unexpected address for %s", tt.name) } artifactsFSL2, cleanupL2, err := artifacts.Download( diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index 9e65fd4f54a36..c2d409b5c3a49 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -35,10 +35,12 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro var err error if intent.L1ContractsLocator.IsTag() && intent.DeploymentStrategy == state.DeploymentStrategyLive { standardVersionsTOML, err = standard.L1VersionsDataFor(intent.L1ChainID) - if err != nil { - return fmt.Errorf("error getting standard versions TOML: %w", err) + if err == nil { + contractsRelease = intent.L1ContractsLocator.Tag + } else { + contractsRelease = "dev" } - contractsRelease = intent.L1ContractsLocator.Tag + } else { contractsRelease = "dev" } diff --git a/op-deployer/pkg/deployer/pipeline/init.go b/op-deployer/pkg/deployer/pipeline/init.go index b5c37c246f294..88caf760f41d8 100644 --- a/op-deployer/pkg/deployer/pipeline/init.go +++ b/op-deployer/pkg/deployer/pipeline/init.go @@ -1,11 +1,15 @@ package pipeline import ( + "bufio" "context" "crypto/rand" "fmt" + "os" + "strings" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" + "github.com/mattn/go-isatty" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" @@ -26,7 +30,11 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s return err } - if intent.L1ContractsLocator.IsTag() { + opcmAddress, opcmAddrErr := standard.ManagerImplementationAddrFor(intent.L1ChainID) + hasPredeployedOPCM := opcmAddrErr == nil + isTag := intent.L1ContractsLocator.IsTag() + + if isTag && hasPredeployedOPCM { superCfg, err := standard.SuperchainFor(intent.L1ChainID) if err != nil { return fmt.Errorf("error getting superchain config: %w", err) @@ -45,13 +53,13 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s SuperchainConfigProxyAddress: common.Address(*superCfg.Config.SuperchainConfigAddr), } - opcmAddress, err := standard.ManagerImplementationAddrFor(intent.L1ChainID) - if err != nil { - return fmt.Errorf("error getting OPCM proxy address: %w", err) - } st.ImplementationsDeployment = &state.ImplementationsDeployment{ OpcmAddress: opcmAddress, } + } else if isTag && !hasPredeployedOPCM { + if err := displayWarning(); err != nil { + return err + } } l1ChainID, err := env.L1Client.ChainID(ctx) @@ -127,3 +135,38 @@ func InitGenesisStrategy(env *Env, intent *state.Intent, st *state.State) error func immutableErr(field string, was, is any) error { return fmt.Errorf("%s is immutable: was %v, is %v", field, was, is) } + +func displayWarning() error { + warning := strings.TrimPrefix(` +####################### WARNING! WARNING WARNING! ####################### + +You are deploying a tagged release to a chain with no pre-deployed OPCM. +The contracts you are deploying may not be audited, or match a governance +approved release. + +USE OF THIS DEPLOYMENT IS NOT RECOMMENDED FOR PRODUCTION. USE AT YOUR OWN +RISK. BUGS OR LOSS OF FUNDS MAY OCCUR. WE HOPE YOU KNOW WHAT YOU ARE +DOING. + +####################### WARNING! WARNING WARNING! ####################### +`, "\n") + + _, _ = fmt.Fprint(os.Stderr, warning) + + if isatty.IsTerminal(os.Stdout.Fd()) { + _, _ = fmt.Fprintf(os.Stderr, "Please confirm that you have read and understood the warning above [y/n]: ") + + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read input: %w", err) + } + + input = strings.ToLower(strings.TrimSpace(input)) + if input != "y" && input != "yes" { + return fmt.Errorf("aborted") + } + } + + return nil +} diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index f8bbaa51b36eb..75902424ac78c 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -184,6 +184,17 @@ func ArtifactsURLForTag(tag string) (*url.URL, error) { } } +func ArtifactsHashForTag(tag string) (common.Hash, error) { + switch tag { + case "op-contracts/v1.6.0": + return common.HexToHash("d20a930cc0ff204c2d93b7aa60755ec7859ba4f328b881f5090c6a6a2a86dcba"), nil + case "op-contracts/v1.7.0-beta.1+l2-contracts": + return common.HexToHash("9e3ad322ec9b2775d59143ce6874892f9b04781742c603ad59165159e90b00b9"), nil + default: + return common.Hash{}, fmt.Errorf("unsupported tag: %s", tag) + } +} + func standardArtifactsURL(checksum string) string { return fmt.Sprintf("https://storage.googleapis.com/oplabs-contract-artifacts/artifacts-v1-%s.tar.gz", checksum) } diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 1a03c21d7e94b..11445c2fb6984 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -63,6 +63,11 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, EIP1559DenominatorCanyon: 250, EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, + + // STOP! This struct sets the _default_ upgrade schedule for all chains. + // Any upgrades you enable here will be enabled for all new deployments. + // In-development hardforks should never be activated here. Instead, they + // should be specified as overrides. UpgradeScheduleDeployConfig: genesis.UpgradeScheduleDeployConfig{ L2GenesisRegolithTimeOffset: u64UtilPtr(0), L2GenesisCanyonTimeOffset: u64UtilPtr(0), diff --git a/op-service/testutils/anvil/anvil.go b/op-service/testutils/anvil/anvil.go index 7419f9da6252b..50590a096a7f1 100644 --- a/op-service/testutils/anvil/anvil.go +++ b/op-service/testutils/anvil/anvil.go @@ -38,6 +38,8 @@ func New(l1RPCURL string, logger log.Logger) (*Runner, error) { "--fork-url", l1RPCURL, "--port", "0", + "--base-fee", + "1000000000", ) stdout, err := proc.StdoutPipe() if err != nil {