Commit c0298e37 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into jm/streamline-migration/phase2-finalize

parents 536178b0 5d711a05
...@@ -108,7 +108,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { ...@@ -108,7 +108,7 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) {
// Check the FPP confirms the expected output // Check the FPP confirms the expected output
t.Log("Running fault proof in fetching mode") t.Log("Running fault proof in fetching mode")
err = opp.FaultProofProgram(log, fppConfig) err = opp.FaultProofProgram(ctx, log, fppConfig)
require.NoError(t, err) require.NoError(t, err)
t.Log("Shutting down network") t.Log("Shutting down network")
...@@ -124,13 +124,13 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) { ...@@ -124,13 +124,13 @@ func testVerifyL2OutputRoot(t *testing.T, detached bool) {
// Should be able to rerun in offline mode using the pre-fetched images // Should be able to rerun in offline mode using the pre-fetched images
fppConfig.L1URL = "" fppConfig.L1URL = ""
fppConfig.L2URL = "" fppConfig.L2URL = ""
err = opp.FaultProofProgram(log, fppConfig) err = opp.FaultProofProgram(ctx, log, fppConfig)
require.NoError(t, err) require.NoError(t, err)
// Check that a fault is detected if we provide an incorrect claim // Check that a fault is detected if we provide an incorrect claim
t.Log("Running fault proof with invalid claim") t.Log("Running fault proof with invalid claim")
fppConfig.L2Claim = common.Hash{0xaa} fppConfig.L2Claim = common.Hash{0xaa}
err = opp.FaultProofProgram(log, fppConfig) err = opp.FaultProofProgram(ctx, log, fppConfig)
if detached { if detached {
require.Error(t, err, "exit status 1") require.Error(t, err, "exit status 1")
} else { } else {
......
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"github.com/ethereum-optimism/optimism/op-program/client/driver"
"github.com/ethereum-optimism/optimism/op-program/host" "github.com/ethereum-optimism/optimism/op-program/host"
"github.com/ethereum-optimism/optimism/op-program/host/config" "github.com/ethereum-optimism/optimism/op-program/host/config"
"github.com/ethereum-optimism/optimism/op-program/host/flags" "github.com/ethereum-optimism/optimism/op-program/host/flags"
...@@ -37,12 +35,8 @@ var VersionWithMeta = func() string { ...@@ -37,12 +35,8 @@ var VersionWithMeta = func() string {
func main() { func main() {
args := os.Args args := os.Args
if err := run(args, host.FaultProofProgram); errors.Is(err, driver.ErrClaimNotValid) { if err := run(args, host.Main); err != nil {
log.Crit("Claim is invalid", "err", err)
} else if err != nil {
log.Crit("Application failed", "err", err) log.Crit("Application failed", "err", err)
} else {
log.Info("Claim successfully verified")
} }
} }
......
...@@ -232,6 +232,28 @@ func TestExec(t *testing.T) { ...@@ -232,6 +232,28 @@ func TestExec(t *testing.T) {
}) })
} }
func TestServerMode(t *testing.T) {
t.Run("DefaultFalse", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs())
require.False(t, cfg.ServerMode)
})
t.Run("Enabled", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--server"))
require.True(t, cfg.ServerMode)
})
t.Run("EnabledWithArg", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--server=true"))
require.True(t, cfg.ServerMode)
})
t.Run("DisabledWithArg", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--server=false"))
require.False(t, cfg.ServerMode)
})
t.Run("InvalidArg", func(t *testing.T) {
verifyArgsInvalid(t, "invalid boolean value \"foo\" for -server", addRequiredArgs("--server=foo"))
})
}
func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) { func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := runWithArgs(cliArgs) _, _, err := runWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains) require.ErrorContains(t, err, messageContains)
......
...@@ -25,6 +25,7 @@ var ( ...@@ -25,6 +25,7 @@ var (
ErrInvalidL2Claim = errors.New("invalid l2 claim") ErrInvalidL2Claim = errors.New("invalid l2 claim")
ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number") ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number")
ErrDataDirRequired = errors.New("datadir must be specified when in non-fetching mode") ErrDataDirRequired = errors.New("datadir must be specified when in non-fetching mode")
ErrNoExecInServerMode = errors.New("exec command must not be set when in server mode")
) )
type Config struct { type Config struct {
...@@ -52,6 +53,10 @@ type Config struct { ...@@ -52,6 +53,10 @@ type Config struct {
// ExecCmd specifies the client program to execute in a separate process. // ExecCmd specifies the client program to execute in a separate process.
// If unset, the fault proof client is run in the same process. // If unset, the fault proof client is run in the same process.
ExecCmd string ExecCmd string
// ServerMode indicates that the program should run in pre-image server mode and wait for requests.
// No client program is run.
ServerMode bool
} }
func (c *Config) Check() error { func (c *Config) Check() error {
...@@ -82,6 +87,9 @@ func (c *Config) Check() error { ...@@ -82,6 +87,9 @@ func (c *Config) Check() error {
if !c.FetchingEnabled() && c.DataDir == "" { if !c.FetchingEnabled() && c.DataDir == "" {
return ErrDataDirRequired return ErrDataDirRequired
} }
if c.ServerMode && c.ExecCmd != "" {
return ErrNoExecInServerMode
}
return nil return nil
} }
...@@ -149,7 +157,8 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) { ...@@ -149,7 +157,8 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
L1URL: ctx.GlobalString(flags.L1NodeAddr.Name), L1URL: ctx.GlobalString(flags.L1NodeAddr.Name),
L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name), L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name),
L1RPCKind: sources.RPCProviderKind(ctx.GlobalString(flags.L1RPCProviderKind.Name)), L1RPCKind: sources.RPCProviderKind(ctx.GlobalString(flags.L1RPCProviderKind.Name)),
ExecCmd: ctx.String(flags.Exec.Name), ExecCmd: ctx.GlobalString(flags.Exec.Name),
ServerMode: ctx.GlobalBool(flags.Server.Name),
}, nil }, nil
} }
......
...@@ -142,6 +142,14 @@ func TestRequireDataDirInNonFetchingMode(t *testing.T) { ...@@ -142,6 +142,14 @@ func TestRequireDataDirInNonFetchingMode(t *testing.T) {
require.ErrorIs(t, err, ErrDataDirRequired) require.ErrorIs(t, err, ErrDataDirRequired)
} }
func TestRejectExecAndServerMode(t *testing.T) {
cfg := validConfig()
cfg.ServerMode = true
cfg.ExecCmd = "echo"
err := cfg.Check()
require.ErrorIs(t, err, ErrNoExecInServerMode)
}
func validConfig() *Config { func validConfig() *Config {
cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2Claim, validL2ClaimBlockNum) cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2Claim, validL2ClaimBlockNum)
cfg.DataDir = "/tmp/configTest" cfg.DataDir = "/tmp/configTest"
......
...@@ -86,6 +86,11 @@ var ( ...@@ -86,6 +86,11 @@ var (
Usage: "Run the specified client program as a separate process detached from the host. Default is to run the client program in the host process.", Usage: "Run the specified client program as a separate process detached from the host. Default is to run the client program in the host process.",
EnvVar: service.PrefixEnvVar(envVarPrefix, "EXEC"), EnvVar: service.PrefixEnvVar(envVarPrefix, "EXEC"),
} }
Server = cli.BoolFlag{
Name: "server",
Usage: "Run in pre-image server mode without executing any client program.",
EnvVar: service.PrefixEnvVar(envVarPrefix, "SERVER"),
}
) )
// Flags contains the list of configuration options available to the binary. // Flags contains the list of configuration options available to the binary.
...@@ -107,6 +112,7 @@ var programFlags = []cli.Flag{ ...@@ -107,6 +112,7 @@ var programFlags = []cli.Flag{
L1TrustRPC, L1TrustRPC,
L1RPCProviderKind, L1RPCProviderKind,
Exec, Exec,
Server,
} }
func init() { func init() {
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
cl "github.com/ethereum-optimism/optimism/op-program/client" cl "github.com/ethereum-optimism/optimism/op-program/client"
"github.com/ethereum-optimism/optimism/op-program/client/driver"
"github.com/ethereum-optimism/optimism/op-program/host/config" "github.com/ethereum-optimism/optimism/op-program/host/config"
"github.com/ethereum-optimism/optimism/op-program/host/kvstore" "github.com/ethereum-optimism/optimism/op-program/host/kvstore"
"github.com/ethereum-optimism/optimism/op-program/host/prefetcher" "github.com/ethereum-optimism/optimism/op-program/host/prefetcher"
...@@ -27,56 +28,36 @@ type L2Source struct { ...@@ -27,56 +28,36 @@ type L2Source struct {
*sources.DebugClient *sources.DebugClient
} }
// FaultProofProgram is the programmatic entry-point for the fault proof program func Main(logger log.Logger, cfg *config.Config) error {
func FaultProofProgram(logger log.Logger, cfg *config.Config) error {
if err := cfg.Check(); err != nil { if err := cfg.Check(); err != nil {
return fmt.Errorf("invalid config: %w", err) return fmt.Errorf("invalid config: %w", err)
} }
cfg.Rollup.LogDescription(logger, chaincfg.L2ChainIDToNetworkName) cfg.Rollup.LogDescription(logger, chaincfg.L2ChainIDToNetworkName)
ctx := context.Background() ctx := context.Background()
var kv kvstore.KV if cfg.ServerMode {
if cfg.DataDir == "" { preimageChan := cl.CreatePreimageChannel()
logger.Info("Using in-memory storage") hinterChan := cl.CreateHinterChannel()
kv = kvstore.NewMemKV() return PreimageServer(ctx, logger, cfg, preimageChan, hinterChan)
} else {
logger.Info("Creating disk storage", "datadir", cfg.DataDir)
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
return fmt.Errorf("creating datadir: %w", err)
}
kv = kvstore.NewDiskKV(cfg.DataDir)
} }
var ( if err := FaultProofProgram(ctx, logger, cfg); errors.Is(err, driver.ErrClaimNotValid) {
getPreimage func(key common.Hash) ([]byte, error) log.Crit("Claim is invalid", "err", err)
hinter func(hint string) error } else if err != nil {
) return err
if cfg.FetchingEnabled() {
prefetch, err := makePrefetcher(ctx, logger, kv, cfg)
if err != nil {
return fmt.Errorf("failed to create prefetcher: %w", err)
}
getPreimage = func(key common.Hash) ([]byte, error) { return prefetch.GetPreimage(ctx, key) }
hinter = prefetch.Hint
} else { } else {
logger.Info("Using offline mode. All required pre-images must be pre-populated.") log.Info("Claim successfully verified")
getPreimage = kv.Get
hinter = func(hint string) error {
logger.Debug("ignoring prefetch hint", "hint", hint)
return nil
}
} }
return nil
}
localPreimageSource := kvstore.NewLocalPreimageSource(cfg) // FaultProofProgram is the programmatic entry-point for the fault proof program
splitter := kvstore.NewPreimageSourceSplitter(localPreimageSource.Get, getPreimage) func FaultProofProgram(ctx context.Context, logger log.Logger, cfg *config.Config) error {
// Setup client I/O for preimage oracle interaction // Setup client I/O for preimage oracle interaction
pClientRW, pHostRW, err := oppio.CreateBidirectionalChannel() pClientRW, pHostRW, err := oppio.CreateBidirectionalChannel()
if err != nil { if err != nil {
return fmt.Errorf("failed to create preimage pipe: %w", err) return fmt.Errorf("failed to create preimage pipe: %w", err)
} }
oracleServer := preimage.NewOracleServer(pHostRW)
launchOracleServer(logger, oracleServer, splitter.Get)
defer pHostRW.Close() defer pHostRW.Close()
// Setup client I/O for hint comms // Setup client I/O for hint comms
...@@ -84,9 +65,15 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error { ...@@ -84,9 +65,15 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to create hints pipe: %w", err) return fmt.Errorf("failed to create hints pipe: %w", err)
} }
defer hHostRW.Close()
hHost := preimage.NewHintReader(hHostRW) go func() {
routeHints(logger, hHost, hinter) defer hHostRW.Close()
err := PreimageServer(ctx, logger, cfg, pHostRW, hHostRW)
if err != nil {
logger.Error("preimage server failed", "err", err)
}
logger.Debug("Preimage server stopped")
}()
var cmd *exec.Cmd var cmd *exec.Cmd
if cfg.ExecCmd != "" { if cfg.ExecCmd != "" {
...@@ -106,12 +93,61 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error { ...@@ -106,12 +93,61 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error {
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
return fmt.Errorf("failed to wait for child program: %w", err) return fmt.Errorf("failed to wait for child program: %w", err)
} }
logger.Debug("Client program completed successfully")
return nil return nil
} else { } else {
return cl.RunProgram(logger, pClientRW, hClientRW) return cl.RunProgram(logger, pClientRW, hClientRW)
} }
} }
func PreimageServer(ctx context.Context, logger log.Logger, cfg *config.Config, preimageChannel oppio.FileChannel, hintChannel oppio.FileChannel) error {
logger.Info("Starting preimage server")
var kv kvstore.KV
if cfg.DataDir == "" {
logger.Info("Using in-memory storage")
kv = kvstore.NewMemKV()
} else {
logger.Info("Creating disk storage", "datadir", cfg.DataDir)
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
return fmt.Errorf("creating datadir: %w", err)
}
kv = kvstore.NewDiskKV(cfg.DataDir)
}
var (
getPreimage kvstore.PreimageSource
hinter preimage.HintHandler
)
if cfg.FetchingEnabled() {
prefetch, err := makePrefetcher(ctx, logger, kv, cfg)
if err != nil {
return fmt.Errorf("failed to create prefetcher: %w", err)
}
getPreimage = func(key common.Hash) ([]byte, error) { return prefetch.GetPreimage(ctx, key) }
hinter = prefetch.Hint
} else {
logger.Info("Using offline mode. All required pre-images must be pre-populated.")
getPreimage = kv.Get
hinter = func(hint string) error {
logger.Debug("ignoring prefetch hint", "hint", hint)
return nil
}
}
localPreimageSource := kvstore.NewLocalPreimageSource(cfg)
splitter := kvstore.NewPreimageSourceSplitter(localPreimageSource.Get, getPreimage)
preimageGetter := splitter.Get
serverDone := launchOracleServer(logger, preimageChannel, preimageGetter)
hinterDone := routeHints(logger, hintChannel, hinter)
select {
case err := <-serverDone:
return err
case err := <-hinterDone:
return err
}
}
func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg *config.Config) (*prefetcher.Prefetcher, error) { func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg *config.Config) (*prefetcher.Prefetcher, error) {
logger.Info("Connecting to L1 node", "l1", cfg.L1URL) logger.Info("Connecting to L1 node", "l1", cfg.L1URL)
l1RPC, err := client.NewRPC(ctx, logger, cfg.L1URL) l1RPC, err := client.NewRPC(ctx, logger, cfg.L1URL)
...@@ -139,8 +175,11 @@ func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg * ...@@ -139,8 +175,11 @@ func makePrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV, cfg *
return prefetcher.NewPrefetcher(logger, l1Cl, l2DebugCl, kv), nil return prefetcher.NewPrefetcher(logger, l1Cl, l2DebugCl, kv), nil
} }
func routeHints(logger log.Logger, hintReader *preimage.HintReader, hinter func(hint string) error) { func routeHints(logger log.Logger, hHostRW io.ReadWriter, hinter preimage.HintHandler) chan error {
chErr := make(chan error)
hintReader := preimage.NewHintReader(hHostRW)
go func() { go func() {
defer close(chErr)
for { for {
if err := hintReader.NextHint(hinter); err != nil { if err := hintReader.NextHint(hinter); err != nil {
if err == io.EOF || errors.Is(err, fs.ErrClosed) { if err == io.EOF || errors.Is(err, fs.ErrClosed) {
...@@ -148,14 +187,19 @@ func routeHints(logger log.Logger, hintReader *preimage.HintReader, hinter func( ...@@ -148,14 +187,19 @@ func routeHints(logger log.Logger, hintReader *preimage.HintReader, hinter func(
return return
} }
logger.Error("pre-image hint router error", "err", err) logger.Error("pre-image hint router error", "err", err)
chErr <- err
return return
} }
} }
}() }()
return chErr
} }
func launchOracleServer(logger log.Logger, server *preimage.OracleServer, getter func(key common.Hash) ([]byte, error)) { func launchOracleServer(logger log.Logger, pHostRW io.ReadWriteCloser, getter preimage.PreimageGetter) chan error {
chErr := make(chan error)
server := preimage.NewOracleServer(pHostRW)
go func() { go func() {
defer close(chErr)
for { for {
if err := server.NextPreimageRequest(getter); err != nil { if err := server.NextPreimageRequest(getter); err != nil {
if err == io.EOF || errors.Is(err, fs.ErrClosed) { if err == io.EOF || errors.Is(err, fs.ErrClosed) {
...@@ -163,8 +207,10 @@ func launchOracleServer(logger log.Logger, server *preimage.OracleServer, getter ...@@ -163,8 +207,10 @@ func launchOracleServer(logger log.Logger, server *preimage.OracleServer, getter
return return
} }
logger.Error("pre-image server error", "error", err) logger.Error("pre-image server error", "error", err)
chErr <- err
return return
} }
} }
}() }()
return chErr
} }
package host
import (
"context"
"errors"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-program/client"
"github.com/ethereum-optimism/optimism/op-program/client/l1"
"github.com/ethereum-optimism/optimism/op-program/host/config"
"github.com/ethereum-optimism/optimism/op-program/host/kvstore"
"github.com/ethereum-optimism/optimism/op-program/io"
"github.com/ethereum-optimism/optimism/op-program/preimage"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
func TestServerMode(t *testing.T) {
dir := t.TempDir()
l1Head := common.Hash{0x11}
cfg := config.NewConfig(&chaincfg.Goerli, config.OPGoerliChainConfig, l1Head, common.Hash{0x22}, common.Hash{0x33}, 1000)
cfg.DataDir = dir
cfg.ServerMode = true
preimageServer, preimageClient, err := io.CreateBidirectionalChannel()
require.NoError(t, err)
defer preimageClient.Close()
hintServer, hintClient, err := io.CreateBidirectionalChannel()
require.NoError(t, err)
defer hintClient.Close()
logger := testlog.Logger(t, log.LvlTrace)
result := make(chan error)
go func() {
result <- PreimageServer(context.Background(), logger, cfg, preimageServer, hintServer)
}()
pClient := preimage.NewOracleClient(preimageClient)
hClient := preimage.NewHintWriter(hintClient)
l1PreimageOracle := l1.NewPreimageOracle(pClient, hClient)
require.Equal(t, l1Head.Bytes(), pClient.Get(client.L1HeadLocalIndex), "Should get preimages")
// Should exit when a preimage is unavailable
require.Panics(t, func() {
l1PreimageOracle.HeaderByBlockHash(common.HexToHash("0x1234"))
}, "Preimage should not be available")
require.ErrorIs(t, waitFor(result), kvstore.ErrNotFound)
}
func waitFor(ch chan error) error {
timeout := time.After(30 * time.Second)
select {
case err := <-ch:
return err
case <-timeout:
return errors.New("timed out")
}
}
...@@ -43,7 +43,9 @@ func NewHintReader(rw io.ReadWriter) *HintReader { ...@@ -43,7 +43,9 @@ func NewHintReader(rw io.ReadWriter) *HintReader {
return &HintReader{rw: rw} return &HintReader{rw: rw}
} }
func (hr *HintReader) NextHint(router func(hint string) error) error { type HintHandler func(hint string) error
func (hr *HintReader) NextHint(router HintHandler) error {
var length uint32 var length uint32
if err := binary.Read(hr.rw, binary.BigEndian, &length); err != nil { if err := binary.Read(hr.rw, binary.BigEndian, &length); err != nil {
if err == io.EOF { if err == io.EOF {
......
...@@ -47,7 +47,9 @@ func NewOracleServer(rw io.ReadWriter) *OracleServer { ...@@ -47,7 +47,9 @@ func NewOracleServer(rw io.ReadWriter) *OracleServer {
return &OracleServer{rw: rw} return &OracleServer{rw: rw}
} }
func (o *OracleServer) NextPreimageRequest(getPreimage func(key common.Hash) ([]byte, error)) error { type PreimageGetter func(key common.Hash) ([]byte, error)
func (o *OracleServer) NextPreimageRequest(getPreimage PreimageGetter) error {
var key common.Hash var key common.Hash
if _, err := io.ReadFull(o.rw, key[:]); err != nil { if _, err := io.ReadFull(o.rw, key[:]); err != nil {
if err == io.EOF { if err == io.EOF {
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { console } from "forge-std/console.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
import { LibSort } from "../libraries/LibSort.sol";
import { IGnosisSafe, Enum } from "../interfaces/IGnosisSafe.sol";
import { SafeBuilder } from "../universal/SafeBuilder.sol";
import { Types } from "../../contracts/libraries/Types.sol";
import { FeeVault } from "../../contracts/universal/FeeVault.sol";
import { L2OutputOracle } from "../../contracts/L1/L2OutputOracle.sol";
import { Predeploys } from "../../contracts/libraries/Predeploys.sol";
/**
* @title DeleteOutput
* @notice Deletes an output root from the L2OutputOracle.
* @notice Example usage is provided in the README documentation.
*/
contract DeleteOutput is SafeBuilder {
/**
* @notice A set of contract addresses for the script.
*/
struct ContractSet {
address Safe;
address ProxyAdmin;
address L2OutputOracleProxy;
}
/**
* @notice A mapping of chainid to a ContractSet.
*/
mapping(uint256 => ContractSet) internal _contracts;
/**
* @notice The l2 output index we will delete.
*/
uint256 internal index;
/**
* @notice The address of the L2OutputOracle to target.
*/
address internal oracle;
/**
* @notice Place the contract addresses in storage for ux.
*/
function setUp() external {
_contracts[GOERLI] = ContractSet({
Safe: 0xBc1233d0C3e6B5d53Ab455cF65A6623F6dCd7e4f,
ProxyAdmin: 0x01d3670863c3F4b24D7b107900f0b75d4BbC6e0d,
L2OutputOracleProxy: 0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0
});
}
/**
* @notice Returns the ContractSet for the defined block chainid.
*
* @dev Reverts if no ContractSet is defined.
*/
function contracts() public view returns (ContractSet memory) {
ContractSet memory cs = _contracts[block.chainid];
if (cs.Safe == address(0) || cs.ProxyAdmin == address(0) || cs.L2OutputOracleProxy == address(0)) {
revert("Missing Contract Set for the given block.chainid");
}
return cs;
}
/**
* @notice Executes the gnosis safe transaction to delete an L2 Output Root.
*/
function run(uint256 _index) external returns (bool) {
address _safe = contracts().Safe;
address _proxyAdmin = contracts().ProxyAdmin;
index = _index;
return run(_safe, _proxyAdmin);
}
/**
* @notice Follow up assertions to ensure that the script ran to completion.
*/
function _postCheck() internal view override {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
Types.OutputProposal memory proposal = l2oo.getL2Output(index);
require(proposal.l2BlockNumber == 0, "DeleteOutput: Output deletion failed.");
}
/**
* @notice Test coverage of the script.
*/
function test_script_succeeds() skipWhenNotForking external {
uint256 _index = getLatestIndex();
require(_index != 0, "DeleteOutput: No outputs to delete.");
index = _index;
address safe = contracts().Safe;
require(safe != address(0), "DeleteOutput: Invalid safe address.");
address proxyAdmin = contracts().ProxyAdmin;
require(proxyAdmin != address(0), "DeleteOutput: Invalid proxy admin address.");
address[] memory owners = IGnosisSafe(payable(safe)).getOwners();
for (uint256 i; i < owners.length; i++) {
address owner = owners[i];
vm.startBroadcast(owner);
bool success = _run(safe, proxyAdmin);
vm.stopBroadcast();
if (success) {
console.log("tx success");
break;
}
}
_postCheck();
}
function buildCalldata(address _proxyAdmin) internal view override returns (bytes memory) {
IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1);
calls[0] = IMulticall3.Call3({
target: oracle,
allowFailure: false,
callData: abi.encodeCall(
L2OutputOracle.deleteL2Outputs,
(index)
)
});
return abi.encodeCall(IMulticall3.aggregate3, (calls));
}
/**
* @notice Computes the safe transaction hash.
*/
function computeSafeTransactionHash(uint256 _index) public returns (bytes32) {
ContractSet memory cs = contracts();
address _safe = cs.Safe;
address _proxyAdmin = cs.ProxyAdmin;
index = _index;
oracle = cs.L2OutputOracleProxy;
return _getTransactionHash(_safe, _proxyAdmin);
}
/**
* @notice Returns the challenger for the L2OutputOracle.
*/
function getChallenger() public view returns (address) {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
return l2oo.CHALLENGER();
}
/**
* @notice Returns the L2 Block Number for the given index.
*/
function getL2BlockNumber(uint256 _index) public view returns (uint256) {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
return l2oo.getL2Output(_index).l2BlockNumber;
}
/**
* @notice Returns the output root for the given index.
*/
function getOutputFromIndex(uint256 _index) public view returns (bytes32) {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
return l2oo.getL2Output(_index).outputRoot;
}
/**
* @notice Returns the output root with the corresponding to the L2 Block Number.
*/
function getOutputFromL2BlockNumber(uint256 l2BlockNumber) public view returns (bytes32) {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
return l2oo.getL2OutputAfter(l2BlockNumber).outputRoot;
}
/**
* @notice Returns the latest l2 output index.
*/
function getLatestIndex() public view returns (uint256) {
L2OutputOracle l2oo = L2OutputOracle(contracts().L2OutputOracleProxy);
return l2oo.latestOutputIndex();
}
}
## L2 Output Oracle Scripts
A collection of scripts to interact with the L2OutputOracle.
### Output Deletion
[DeleteOutput](./DeleteOutput.s.sol) contains a variety of functions that deal
with deleting an output root from the [L2OutputOracle](../../contracts/L1/L2OutputOracle.sol).
To delete an output root, the script can be run as follows, where `<L2_OUTPUT_INDEX>` is
the index of the posted output to delete.
```bash
$ forge script scripts/output/DeleteOutput.s.sol \
--sig "run(uint256)" \
--rpc-url $ETH_RPC_URL \
--broadcast \
--private-key $PRIVATE_KEY \
<L2_OUTPUT_INDEX>
```
To find and confirm the output index, there are a variety of helper functions that
can be run using the script `--sig` flag, passing the function signatures in as arguments.
These are outlined below.
### Retrieving an L2 Block Number
The output's associated L2 block number can be retrieved using the following command, where
`<L2_OUTPUT_INDEX>` is the index of the output in the [L2OutputOracle](../../contracts/L1/L2OutputOracle.sol).
```bash
$ forge script scripts/output/DeleteOutput.s.sol \
--sig "getL2BlockNumber(uint256)" \
--rpc-url $ETH_RPC_URL \
<L2_OUTPUT_INDEX>
```
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { console } from "forge-std/console.sol";
import { Script } from "forge-std/Script.sol";
import { Semver } from "../../contracts/universal/Semver.sol";
/**
* @title EnhancedScript
* @notice Enhances forge-std' Script.sol with some additional application-specific functionality.
* Logs simulation links using Tenderly.
*/
abstract contract EnhancedScript is Script {
/**
* @notice Helper function used to compute the hash of Semver's version string to be used in a
* comparison.
*/
function _versionHash(address _addr) internal view returns (bytes32) {
return keccak256(bytes(Semver(_addr).version()));
}
/**
* @notice Log a tenderly simulation link. The TENDERLY_USERNAME and TENDERLY_PROJECT
* environment variables will be used if they are present. The vm is staticcall'ed
* because of a compiler issue with the higher level ABI.
*/
function logSimulationLink(address _to, bytes memory _data, address _from) public view {
(, bytes memory projData) = VM_ADDRESS.staticcall(
abi.encodeWithSignature("envOr(string,string)", "TENDERLY_PROJECT", "TENDERLY_PROJECT")
);
string memory proj = abi.decode(projData, (string));
(, bytes memory userData) = VM_ADDRESS.staticcall(
abi.encodeWithSignature("envOr(string,string)", "TENDERLY_USERNAME", "TENDERLY_USERNAME")
);
string memory username = abi.decode(userData, (string));
string memory str = string.concat(
"https://dashboard.tenderly.co/",
username,
"/",
proj,
"/simulator/new?network=",
vm.toString(block.chainid),
"&contractAddress=",
vm.toString(_to),
"&rawFunctionInput=",
vm.toString(_data),
"&from=",
vm.toString(_from)
);
console.log(str);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/**
* @title GlobalConstants
* @notice A set of constants.
*/
contract GlobalConstants {
/**
* @notice Mainnet chain id.
*/
uint256 constant MAINNET = 1;
/**
* @notice Goerli chain id.
*/
uint256 constant GOERLI = 5;
/**
* @notice Optimism Goerli chain id.
*/
uint256 constant OP_GOERLI = 420;
}
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
pragma solidity 0.8.15; pragma solidity 0.8.15;
import { console } from "forge-std/console.sol"; import { console } from "forge-std/console.sol";
import { Script } from "forge-std/Script.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
import { IGnosisSafe, Enum } from "./IGnosisSafe.sol";
import { LibSort } from "./LibSort.sol"; import { LibSort } from "../libraries/LibSort.sol";
import { Semver } from "../../contracts/universal/Semver.sol"; import { IGnosisSafe, Enum } from "../interfaces/IGnosisSafe.sol";
import { EnhancedScript } from "../universal/EnhancedScript.sol";
import { GlobalConstants } from "../universal/GlobalConstants.sol";
import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol"; import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol";
/** /**
...@@ -21,22 +22,7 @@ import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol"; ...@@ -21,22 +22,7 @@ import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol";
* for the most simple user experience when using automation and no indexer. * for the most simple user experience when using automation and no indexer.
* Run the command without the `--broadcast` flag and it will print a tenderly URL. * Run the command without the `--broadcast` flag and it will print a tenderly URL.
*/ */
abstract contract SafeBuilder is Script { abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
/**
* @notice Mainnet chain id.
*/
uint256 constant MAINNET = 1;
/**
* @notice Goerli chain id.
*/
uint256 constant GOERLI = 5;
/**
* @notice Optimism Goerli chain id.
*/
uint256 constant OP_GOERLI = 420;
/** /**
* @notice Interface for multicall3. * @notice Interface for multicall3.
*/ */
...@@ -50,7 +36,7 @@ abstract contract SafeBuilder is Script { ...@@ -50,7 +36,7 @@ abstract contract SafeBuilder is Script {
/** /**
* @notice The entrypoint to this script. * @notice The entrypoint to this script.
*/ */
function run(address _safe, address _proxyAdmin) external returns (bool) { function run(address _safe, address _proxyAdmin) public returns (bool) {
vm.startBroadcast(); vm.startBroadcast();
bool success = _run(_safe, _proxyAdmin); bool success = _run(_safe, _proxyAdmin);
if (success) _postCheck(); if (success) _postCheck();
...@@ -58,12 +44,19 @@ abstract contract SafeBuilder is Script { ...@@ -58,12 +44,19 @@ abstract contract SafeBuilder is Script {
} }
/** /**
* @notice The implementation of the upgrade. Split into its own function * @notice Follow up assertions to ensure that the script ran to completion.
* to allow for testability. This is subject to a race condition if
* the nonce changes by a different transaction finalizing while not
* all of the signers have used this script.
*/ */
function _run(address _safe, address _proxyAdmin) public returns (bool) { function _postCheck() internal virtual view;
/**
* @notice Creates the calldata
*/
function buildCalldata(address _proxyAdmin) internal virtual view returns (bytes memory);
/**
* @notice Internal helper function to compute the safe transaction hash.
*/
function _getTransactionHash(address _safe, address _proxyAdmin) internal returns (bytes32) {
// Ensure that the required contracts exist // Ensure that the required contracts exist
require(address(multicall).code.length > 0, "multicall3 not deployed"); require(address(multicall).code.length > 0, "multicall3 not deployed");
require(_safe.code.length > 0, "no code at safe address"); require(_safe.code.length > 0, "no code at safe address");
...@@ -88,6 +81,23 @@ abstract contract SafeBuilder is Script { ...@@ -88,6 +81,23 @@ abstract contract SafeBuilder is Script {
_nonce: nonce _nonce: nonce
}); });
return hash;
}
/**
* @notice The implementation of the upgrade. Split into its own function
* to allow for testability. This is subject to a race condition if
* the nonce changes by a different transaction finalizing while not
* all of the signers have used this script.
*/
function _run(address _safe, address _proxyAdmin) public returns (bool) {
IGnosisSafe safe = IGnosisSafe(payable(_safe));
bytes memory data = buildCalldata(_proxyAdmin);
// Compute the safe transaction hash
bytes32 hash = _getTransactionHash(_safe, _proxyAdmin);
// Send a transaction to approve the hash // Send a transaction to approve the hash
safe.approveHash(hash); safe.approveHash(hash);
...@@ -158,52 +168,6 @@ abstract contract SafeBuilder is Script { ...@@ -158,52 +168,6 @@ abstract contract SafeBuilder is Script {
return false; return false;
} }
/**
* @notice Log a tenderly simulation link. The TENDERLY_USERNAME and TENDERLY_PROJECT
* environment variables will be used if they are present. The vm is staticcall'ed
* because of a compiler issue with the higher level ABI.
*/
function logSimulationLink(address _to, bytes memory _data, address _from) public view {
(, bytes memory projData) = VM_ADDRESS.staticcall(
abi.encodeWithSignature("envOr(string,string)", "TENDERLY_PROJECT", "TENDERLY_PROJECT")
);
string memory proj = abi.decode(projData, (string));
(, bytes memory userData) = VM_ADDRESS.staticcall(
abi.encodeWithSignature("envOr(string,string)", "TENDERLY_USERNAME", "TENDERLY_USERNAME")
);
string memory username = abi.decode(userData, (string));
string memory str = string.concat(
"https://dashboard.tenderly.co/",
username,
"/",
proj,
"/simulator/new?network=",
vm.toString(block.chainid),
"&contractAddress=",
vm.toString(_to),
"&rawFunctionInput=",
vm.toString(_data),
"&from=",
vm.toString(_from)
);
console.log(str);
}
/**
* @notice Follow up assertions to ensure that the script ran to completion.
*/
function _postCheck() internal virtual view;
/**
* @notice Helper function used to compute the hash of Semver's version string to be used in a
* comparison.
*/
function _versionHash(address _addr) internal view returns (bytes32) {
return keccak256(bytes(Semver(_addr).version()));
}
/** /**
* @notice Builds the signatures by tightly packing them together. * @notice Builds the signatures by tightly packing them together.
* Ensures that they are sorted. * Ensures that they are sorted.
...@@ -226,9 +190,5 @@ abstract contract SafeBuilder is Script { ...@@ -226,9 +190,5 @@ abstract contract SafeBuilder is Script {
return signatures; return signatures;
} }
/**
* @notice Creates the calldata
*/
function buildCalldata(address _proxyAdmin) internal virtual view returns (bytes memory);
} }
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
pragma solidity 0.8.15; pragma solidity 0.8.15;
import { console } from "forge-std/console.sol"; import { console } from "forge-std/console.sol";
import { SafeBuilder } from "./SafeBuilder.sol"; import { SafeBuilder } from "../universal/SafeBuilder.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
import { IGnosisSafe, Enum } from "./IGnosisSafe.sol"; import { IGnosisSafe, Enum } from "../interfaces/IGnosisSafe.sol";
import { LibSort } from "./LibSort.sol"; import { LibSort } from "../libraries/LibSort.sol";
import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol"; import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol";
import { Constants } from "../../contracts/libraries/Constants.sol"; import { Constants } from "../../contracts/libraries/Constants.sol";
import { SystemConfig } from "../../contracts/L1/SystemConfig.sol"; import { SystemConfig } from "../../contracts/L1/SystemConfig.sol";
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
pragma solidity 0.8.15; pragma solidity 0.8.15;
import { console } from "forge-std/console.sol"; import { console } from "forge-std/console.sol";
import { SafeBuilder } from "./SafeBuilder.sol"; import { SafeBuilder } from "../universal/SafeBuilder.sol";
import { IGnosisSafe, Enum } from "./IGnosisSafe.sol"; import { IGnosisSafe, Enum } from "../libraries/IGnosisSafe.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol"; import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
import { Predeploys } from "../../contracts/libraries/Predeploys.sol"; import { Predeploys } from "../../contracts/libraries/Predeploys.sol";
import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol"; import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol";
......
...@@ -33,14 +33,37 @@ const checkPredeploys = async ( ...@@ -33,14 +33,37 @@ const checkPredeploys = async (
provider: providers.Provider provider: providers.Provider
) => { ) => {
console.log('Checking predeploys are configured correctly') console.log('Checking predeploys are configured correctly')
const admin = hre.ethers.utils.hexConcat([
'0x000000000000000000000000',
predeploys.ProxyAdmin,
])
const codeReq = []
const slotReq = []
// First loop for requests
for (let i = 0; i < 2048; i++) { for (let i = 0; i < 2048; i++) {
const num = hre.ethers.utils.hexZeroPad('0x' + i.toString(16), 2) const num = hre.ethers.utils.hexZeroPad('0x' + i.toString(16), 2)
const addr = hre.ethers.utils.getAddress( const addr = hre.ethers.utils.getAddress(
hre.ethers.utils.hexConcat([prefix, num]) hre.ethers.utils.hexConcat([prefix, num])
) )
const code = await provider.getCode(addr) codeReq.push(provider.getCode(addr))
if (code === '0x') { slotReq.push(provider.getStorageAt(addr, adminSlot))
}
// Wait for all requests to finish
// The `JsonRpcBatchProvider` will batch requests in the background.
const codeRes = await Promise.all(codeReq)
const slotRes = await Promise.all(slotReq)
// Second loop for response checks
for (let i = 0; i < 2048; i++) {
const num = hre.ethers.utils.hexZeroPad('0x' + i.toString(16), 2)
const addr = hre.ethers.utils.getAddress(
hre.ethers.utils.hexConcat([prefix, num])
)
if (codeRes[i] === '0x') {
throw new Error(`no code found at ${addr}`) throw new Error(`no code found at ${addr}`)
} }
...@@ -52,13 +75,7 @@ const checkPredeploys = async ( ...@@ -52,13 +75,7 @@ const checkPredeploys = async (
continue continue
} }
const slot = await provider.getStorageAt(addr, adminSlot) if (slotRes[i] !== admin) {
const admin = hre.ethers.utils.hexConcat([
'0x000000000000000000000000',
predeploys.ProxyAdmin,
])
if (admin !== slot) {
throw new Error(`incorrect admin slot in ${addr}`) throw new Error(`incorrect admin slot in ${addr}`)
} }
...@@ -686,7 +703,9 @@ task('check-l2', 'Checks a freshly migrated L2 system for correct migration') ...@@ -686,7 +703,9 @@ task('check-l2', 'Checks a freshly migrated L2 system for correct migration')
if (args.l2RpcUrl !== '') { if (args.l2RpcUrl !== '') {
console.log('Using CLI URL for provider instead of hardhat network') console.log('Using CLI URL for provider instead of hardhat network')
const provider = new hre.ethers.providers.JsonRpcProvider(args.l2RpcUrl) const provider = new hre.ethers.providers.JsonRpcBatchProvider(
args.l2RpcUrl
)
signer = Wallet.createRandom().connect(provider) signer = Wallet.createRandom().connect(provider)
} }
......
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