Commit b05ec5af authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

op-deployer: Safety and validation improvements (#12993)

- Enables support for deploying tagged versions against new chains, but behind a huge warning that requires user input to bypass. Since our tagged release versions do not contain all implementations, using op-deployer in this way will deploy contracts that haven't been governance approved. Since this workflow is useful for development but bad for prod, I've added support for it but users have to bypass a large warning that describes the risks.
- Validates the hashes of tagged version artifacts after downloading them. This prevents users from downloading tampered versions of the artifacts from GCS.
parent 0bfa9305
......@@ -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 {
......
......@@ -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,6 +36,7 @@ func TestDownloadArtifacts(t *testing.T) {
URL: artifactsURL,
}
t.Run("success", func(t *testing.T) {
fs, cleanup, err := Download(ctx, loc, nil)
require.NoError(t, err)
require.NotNil(t, fs)
......@@ -41,4 +47,39 @@ func TestDownloadArtifacts(t *testing.T) {
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)
})
}
}
......@@ -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
}
......@@ -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)
t.Run("two chains one after another", func(t *testing.T) {
intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc)
cg := ethClientCodeGetter(ctx, l1Client)
t.Run("initial chain", func(t *testing.T) {
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(
......
......@@ -35,14 +35,16 @@ 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"
}
} else {
contractsRelease = "dev"
}
proofParams, err := jsonutil.MergeJSON(
SuperchainProofParams{
WithdrawalDelaySeconds: standard.WithdrawalDelaySeconds,
......
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
}
......@@ -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)
}
......
......@@ -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),
......
......@@ -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 {
......
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