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

Merge branch 'develop' into extradata

parents cedeb4ec 71aadcfc
......@@ -27,11 +27,9 @@ require (
github.com/multiformats/go-multiaddr-dns v0.3.1
github.com/olekukonko/tablewriter v0.0.5
github.com/prometheus/client_golang v1.14.0
github.com/schollz/progressbar/v3 v3.13.0
github.com/stretchr/testify v1.8.1
github.com/urfave/cli v1.22.9
github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa
golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/sync v0.1.0
golang.org/x/term v0.6.0
......@@ -120,7 +118,6 @@ require (
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
......@@ -167,6 +164,7 @@ require (
go.uber.org/fx v1.19.1 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
......
......@@ -360,7 +360,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
......@@ -474,8 +473,6 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
......@@ -610,8 +607,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=
github.com/schollz/progressbar/v3 v3.13.0/go.mod h1:ZBYnSuLAX2LU8P8UiKN/KgF2DY58AJC8yfVYLPC8Ly4=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
......@@ -867,13 +862,11 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
......
......@@ -5,23 +5,27 @@ import (
"net/http/httptest"
"testing"
"github.com/google/uuid"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
// MockBridgeView mocks the BridgeView interface
type MockBridgeView struct{}
const (
guid1 = "8408b6d2-7c90-4cfc-8604-b2204116cb6a"
guid2 = "8408b6d2-7c90-4cfc-8604-b2204116cb6b"
)
// DepositsByAddress mocks returning deposits by an address
func (mbv *MockBridgeView) DepositsByAddress(address common.Address) ([]*database.DepositWithTransactionHash, error) {
return []*database.DepositWithTransactionHash{
{
Deposit: database.Deposit{
GUID: uuid.New(),
InitiatedL1EventGUID: "mockEventGUID1",
GUID: uuid.MustParse(guid1),
InitiatedL1EventGUID: guid2,
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
},
......@@ -35,8 +39,8 @@ func (mbv *MockBridgeView) WithdrawalsByAddress(address common.Address) ([]*data
return []*database.WithdrawalWithTransactionHashes{
{
Withdrawal: database.Withdrawal{
GUID: uuid.New(),
InitiatedL2EventGUID: "mockEventGUID2",
GUID: uuid.MustParse(guid2),
InitiatedL2EventGUID: guid1,
WithdrawalHash: common.HexToHash("0x456"),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
......
FROM golang:1.19.9-alpine3.15 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./op-chain-ops /app/op-chain-ops
COPY ./op-bindings /app/op-bindings
COPY ./op-node /app/op-node
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./.git /app/.git
WORKDIR /app/op-chain-ops
RUN make op-migrate
FROM alpine:3.15
COPY --from=builder /app/op-chain-ops/bin/op-migrate /usr/local/bin
ENTRYPOINT ["op-migrate"]
package main
import (
"context"
"fmt"
"math/big"
"os"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/mattn/go-isatty"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/hardhat"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
)
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := &cli.App{
Name: "check-migration",
Usage: "Run sanity checks on a migrated database",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l1-rpc-url",
Value: "http://127.0.0.1:8545",
Usage: "RPC URL for an L1 Node",
Required: true,
},
&cli.StringFlag{
Name: "ovm-addresses",
Usage: "Path to ovm-addresses.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-allowances",
Usage: "Path to ovm-allowances.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-messages",
Usage: "Path to ovm-messages.json",
Required: true,
},
&cli.StringFlag{
Name: "witness-file",
Usage: "Path to witness file",
Required: true,
},
&cli.StringFlag{
Name: "db-path",
Usage: "Path to database",
Required: true,
},
cli.StringFlag{
Name: "deploy-config",
Usage: "Path to hardhat deploy config file",
Required: true,
},
cli.StringFlag{
Name: "network",
Usage: "Name of hardhat deploy network",
Required: true,
},
cli.StringFlag{
Name: "hardhat-deployments",
Usage: "Comma separated list of hardhat deployment directories",
Required: true,
},
cli.IntFlag{
Name: "db-cache",
Usage: "LevelDB cache size in mb",
Value: 1024,
},
cli.IntFlag{
Name: "db-handles",
Usage: "LevelDB number of handles",
Value: 60,
},
},
Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config")
config, err := genesis.NewDeployConfig(deployConfig)
if err != nil {
return err
}
ovmAddresses, err := crossdomain.NewAddresses(ctx.String("ovm-addresses"))
if err != nil {
return err
}
ovmAllowances, err := crossdomain.NewAllowances(ctx.String("ovm-allowances"))
if err != nil {
return err
}
ovmMessages, err := crossdomain.NewSentMessageFromJSON(ctx.String("ovm-messages"))
if err != nil {
return err
}
evmMessages, evmAddresses, err := crossdomain.ReadWitnessData(ctx.String("witness-file"))
if err != nil {
return err
}
log.Info(
"Loaded witness data",
"ovmAddresses", len(ovmAddresses),
"evmAddresses", len(evmAddresses),
"ovmAllowances", len(ovmAllowances),
"ovmMessages", len(ovmMessages),
"evmMessages", len(evmMessages),
)
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresses,
OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
network := ctx.String("network")
deployments := strings.Split(ctx.String("hardhat-deployments"), ",")
hh, err := hardhat.New(network, []string{}, deployments)
if err != nil {
return err
}
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return err
}
var block *types.Block
tag := config.L1StartingBlockTag
if tag.BlockNumber != nil {
block, err = l1Client.BlockByNumber(context.Background(), big.NewInt(tag.BlockNumber.Int64()))
} else if tag.BlockHash != nil {
block, err = l1Client.BlockByHash(context.Background(), *tag.BlockHash)
} else {
return fmt.Errorf("invalid l1StartingBlockTag in deploy config: %v", tag)
}
if err != nil {
return err
}
dbCache := ctx.Int("db-cache")
dbHandles := ctx.Int("db-handles")
// Read the required deployment addresses from disk if required
if err := config.GetDeployedAddresses(hh); err != nil {
return err
}
if err := config.Check(); err != nil {
return err
}
postLDB, err := db.Open(ctx.String("db-path"), dbCache, dbHandles)
if err != nil {
return err
}
if err := genesis.PostCheckMigratedDB(
postLDB,
migrationData,
&config.L1CrossDomainMessengerProxy,
config.L1ChainID,
config.L2ChainID,
config.FinalSystemOwner,
config.ProxyAdminOwner,
&derive.L1BlockInfo{
Number: block.NumberU64(),
Time: block.Time(),
BaseFee: block.BaseFee(),
BlockHash: block.Hash(),
BatcherAddr: config.BatchSenderAddress,
L1FeeOverhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleOverhead))),
L1FeeScalar: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleScalar))),
},
); err != nil {
return err
}
if err := postLDB.Close(); err != nil {
return err
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error in migration", "err", err)
}
}
package main
import (
"fmt"
"os"
"github.com/mattn/go-isatty"
"github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/ethereum-optimism/optimism/op-chain-ops/ether"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/log"
"github.com/schollz/progressbar/v3"
"github.com/urfave/cli"
)
func main() {
lvlHdlr := log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd())))
log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, lvlHdlr))
app := &cli.App{
Name: "inject-mints",
Usage: "Injects mints into l2geth witness data",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "db-path",
Usage: "Path to database",
Required: true,
},
&cli.StringFlag{
Name: "witness-file-out",
Usage: "Path to the witness file",
Required: true,
},
cli.IntFlag{
Name: "db-cache",
Usage: "LevelDB cache size in mb",
Value: 1024,
},
cli.IntFlag{
Name: "db-handles",
Usage: "LevelDB number of handles",
Value: 60,
},
},
Action: func(ctx *cli.Context) error {
ldb, err := db.Open(ctx.String("db-path"), ctx.Int("db-cache"), ctx.Int("db-handles"))
if err != nil {
return fmt.Errorf("error opening db: %w", err)
}
defer ldb.Close()
f, err := os.OpenFile(ctx.String("witness-file-out"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("error opening witness file: %w", err)
}
log.Info("Reading mint events from DB")
headBlock := rawdb.ReadHeadBlock(ldb)
headNum := headBlock.NumberU64()
seenAddrs := make(map[common.Address]bool)
bar := progressbar.Default(int64(headNum))
var count uint64
progressCb := func(headNum uint64) {
_ = bar.Add(1)
}
err = ether.IterateMintEvents(ldb, headNum, func(address common.Address, headNum uint64) error {
if seenAddrs[address] {
return nil
}
count++
seenAddrs[address] = true
_, err := fmt.Fprintf(f, "ETH|%s\n", address.Hex())
return err
}, progressCb)
if err != nil {
return fmt.Errorf("error iterating mint events: %w", err)
}
log.Info("Done")
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error in inject-mints", "err", err)
}
}
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/mattn/go-isatty"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/hardhat"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
)
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := &cli.App{
Name: "migrate",
Usage: "Migrate a legacy database",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l1-rpc-url",
Value: "http://127.0.0.1:8545",
Usage: "RPC URL for an L1 Node",
Required: true,
},
&cli.StringFlag{
Name: "ovm-addresses",
Usage: "Path to ovm-addresses.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-allowances",
Usage: "Path to ovm-allowances.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-messages",
Usage: "Path to ovm-messages.json",
Required: true,
},
&cli.StringFlag{
Name: "witness-file",
Usage: "Path to witness file",
Required: true,
},
&cli.StringFlag{
Name: "db-path",
Usage: "Path to database",
Required: true,
},
cli.StringFlag{
Name: "deploy-config",
Usage: "Path to hardhat deploy config file",
Required: true,
},
cli.StringFlag{
Name: "network",
Usage: "Name of hardhat deploy network",
Required: true,
},
cli.StringFlag{
Name: "hardhat-deployments",
Usage: "Comma separated list of hardhat deployment directories",
Required: true,
},
cli.BoolFlag{
Name: "dry-run",
Usage: "Dry run the upgrade by not committing the database",
},
cli.BoolFlag{
Name: "no-check",
Usage: "Do not perform sanity checks. This should only be used for testing",
},
cli.IntFlag{
Name: "db-cache",
Usage: "LevelDB cache size in mb",
Value: 1024,
},
cli.IntFlag{
Name: "db-handles",
Usage: "LevelDB number of handles",
Value: 60,
},
cli.StringFlag{
Name: "rollup-config-out",
Usage: "Path that op-node config will be written to disk",
Value: "rollup.json",
Required: true,
},
cli.BoolFlag{
Name: "post-check-only",
Usage: "Only perform sanity checks",
Required: false,
},
},
Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config")
config, err := genesis.NewDeployConfig(deployConfig)
if err != nil {
return err
}
ovmAddresses, err := crossdomain.NewAddresses(ctx.String("ovm-addresses"))
if err != nil {
return err
}
ovmAllowances, err := crossdomain.NewAllowances(ctx.String("ovm-allowances"))
if err != nil {
return err
}
ovmMessages, err := crossdomain.NewSentMessageFromJSON(ctx.String("ovm-messages"))
if err != nil {
return err
}
evmMessages, evmAddresses, err := crossdomain.ReadWitnessData(ctx.String("witness-file"))
if err != nil {
return err
}
log.Info(
"Loaded witness data",
"ovmAddresses", len(ovmAddresses),
"evmAddresses", len(evmAddresses),
"ovmAllowances", len(ovmAllowances),
"ovmMessages", len(ovmMessages),
"evmMessages", len(evmMessages),
)
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresses,
OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
network := ctx.String("network")
deployments := strings.Split(ctx.String("hardhat-deployments"), ",")
hh, err := hardhat.New(network, []string{}, deployments)
if err != nil {
return err
}
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return fmt.Errorf("cannot dial L1 client: %w", err)
}
chainId, err := l1Client.ChainID(context.Background())
if err != nil {
return fmt.Errorf("failed to get L1 ChainID: %w", err)
}
log.Info("L1 ChainID", "chainId", chainId)
var block *types.Block
tag := config.L1StartingBlockTag
if tag == nil {
return errors.New("l1StartingBlockTag cannot be nil")
}
log.Info("Using L1 Starting Block Tag", "tag", tag.String())
if number, isNumber := tag.Number(); isNumber {
block, err = l1Client.BlockByNumber(context.Background(), big.NewInt(number.Int64()))
} else if hash, isHash := tag.Hash(); isHash {
block, err = l1Client.BlockByHash(context.Background(), hash)
} else {
return fmt.Errorf("invalid l1StartingBlockTag in deploy config: %v", tag)
}
if err != nil {
return fmt.Errorf("cannot fetch L1 starting block tag: %w", err)
}
dbCache := ctx.Int("db-cache")
dbHandles := ctx.Int("db-handles")
dbPath := ctx.String("db-path")
log.Info("Opening database", "dbCache", dbCache, "dbHandles", dbHandles, "dbPath", dbPath)
ldb, err := db.Open(dbPath, dbCache, dbHandles)
if err != nil {
return fmt.Errorf("cannot open DB: %w", err)
}
// Read the required deployment addresses from disk if required
if err := config.GetDeployedAddresses(hh); err != nil {
return err
}
if err := config.Check(); err != nil {
return err
}
dryRun := ctx.Bool("dry-run")
noCheck := ctx.Bool("no-check")
if noCheck {
panic("must run with check on")
}
// Perform the migration
res, err := genesis.MigrateDB(ldb, config, block, &migrationData, !dryRun, noCheck)
if err != nil {
return err
}
// Close the database handle
if err := ldb.Close(); err != nil {
return err
}
postLDB, err := db.Open(dbPath, dbCache, dbHandles)
if err != nil {
return err
}
if err := genesis.PostCheckMigratedDB(
postLDB,
migrationData,
&config.L1CrossDomainMessengerProxy,
config.L1ChainID,
config.L2ChainID,
config.FinalSystemOwner,
config.ProxyAdminOwner,
&derive.L1BlockInfo{
Number: block.NumberU64(),
Time: block.Time(),
BaseFee: block.BaseFee(),
BlockHash: block.Hash(),
BatcherAddr: config.BatchSenderAddress,
L1FeeOverhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleOverhead))),
L1FeeScalar: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(config.GasPriceOracleScalar))),
},
); err != nil {
return err
}
if err := postLDB.Close(); err != nil {
return err
}
opNodeConfig, err := config.RollupConfig(block, res.TransitionBlockHash, res.TransitionHeight)
if err != nil {
return err
}
if err := writeJSON(ctx.String("rollup-config-out"), opNodeConfig); err != nil {
return err
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error in migration", "err", err)
}
}
func writeJSON(outfile string, input interface{}) error {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(input)
}
This diff is collapsed.
This diff is collapsed.
package crossdomain
import (
"errors"
"fmt"
"math/big"
......@@ -9,16 +8,9 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
var (
abiTrue = common.Hash{31: 0x01}
errLegacyStorageSlotNotFound = errors.New("cannot find storage slot")
)
// Constants used by `CrossDomainMessenger.baseGas`
var (
RelayConstantOverhead uint64 = 200_000
......@@ -30,43 +22,6 @@ var (
RelayGasCheckBuffer uint64 = 5_000
)
// MigrateWithdrawals will migrate a list of pending withdrawals given a StateDB.
func MigrateWithdrawals(
withdrawals SafeFilteredWithdrawals,
db vm.StateDB,
l1CrossDomainMessenger *common.Address,
noCheck bool,
chainID *big.Int,
) error {
for i, legacy := range withdrawals {
legacySlot, err := legacy.StorageSlot()
if err != nil {
return err
}
if !noCheck {
legacyValue := db.GetState(predeploys.LegacyMessagePasserAddr, legacySlot)
if legacyValue != abiTrue {
return fmt.Errorf("%w: %s", errLegacyStorageSlotNotFound, legacySlot)
}
}
withdrawal, err := MigrateWithdrawal(legacy, l1CrossDomainMessenger, chainID)
if err != nil {
return err
}
slot, err := withdrawal.StorageSlot()
if err != nil {
return fmt.Errorf("cannot compute withdrawal storage slot: %w", err)
}
db.SetState(predeploys.L2ToL1MessagePasserAddr, slot, abiTrue)
log.Info("Migrated withdrawal", "number", i, "slot", slot)
}
return nil
}
// MigrateWithdrawal will turn a LegacyWithdrawal into a bedrock
// style Withdrawal.
func MigrateWithdrawal(
......
package crossdomain
import (
"math/big"
)
// Params contains the configuration parameters used for verifying
// the integrity of the migration.
type Params struct {
// ExpectedSupplyDelta is the expected delta between the total supply of OVM ETH,
// and ETH we were able to migrate. This is used to account for supply bugs in
//previous regenesis events.
ExpectedSupplyDelta *big.Int
}
var ParamsByChainID = map[int]*Params{
1: {
// Regenesis 4 (Nov 11 2021) contained a supply bug such that the total OVM ETH
// supply was 1.628470012 ETH greater than the sum balance of every account migrated
// / during the regenesis. A further 0.0012 ETH was incorrectly not removed from the
// total supply by accidental invocations of the Saurik bug (https://www.saurik.com/optimism.html).
new(big.Int).SetUint64(1627270011999999992),
},
5: {
new(big.Int),
},
}
package crossdomain
import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log"
)
var (
ErrUnknownSlotInMessagePasser = errors.New("unknown slot in legacy message passer")
ErrMissingSlotInWitness = errors.New("missing storage slot in witness data (see logs for details)")
)
// PreCheckWithdrawals checks that the given list of withdrawals represents all withdrawals made
// in the legacy system and filters out any extra withdrawals not included in the legacy system.
func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithdrawals, invalidMessages []InvalidMessage) (SafeFilteredWithdrawals, error) {
// Convert each withdrawal into a storage slot, and build a map of those slots.
validSlotsInp := make(map[common.Hash]*LegacyWithdrawal)
for _, wd := range withdrawals {
slot, err := wd.StorageSlot()
if err != nil {
return nil, fmt.Errorf("cannot check withdrawals: %w", err)
}
validSlotsInp[slot] = wd
}
// Convert each invalid message into a storage slot, and build a map of those slots.
invalidSlotsInp := make(map[common.Hash]InvalidMessage)
for _, msg := range invalidMessages {
slot, err := msg.StorageSlot()
if err != nil {
return nil, fmt.Errorf("cannot check invalid messages: %w", err)
}
invalidSlotsInp[slot] = msg
}
// Build a mapping of the slots of all messages actually sent in the legacy system.
var count int
var innerErr error
slotsAct := make(map[common.Hash]bool)
progress := util.ProgressLogger(1000, "Iterating legacy messages")
err := db.ForEachStorage(predeploys.LegacyMessagePasserAddr, func(key, value common.Hash) bool {
progress()
// When a message is inserted into the LegacyMessagePasser, it is stored with the value
// of the ABI encoding of "true". Although there should not be any other storage slots, we
// can safely ignore anything that is not "true".
if value != abiTrue {
// Should not happen!
innerErr = fmt.Errorf("%w: key: %s, val: %s", ErrUnknownSlotInMessagePasser, key.String(), value.String())
return true
}
// Slot exists, so add it to the map.
slotsAct[key] = true
count++
return true
})
if err != nil {
return nil, fmt.Errorf("cannot iterate over LegacyMessagePasser: %w", err)
}
if innerErr != nil {
return nil, innerErr
}
// Log the number of messages we found.
log.Info("Iterated legacy messages", "count", count)
// Iterate over the list of actual slots and check that we have an input message for each one.
var missing int
for slot := range slotsAct {
_, okValid := validSlotsInp[slot]
_, okInvalid := invalidSlotsInp[slot]
if !okValid && !okInvalid {
log.Error("missing storage slot", "slot", slot.String())
missing++
}
}
if missing > 0 {
log.Error("missing storage slots in witness data", "count", missing)
return nil, ErrMissingSlotInWitness
}
// Iterate over the list of input messages and check that we have a known slot for each one.
// We'll filter out any extra messages that are not in the legacy system.
filtered := make(SafeFilteredWithdrawals, 0)
for slot := range validSlotsInp {
_, ok := slotsAct[slot]
if !ok {
log.Info("filtering out unknown input message", "slot", slot.String())
continue
}
wd := validSlotsInp[slot]
if wd.MessageSender != predeploys.L2CrossDomainMessengerAddr {
log.Info("filtering out message from sender other than the L2XDM", "sender", wd.MessageSender)
continue
}
filtered = append(filtered, wd)
}
// At this point, we know that the list of filtered withdrawals MUST be exactly the same as the
// list of withdrawals in the state. If we didn't have enough withdrawals, we would've errored
// out, and if we had too many, we would've filtered them out.
return filtered, nil
}
package crossdomain
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/trie"
"github.com/stretchr/testify/require"
)
func TestPreCheckWithdrawals_Filtering(t *testing.T) {
dbWds := []*LegacyWithdrawal{
// Random legacy WD to something other than the L2XDM.
{
MessageSender: common.Address{19: 0xFF},
XDomainTarget: common.Address{19: 0x01},
XDomainSender: common.Address{19: 0x02},
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(0),
},
// Random legacy WD to the L2XDM. Should be the only thing
// returned by the prechecker.
{
MessageSender: predeploys.L2CrossDomainMessengerAddr,
XDomainTarget: common.Address{19: 0x01},
XDomainSender: common.Address{19: 0x02},
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(1),
},
}
// Add an additional witness to the witnesses list to
// test how the prechecker handles witness data that
// isn't in state.
witnessWds := append([]*LegacyWithdrawal{
{
MessageSender: common.Address{19: 0xAA},
XDomainTarget: common.Address{19: 0x03},
XDomainSender: predeploys.L2CrossDomainMessengerAddr,
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(0),
},
}, dbWds...)
filteredWds, err := runPrecheck(t, dbWds, witnessWds)
require.NoError(t, err)
require.EqualValues(t, []*LegacyWithdrawal{dbWds[1]}, filteredWds)
}
func TestPreCheckWithdrawals_InvalidSlotInStorage(t *testing.T) {
rawDB := rawdb.NewMemoryDatabase()
rawStateDB := state.NewDatabaseWithConfig(rawDB, &trie.Config{
Preimages: true,
Cache: 1024,
})
stateDB, err := state.New(common.Hash{}, rawStateDB, nil)
require.NoError(t, err)
// Create account, and set a random storage slot to a value
// other than abiTrue.
stateDB.CreateAccount(predeploys.LegacyMessagePasserAddr)
stateDB.SetState(predeploys.LegacyMessagePasserAddr, common.Hash{0: 0xff}, common.Hash{0: 0xff})
root, err := stateDB.Commit(false)
require.NoError(t, err)
err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
_, err = PreCheckWithdrawals(stateDB, nil, nil)
require.ErrorIs(t, err, ErrUnknownSlotInMessagePasser)
}
func TestPreCheckWithdrawals_MissingStorageSlot(t *testing.T) {
// Add a legacy WD to state that does not appear in witness data.
dbWds := []*LegacyWithdrawal{
{
XDomainTarget: common.Address{19: 0x01},
XDomainSender: predeploys.L2CrossDomainMessengerAddr,
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(1),
},
}
// Create some witness data that includes both a valid
// and an invalid witness, but neither of which correspond
// to the value above in state.
witnessWds := []*LegacyWithdrawal{
{
XDomainTarget: common.Address{19: 0x01},
XDomainSender: common.Address{19: 0x02},
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(0),
},
{
XDomainTarget: common.Address{19: 0x03},
XDomainSender: predeploys.L2CrossDomainMessengerAddr,
XDomainData: []byte{0x01, 0x02, 0x03},
XDomainNonce: big.NewInt(0),
},
}
_, err := runPrecheck(t, dbWds, witnessWds)
require.ErrorIs(t, err, ErrMissingSlotInWitness)
}
func runPrecheck(t *testing.T, dbWds []*LegacyWithdrawal, witnessWds []*LegacyWithdrawal) ([]*LegacyWithdrawal, error) {
rawDB := rawdb.NewMemoryDatabase()
rawStateDB := state.NewDatabaseWithConfig(rawDB, &trie.Config{
Preimages: true,
Cache: 1024,
})
stateDB, err := state.New(common.Hash{}, rawStateDB, nil)
require.NoError(t, err)
stateDB.CreateAccount(predeploys.LegacyMessagePasserAddr)
for _, wd := range dbWds {
slot, err := wd.StorageSlot()
require.NoError(t, err)
stateDB.SetState(predeploys.LegacyMessagePasserAddr, slot, abiTrue)
}
root, err := stateDB.Commit(false)
require.NoError(t, err)
err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
return PreCheckWithdrawals(stateDB, witnessWds, nil)
}
package crossdomain
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// DangerousUnfilteredWithdrawals is a list of raw withdrawal witness
// data. It has not been filtered for messages from sources other than
// the
type DangerousUnfilteredWithdrawals []*LegacyWithdrawal
// SafeFilteredWithdrawals is a list of withdrawals that have been filtered to only include
// withdrawals that were from the L2XDM.
type SafeFilteredWithdrawals []*LegacyWithdrawal
var (
// Standard ABI types
Uint256Type, _ = abi.NewType("uint256", "", nil)
......@@ -33,35 +21,3 @@ type WithdrawalMessage interface {
Hash() (common.Hash, error)
StorageSlot() (common.Hash, error)
}
// InvalidMessage represents a message to the L1 message passer that
// cannot be decoded as a withdrawal. They are defined as a separate
// type in order to completely disambiguate them from any other
// message.
type InvalidMessage SentMessage
func (msg *InvalidMessage) Encode() ([]byte, error) {
out := make([]byte, len(msg.Msg)+20)
copy(out, msg.Msg)
copy(out[len(msg.Msg):], msg.Who.Bytes())
return out, nil
}
func (msg *InvalidMessage) Hash() (common.Hash, error) {
bytes, err := msg.Encode()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot hash: %w", err)
}
return crypto.Keccak256Hash(bytes), nil
}
func (msg *InvalidMessage) StorageSlot() (common.Hash, error) {
hash, err := msg.Hash()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot compute storage slot: %w", err)
}
preimage := make([]byte, 64)
copy(preimage, hash.Bytes())
return crypto.Keccak256Hash(preimage), nil
}
package crossdomain
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestInvalidMessage(t *testing.T) {
tests := []struct {
name string
msg InvalidMessage
slot common.Hash
}{
{
name: "unparseable x-domain message on mainnet",
msg: InvalidMessage{
Who: common.HexToAddress("0x8b1d477410344785ff1df52500032e6d5f532ee4"),
Msg: common.FromHex("0x042069"),
},
slot: common.HexToHash("0x2a49ae6579c3878f10cf87ecdbebc6c4e2b2159ffe2b1af88af6ca9697fc32cb"),
},
{
name: "valid x-domain message on mainnet for validation",
msg: InvalidMessage{
Who: common.HexToAddress("0x4200000000000000000000000000000000000007"),
Msg: common.FromHex("" +
"0xcbd4ece900000000000000000000000099c9fc46f92e8a1c0dec1b1747d01090" +
"3e884be100000000000000000000000042000000000000000000000000000000" +
"0000001000000000000000000000000000000000000000000000000000000000" +
"0000008000000000000000000000000000000000000000000000000000000000" +
"00019be200000000000000000000000000000000000000000000000000000000" +
"000000e4a9f9e675000000000000000000000000a0b86991c6218b36c1d19d4a" +
"2e9eb0ce3606eb480000000000000000000000007f5c764cbc14f9669b88837c" +
"a1490cca17c31607000000000000000000000000a420b2d1c0841415a695b81e" +
"5b867bcd07dff8c9000000000000000000000000c186fa914353c44b2e33ebe0" +
"5f21846f1048beda000000000000000000000000000000000000000000000000" +
"00000000295d681d000000000000000000000000000000000000000000000000" +
"00000000000000c0000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"00000000",
),
},
slot: common.HexToHash("0x8f8f6be7a4c5048f46ca41897181d17c10c39365ead5ac27c23d1e8e466d0ed5"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// StorageSlot() tests Hash() and Encode() so we don't
// need to test these separately.
slot, err := test.msg.StorageSlot()
require.NoError(t, err)
require.Equal(t, test.slot, slot)
})
}
}
package crossdomain
import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
// A PendingWithdrawal represents a withdrawal that has
// not been finalized on L1
type PendingWithdrawal struct {
LegacyWithdrawal `json:"withdrawal"`
TransactionHash common.Hash `json:"transactionHash"`
}
// Backends represents a set of backends for L1 and L2.
// These are used as the backends for the Messengers
type Backends struct {
L1 bind.ContractBackend
L2 bind.ContractBackend
}
func NewBackends(l1, l2 bind.ContractBackend) *Backends {
return &Backends{
L1: l1,
L2: l2,
}
}
// Messengers represents a pair of L1 and L2 cross domain messengers
// that are connected to the correct contract addresses
type Messengers struct {
L1 *bindings.L1CrossDomainMessenger
L2 *bindings.L2CrossDomainMessenger
}
// NewMessengers constructs Messengers. Passing in the address of the
// L1CrossDomainMessenger is required to connect to the
func NewMessengers(backends *Backends, l1CrossDomainMessenger common.Address) (*Messengers, error) {
l1Messenger, err := bindings.NewL1CrossDomainMessenger(l1CrossDomainMessenger, backends.L1)
if err != nil {
return nil, err
}
l2Messenger, err := bindings.NewL2CrossDomainMessenger(predeploys.L2CrossDomainMessengerAddr, backends.L2)
if err != nil {
return nil, err
}
return &Messengers{
L1: l1Messenger,
L2: l2Messenger,
}, nil
}
// GetPendingWithdrawals will fetch pending withdrawals by getting
// L2CrossDomainMessenger `SentMessage` events and then checking to see if the
// cross domain message hash has been finalized on L1. It will return a slice of
// PendingWithdrawals that have not been finalized on L1.
func GetPendingWithdrawals(messengers *Messengers, version *big.Int, start, end uint64) ([]PendingWithdrawal, error) {
withdrawals := make([]PendingWithdrawal, 0)
// This will not take into account "pending" state, this ensures that
// transactions in the mempool are upgraded as well.
opts := bind.FilterOpts{
Start: start,
}
// Only set the end block range if end is non zero. When end is zero, the
// filter will extend to the latest block.
if end != 0 {
opts.End = &end
}
messages, err := messengers.L2.FilterSentMessage(&opts, nil)
if err != nil {
return nil, err
}
defer messages.Close()
for messages.Next() {
event := messages.Event
msg := NewCrossDomainMessage(
event.MessageNonce,
event.Sender,
event.Target,
common.Big0,
event.GasLimit,
event.Message,
)
// Optional version check
if version != nil {
if version.Uint64() != msg.Version() {
return nil, fmt.Errorf("expected version %d, got version %d", version, msg.Version())
}
}
hash, err := msg.Hash()
if err != nil {
return nil, err
}
relayed, err := messengers.L1.SuccessfulMessages(&bind.CallOpts{}, hash)
if err != nil {
return nil, err
}
if !relayed {
log.Info("%s not yet relayed", event.Raw.TxHash)
withdrawal := PendingWithdrawal{
LegacyWithdrawal: LegacyWithdrawal{
XDomainTarget: event.Target,
XDomainSender: event.Sender,
XDomainData: event.Message,
XDomainNonce: event.MessageNonce,
},
TransactionHash: event.Raw.TxHash,
}
withdrawals = append(withdrawals, withdrawal)
} else {
log.Info("%s already relayed", event.Raw.TxHash)
}
}
return withdrawals, nil
}
package crossdomain_test
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/state"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
var (
// testKey is the same test key that geth uses
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// chainID is the chain id used for simulated backends
chainID = big.NewInt(1337)
// testAccount represents the sender account for tests
testAccount = crypto.PubkeyToAddress(testKey.PublicKey)
)
// sendMessageArgs represents the input to `SendMessage`. The value
// is excluded specifically here because we want to simulate v0 messages
// as closely as possible.
type sendMessageArgs struct {
Target common.Address
Message []byte
MinGasLimit uint32
}
// setL1CrossDomainMessenger will set the L1CrossDomainMessenger into
// a state db that represents L1. It accepts a list of "successfulMessages"
// to be placed into the state. This allows for a subset of messages that
// were withdrawn on L2 to be placed into the L1 state to simulate
// a set of withdrawals that are not finalized on L1
func setL1CrossDomainMessenger(db vm.StateDB, successful []common.Hash) error {
bytecode, err := bindings.GetDeployedBytecode("L1CrossDomainMessenger")
if err != nil {
return err
}
db.CreateAccount(predeploys.DevL1CrossDomainMessengerAddr)
db.SetCode(predeploys.DevL1CrossDomainMessengerAddr, bytecode)
msgs := make(map[any]any)
for _, hash := range successful {
msgs[hash] = true
}
return state.SetStorage(
"L1CrossDomainMessenger",
predeploys.DevL1CrossDomainMessengerAddr,
state.StorageValues{
"successfulMessages": msgs,
},
db,
)
}
// setL2CrossDomainMessenger will set the L2CrossDomainMessenger into
// a state db that represents L2. It does not set any state as the only
// function called in this test is "sendMessage" which calls a hardcoded
// address that represents the L2ToL1MessagePasser
func setL2CrossDomainMessenger(db vm.StateDB) error {
bytecode, err := bindings.GetDeployedBytecode("L2CrossDomainMessenger")
if err != nil {
return err
}
db.CreateAccount(predeploys.L2CrossDomainMessengerAddr)
db.SetCode(predeploys.L2CrossDomainMessengerAddr, bytecode)
return state.SetStorage(
"L2CrossDomainMessenger",
predeploys.L2CrossDomainMessengerAddr,
state.StorageValues{
"successfulMessages": map[any]any{},
},
db,
)
}
// setL2ToL1MessagePasser will set the L2ToL1MessagePasser into a state
// db that represents L2. This must be set so the L2CrossDomainMessenger
// can call it as part of "sendMessage"
func setL2ToL1MessagePasser(db vm.StateDB) error {
bytecode, err := bindings.GetDeployedBytecode("L2ToL1MessagePasser")
if err != nil {
return err
}
db.CreateAccount(predeploys.L2ToL1MessagePasserAddr)
db.SetCode(predeploys.L2ToL1MessagePasserAddr, bytecode)
return state.SetStorage(
"L2ToL1MessagePasser",
predeploys.L2ToL1MessagePasserAddr,
state.StorageValues{},
db,
)
}
// sendCrossDomainMessage will send a L2 to L1 cross domain message.
// The state cannot just be set because logs must be generated by
// transaction execution
func sendCrossDomainMessage(
l2xdm *bindings.L2CrossDomainMessenger,
backend *backends.SimulatedBackend,
message *sendMessageArgs,
t *testing.T,
) *crossdomain.CrossDomainMessage {
opts, err := bind.NewKeyedTransactorWithChainID(testKey, chainID)
require.Nil(t, err)
tx, err := l2xdm.SendMessage(opts, message.Target, message.Message, message.MinGasLimit)
require.Nil(t, err)
backend.Commit()
receipt, err := backend.TransactionReceipt(context.Background(), tx.Hash())
require.Nil(t, err)
abi, _ := bindings.L2CrossDomainMessengerMetaData.GetAbi()
var msg crossdomain.CrossDomainMessage
// Ensure that we see the event so that a default CrossDomainMessage
// is not returned
seen := false
// Assume there is only 1 deposit per transaction
for _, log := range receipt.Logs {
event, _ := abi.EventByID(log.Topics[0])
// Not the event we are looking for
if event == nil {
continue
}
// Parse the legacy event
if event.Name == "SentMessage" {
e, _ := l2xdm.ParseSentMessage(*log)
msg.Target = e.Target
msg.Sender = e.Sender
msg.Data = e.Message
msg.Nonce = e.MessageNonce
msg.GasLimit = e.GasLimit
// Set seen to true to ensure that this event
// was observed
seen = true
}
// Parse the new extension event
if event.Name == "SentMessageExtension1" {
e, _ := l2xdm.ParseSentMessageExtension1(*log)
msg.Value = e.Value
}
}
require.True(t, seen)
return &msg
}
// TestGetPendingWithdrawals tests the high level function used
// to fetch pending withdrawals
func TestGetPendingWithdrawals(t *testing.T) {
// Create a L2 db
L2db := state.NewMemoryStateDB(nil)
// Set the test account and give it a large balance
L2db.CreateAccount(testAccount)
L2db.AddBalance(testAccount, big.NewInt(10000000000000000))
// Set the L2ToL1MessagePasser in the L2 state
err := setL2ToL1MessagePasser(L2db)
require.Nil(t, err)
// Set the L2CrossDomainMessenger in the L2 state
err = setL2CrossDomainMessenger(L2db)
require.Nil(t, err)
L2 := backends.NewSimulatedBackend(
L2db.Genesis().Alloc,
15000000,
)
L2CrossDomainMessenger, err := bindings.NewL2CrossDomainMessenger(
predeploys.L2CrossDomainMessengerAddr,
L2,
)
require.Nil(t, err)
// Create a set of test data that is made up of cross domain messages.
// There is a total of 6 cross domain messages. 3 of them are set to be
// finalized on L1 so 3 of them will be considered not finalized.
msgs := []*sendMessageArgs{
{
Target: common.Address{},
Message: []byte{},
MinGasLimit: 0,
},
{
Target: common.Address{0x01},
Message: []byte{0x01},
MinGasLimit: 0,
},
{
Target: common.Address{},
Message: []byte{},
MinGasLimit: 100,
},
{
Target: common.Address{19: 0x01},
Message: []byte{0xaa, 0xbb},
MinGasLimit: 10000,
},
{
Target: common.HexToAddress("0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263"),
Message: hexutil.MustDecode("0x095ea7b3000000000000000000000000c92e8bdf79f0507f65a392b0ab4667716bfe01100000000000000000000000000000000000000000000000000000000000000000"),
MinGasLimit: 50000,
},
{
Target: common.HexToAddress("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"),
Message: []byte{},
MinGasLimit: 70511,
},
}
// For each test cross domain message, call "sendMessage" on the
// L2CrossDomainMessenger and compute the cross domain message hash
hashes := make([]common.Hash, len(msgs))
for i, msg := range msgs {
sent := sendCrossDomainMessage(L2CrossDomainMessenger, L2, msg, t)
hash, err := sent.Hash()
require.Nil(t, err)
hashes[i] = hash
}
// Create a L1 backend with a dev account
L1db := state.NewMemoryStateDB(nil)
L1db.CreateAccount(testAccount)
L1db.AddBalance(testAccount, big.NewInt(10000000000000000))
// Set the L1CrossDomainMessenger into the L1 state. Only set a subset
// of the messages as finalized, the first 3.
err = setL1CrossDomainMessenger(L1db, hashes[0:3])
require.Nil(t, err)
L1 := backends.NewSimulatedBackend(
L1db.Genesis().Alloc,
15000000,
)
backends := crossdomain.NewBackends(L1, L2)
messengers, err := crossdomain.NewMessengers(backends, predeploys.DevL1CrossDomainMessengerAddr)
require.Nil(t, err)
// Fetch the pending withdrawals
withdrawals, err := crossdomain.GetPendingWithdrawals(messengers, nil, 0, 100)
require.Nil(t, err)
// Since only half of the withdrawals were set as finalized on L1,
// the number of pending withdrawals should be 3
require.Equal(t, 3, len(withdrawals))
// The final 3 test cross domain messages should be equal to the
// fetched pending withdrawals. This shows that `GetPendingWithdrawals`
// fetched the correct messages
for i, msg := range msgs[3:] {
withdrawal := withdrawals[i]
require.Equal(t, msg.Target, withdrawal.XDomainTarget)
require.Equal(t, msg.Message, []byte(withdrawal.XDomainData))
}
}
package crossdomain
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// SentMessage represents an entry in the JSON file that is created by
// the `migration-data` package. Each entry represents a call to the
// `LegacyMessagePasser`. The `who` should always be the
// `L2CrossDomainMessenger` and the `msg` should be an abi encoded
// `relayMessage(address,address,bytes,uint256)`
type SentMessage struct {
Who common.Address `json:"who"`
Msg hexutil.Bytes `json:"msg"`
}
// NewSentMessageFromJSON will read a JSON file from disk given a path to the JSON
// file. The JSON file this function reads from disk is an output from the
// `migration-data` package.
func NewSentMessageFromJSON(path string) ([]*SentMessage, error) {
file, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot find sent message json at %s: %w", path, err)
}
var j []*SentMessage
if err := json.Unmarshal(file, &j); err != nil {
return nil, err
}
return j, nil
}
// decodeWitnessCalldata abi decodes the calldata encoded in the input witness
// file. It errors if the 4 byte selector is not specifically for `passMessageToL1`.
// It also errors if the abi decoding fails.
func decodeWitnessCalldata(msg []byte) ([]byte, error) {
abi, err := bindings.LegacyMessagePasserMetaData.GetAbi()
if err != nil {
panic("should always be able to get message passer abi")
}
if size := len(msg); size < 4 {
return nil, fmt.Errorf("message too short: %d", size)
}
method, err := abi.MethodById(msg[:4])
if err != nil {
return nil, err
}
if method.Sig != "passMessageToL1(bytes)" {
return nil, fmt.Errorf("unknown method: %s", method.Name)
}
out, err := method.Inputs.Unpack(msg[4:])
if err != nil {
return nil, err
}
cast, ok := out[0].([]byte)
if !ok {
panic("should always be able to cast type []byte")
}
return cast, nil
}
// ReadWitnessData will read messages and addresses from a raw l2geth state
// dump file.
func ReadWitnessData(path string) ([]*SentMessage, OVMETHAddresses, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, fmt.Errorf("cannot open witness data file: %w", err)
}
defer f.Close()
scan := bufio.NewScanner(f)
var witnesses []*SentMessage
addresses := make(map[common.Address]bool)
for scan.Scan() {
line := scan.Text()
splits := strings.Split(line, "|")
if len(splits) < 2 {
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
switch splits[0] {
case "MSG":
if len(splits) != 3 {
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
msg := splits[2]
// Make sure that the witness data has a 0x prefix
if !strings.HasPrefix(msg, "0x") {
msg = "0x" + msg
}
msgB := hexutil.MustDecode(msg)
// Skip any errors
calldata, err := decodeWitnessCalldata(msgB)
if err != nil {
log.Warn("cannot decode witness calldata", "err", err)
continue
}
witnesses = append(witnesses, &SentMessage{
Who: common.HexToAddress(splits[1]),
Msg: calldata,
})
case "ETH":
addresses[common.HexToAddress(splits[1])] = true
default:
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
}
return witnesses, addresses, nil
}
// ToLegacyWithdrawal will convert a SentMessageJSON to a LegacyWithdrawal
// struct. This is useful because the LegacyWithdrawal struct has helper
// functions on it that can compute the withdrawal hash and the storage slot.
func (s *SentMessage) ToLegacyWithdrawal() (*LegacyWithdrawal, error) {
data := make([]byte, len(s.Who)+len(s.Msg))
copy(data, s.Msg)
copy(data[len(s.Msg):], s.Who[:])
var w LegacyWithdrawal
if err := w.Decode(data); err != nil {
return nil, err
}
return &w, nil
}
// OVMETHAddresses represents a list of addresses that interacted with
// the ERC20 representation of ether in the pre-bedrock system.
type OVMETHAddresses map[common.Address]bool
// NewAddresses will read an addresses.json file from the filesystem.
func NewAddresses(path string) (OVMETHAddresses, error) {
file, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot find addresses json at %s: %w", path, err)
}
var addresses []common.Address
if err := json.Unmarshal(file, &addresses); err != nil {
return nil, err
}
ovmeth := make(OVMETHAddresses)
for _, addr := range addresses {
ovmeth[addr] = true
}
return ovmeth, nil
}
// Allowance represents the allowances that were set in the
// legacy ERC20 representation of ether
type Allowance struct {
From common.Address `json:"fr"`
To common.Address `json:"to"`
}
// NewAllowances will read the ovm-allowances.json from the file system.
func NewAllowances(path string) ([]*Allowance, error) {
file, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot find allowances json at %s: %w", path, err)
}
var allowances []*Allowance
if err := json.Unmarshal(file, &allowances); err != nil {
return nil, err
}
return allowances, nil
}
// MigrationData represents all of the data required to do a migration
type MigrationData struct {
// OvmAddresses represents the set of addresses that interacted with the
// LegacyERC20ETH contract before the evm equivalence upgrade
OvmAddresses OVMETHAddresses
// EvmAddresses represents the set of addresses that interacted with the
// LegacyERC20ETH contract after the evm equivalence upgrade
EvmAddresses OVMETHAddresses
// OvmAllowances represents the set of allowances in the LegacyERC20ETH from
// before the evm equivalence upgrade
OvmAllowances []*Allowance
// OvmMessages represents the set of withdrawals through the
// L2CrossDomainMessenger from before the evm equivalence upgrade
OvmMessages []*SentMessage
// OvmMessages represents the set of withdrawals through the
// L2CrossDomainMessenger from after the evm equivalence upgrade
EvmMessages []*SentMessage
}
func (m *MigrationData) ToWithdrawals() (DangerousUnfilteredWithdrawals, []InvalidMessage, error) {
messages := make(DangerousUnfilteredWithdrawals, 0)
invalidMessages := make([]InvalidMessage, 0)
for _, msg := range m.OvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
return nil, nil, fmt.Errorf("error serializing OVM message: %w", err)
}
messages = append(messages, wd)
}
for _, msg := range m.EvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
log.Warn("Discovered mal-formed withdrawal", "who", msg.Who, "data", msg.Msg)
invalidMessages = append(invalidMessages, InvalidMessage(*msg))
continue
}
messages = append(messages, wd)
}
return messages, invalidMessages, nil
}
func (m *MigrationData) Addresses() []common.Address {
addresses := make([]common.Address, 0)
for addr := range m.EvmAddresses {
addresses = append(addresses, addr)
}
for addr := range m.OvmAddresses {
addresses = append(addresses, addr)
}
return addresses
}
package crossdomain
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestRead(t *testing.T) {
witnesses, addresses, err := ReadWitnessData("testdata/witness.txt")
require.NoError(t, err)
require.Equal(t, []*SentMessage{
{
Who: common.HexToAddress("0x4200000000000000000000000000000000000007"),
Msg: common.FromHex(
"0xcbd4ece900000000000000000000000099c9fc46f92e8a1c0dec1b1747d01090" +
"3e884be100000000000000000000000042000000000000000000000000000000" +
"0000001000000000000000000000000000000000000000000000000000000000" +
"0000008000000000000000000000000000000000000000000000000000000000" +
"00019bd000000000000000000000000000000000000000000000000000000000" +
"000000e4a9f9e675000000000000000000000000d533a949740bb3306d119cc7" +
"77fa900ba034cd520000000000000000000000000994206dfe8de6ec6920ff4d" +
"779b0d950605fb53000000000000000000000000e3a44dd2a8c108be56a78635" +
"121ec914074da16d000000000000000000000000e3a44dd2a8c108be56a78635" +
"121ec914074da16d0000000000000000000000000000000000000000000001b0" +
"ac98ab3858d75478000000000000000000000000000000000000000000000000" +
"00000000000000c0000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"00000000",
),
},
{
Who: common.HexToAddress("0x8b1d477410344785ff1df52500032e6d5f532ee4"),
Msg: common.FromHex("0x042069"),
},
}, witnesses)
require.Equal(t, OVMETHAddresses{
common.HexToAddress("0x6340d44c5174588B312F545eEC4a42f8a514eF50"): true,
}, addresses)
}
// TestDecodeWitnessCallData tests that the witness data is parsed correctly
// from an input bytes slice.
func TestDecodeWitnessCallData(t *testing.T) {
tests := []struct {
name string
err bool
msg []byte
want []byte
}{
{
name: "too-small",
err: true,
msg: common.FromHex("0x0000"),
},
{
name: "unknown-selector",
err: true,
msg: common.FromHex("0x00000000"),
},
{
name: "wrong-selector",
err: true,
// 0x54fd4d50 is the selector for `version()`
msg: common.FromHex("0x54fd4d50"),
},
{
name: "invalid-calldata-only-selector",
err: true,
// 0xcafa81dc is the selector for `passMessageToL1(bytes)`
msg: common.FromHex("0xcafa81dc"),
},
{
name: "invalid-calldata-invalid-bytes",
err: true,
// 0xcafa81dc is the selector for passMessageToL1(bytes)
msg: common.FromHex("0xcafa81dc0000"),
},
{
name: "valid-calldata",
msg: common.FromHex(
"0xcafa81dc" +
"0000000000000000000000000000000000000000000000000000000000000020" +
"0000000000000000000000000000000000000000000000000000000000000002" +
"1234000000000000000000000000000000000000000000000000000000000000",
),
want: common.FromHex("0x1234"),
},
}
for _, tt := range tests {
test := tt
t.Run(test.name, func(t *testing.T) {
if test.err {
_, err := decodeWitnessCalldata(test.msg)
require.Error(t, err)
} else {
want, err := decodeWitnessCalldata(test.msg)
require.NoError(t, err)
require.Equal(t, test.want, want)
}
})
}
}
// TestMessagePasserSafety ensures that the LegacyMessagePasser contract reverts when it is called
// with incorrect calldata. The function signature is correct but the calldata is not abi encoded
// correctly. It is expected the solidity reverts when it cannot abi decode the calldata correctly.
// Only a call to `passMessageToL1` with abi encoded `bytes` will result in the `successfulMessages`
// mapping being updated.
func TestMessagePasserSafety(t *testing.T) {
testKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
opts, err := bind.NewKeyedTransactorWithChainID(testKey, big.NewInt(1337))
require.NoError(t, err)
backend := backends.NewSimulatedBackend(
core.GenesisAlloc{testAddr: {Balance: big.NewInt(10000000000000000)}},
30_000_000,
)
defer backend.Close()
// deploy the LegacyMessagePasser contract
addr, tx, contract, err := bindings.DeployLegacyMessagePasser(opts, backend)
require.NoError(t, err)
backend.Commit()
_, err = bind.WaitMined(context.Background(), backend, tx)
require.NoError(t, err)
// ensure that it deployed
code, err := backend.CodeAt(context.Background(), addr, nil)
require.NoError(t, err)
require.True(t, len(code) > 0)
// dummy message
msg := []byte{0x00, 0x01, 0x02, 0x03}
// call `passMessageToL1`
msgTx, err := contract.PassMessageToL1(opts, msg)
require.NoError(t, err)
// ensure that the receipt is successful
backend.Commit()
msgReceipt, err := bind.WaitMined(context.Background(), backend, msgTx)
require.NoError(t, err)
require.Equal(t, msgReceipt.Status, types.ReceiptStatusSuccessful)
// check for the data in the `successfulMessages` mapping
data := make([]byte, len(msg)+len(testAddr))
copy(data[:], msg)
copy(data[len(msg):], testAddr.Bytes())
digest := crypto.Keccak256Hash(data)
contains, err := contract.SentMessages(&bind.CallOpts{}, digest)
require.NoError(t, err)
require.True(t, contains)
// build a transaction with improperly formatted calldata
nonce, err := backend.NonceAt(context.Background(), testAddr, nil)
require.NoError(t, err)
// append msg without abi encoding it
selector := crypto.Keccak256([]byte("passMessageToL1(bytes)"))[0:4]
require.Equal(t, selector, hexutil.MustDecode("0xcafa81dc"))
calldata := append(selector, msg...)
faultyTransaction, err := opts.Signer(testAddr, types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(1337),
Nonce: nonce,
GasTipCap: msgTx.GasTipCap(),
GasFeeCap: msgTx.GasFeeCap(),
Gas: msgTx.Gas() * 2,
To: msgTx.To(),
Data: calldata,
}))
require.NoError(t, err)
err = backend.SendTransaction(context.Background(), faultyTransaction)
require.NoError(t, err)
// the transaction should revert
backend.Commit()
badReceipt, err := bind.WaitMined(context.Background(), backend, faultyTransaction)
require.NoError(t, err)
require.Equal(t, badReceipt.Status, types.ReceiptStatusFailed)
// test the transaction calldata against the abi unpacking
abi, err := bindings.LegacyMessagePasserMetaData.GetAbi()
require.NoError(t, err)
method, err := abi.MethodById(selector)
require.NoError(t, err)
require.Equal(t, method.Name, "passMessageToL1")
// the faulty transaction has the correct 4 byte selector but doesn't
// have abi encoded bytes following it
require.Equal(t, faultyTransaction.Data()[:4], selector)
_, err = method.Inputs.Unpack(faultyTransaction.Data()[4:])
require.Error(t, err)
// the original transaction has the correct 4 byte selector and abi encoded bytes
_, err = method.Inputs.Unpack(msgTx.Data()[4:])
require.NoError(t, err)
}
package ether
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
)
var (
// AddressPreimagePrefix is the byte prefix of address preimages
// in Geth's database.
AddressPreimagePrefix = []byte("addr-preimage-")
// ErrStopIteration will stop iterators early when returned from the
// iterator's callback.
ErrStopIteration = errors.New("iteration stopped")
// MintTopic is the topic for mint events on OVM ETH.
MintTopic = common.HexToHash("0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885")
)
type AddressCB func(address common.Address) error
type AddressCBWithHead func(address common.Address, headNum uint64) error
type AllowanceCB func(owner, spender common.Address) error
// IterateDBAddresses iterates over each address in Geth's address
// preimage database, calling the callback with the address.
func IterateDBAddresses(db ethdb.Database, cb AddressCB) error {
iter := db.NewIterator(AddressPreimagePrefix, nil)
for iter.Next() {
if iter.Error() != nil {
return iter.Error()
}
addr := common.BytesToAddress(bytes.TrimPrefix(iter.Key(), AddressPreimagePrefix))
cbErr := cb(addr)
if cbErr == ErrStopIteration {
return nil
}
if cbErr != nil {
return cbErr
}
}
return iter.Error()
}
// IterateAddrList iterates over each address in an address list,
// calling the callback with the address.
func IterateAddrList(r io.Reader, cb AddressCB) error {
scan := bufio.NewScanner(r)
for scan.Scan() {
addrStr := scan.Text()
if !common.IsHexAddress(addrStr) {
return fmt.Errorf("invalid address %s", addrStr)
}
err := cb(common.HexToAddress(addrStr))
if err == ErrStopIteration {
return nil
}
if err != nil {
return err
}
}
return nil
}
// IterateAllowanceList iterates over each address in an allowance list,
// calling the callback with the owner and the spender.
func IterateAllowanceList(r io.Reader, cb AllowanceCB) error {
scan := bufio.NewScanner(r)
for scan.Scan() {
line := scan.Text()
splits := strings.Split(line, ",")
if len(splits) != 2 {
return fmt.Errorf("invalid allowance %s", line)
}
owner := splits[0]
spender := splits[1]
if !common.IsHexAddress(owner) {
return fmt.Errorf("invalid address %s", owner)
}
if !common.IsHexAddress(spender) {
return fmt.Errorf("invalid address %s", spender)
}
err := cb(common.HexToAddress(owner), common.HexToAddress(spender))
if err == ErrStopIteration {
return nil
}
}
return nil
}
// IterateMintEvents iterates over each mint event in the database starting
// from head and stopping at genesis.
func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCBWithHead, progressCb func(uint64)) error {
for headNum > 0 {
hash := rawdb.ReadCanonicalHash(db, headNum)
receipts, err := crossdomain.ReadLegacyReceipts(db, hash, headNum)
if err != nil {
return err
}
for _, receipt := range receipts {
for _, l := range receipt.Logs {
if l.Address != predeploys.LegacyERC20ETHAddr {
continue
}
if common.BytesToHash(l.Topics[0].Bytes()) != MintTopic {
continue
}
err := cb(common.BytesToAddress(l.Topics[1][12:]), headNum)
if errors.Is(err, ErrStopIteration) {
return nil
}
if err != nil {
return err
}
}
}
progressCb(headNum)
headNum--
}
return nil
}
package ether
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
)
// getOVMETHTotalSupply returns OVM ETH's total supply by reading
// the appropriate storage slot.
func getOVMETHTotalSupply(db *state.StateDB) *big.Int {
key := getOVMETHTotalSupplySlot()
return db.GetState(OVMETHAddress, key).Big()
}
func getOVMETHTotalSupplySlot() common.Hash {
position := common.Big2
key := common.BytesToHash(common.LeftPadBytes(position.Bytes(), 32))
return key
}
func GetOVMETHTotalSupplySlot() common.Hash {
return getOVMETHTotalSupplySlot()
}
// GetOVMETHBalance gets a user's OVM ETH balance from state by querying the
// appropriate storage slot directly.
func GetOVMETHBalance(db *state.StateDB, addr common.Address) *big.Int {
return db.GetState(OVMETHAddress, CalcOVMETHStorageKey(addr)).Big()
}
package ether
import (
"path/filepath"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
// MustOpenDB opens a Geth database, or panics. Note that
// the database must be opened with a freezer in order to
// properly read historical data.
func MustOpenDB(dataDir string) ethdb.Database {
return MustOpenDBWithCacheOpts(dataDir, 0, 0)
}
// MustOpenDBWithCacheOpts opens a Geth database or panics. Allows
// the caller to pass in LevelDB cache parameters.
func MustOpenDBWithCacheOpts(dataDir string, cacheSize, handles int) ethdb.Database {
dir := filepath.Join(dataDir, "geth", "chaindata")
db, err := rawdb.Open(rawdb.OpenOptions{
Type: "leveldb",
Directory: dir,
AncientsDirectory: filepath.Join(dir, "ancient"),
Namespace: "",
Cache: cacheSize,
Handles: handles,
ReadOnly: true,
})
if err != nil {
log.Crit("error opening raw DB", "err", err)
}
return db
}
package ether
import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log"
)
const (
// checkJobs is the number of parallel workers to spawn
// when iterating the storage trie.
checkJobs = 64
// BalanceSlot is an ordinal used to represent slots corresponding to OVM_ETH
// balances in the state.
BalanceSlot = 1
// AllowanceSlot is an ordinal used to represent slots corresponding to OVM_ETH
// allowances in the state.
AllowanceSlot = 2
)
var (
// OVMETHAddress is the address of the OVM ETH predeploy.
OVMETHAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
ignoredSlots = map[common.Hash]bool{
// Total Supply
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002"): true,
// Name
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000003"): true,
// Symbol
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000004"): true,
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000005"): true,
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000006"): true,
}
// sequencerEntrypointAddr is the address of the OVM sequencer entrypoint contract.
sequencerEntrypointAddr = common.HexToAddress("0x4200000000000000000000000000000000000005")
)
// accountData is a wrapper struct that contains the balance and address of an account.
// It gets passed via channel to the collector process.
type accountData struct {
balance *big.Int
legacySlot common.Hash
address common.Address
}
// MigrateBalances migrates all balances in the LegacyERC20ETH contract into state. It performs checks
// in parallel with mutations in order to reduce overall migration time.
func MigrateBalances(mutableDB *state.StateDB, dbFactory util.DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) error {
// Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
return fmt.Errorf("no chain params for %d", chainID)
}
return doMigration(mutableDB, dbFactory, addresses, allowances, params.ExpectedSupplyDelta, noCheck)
}
func doMigration(mutableDB *state.StateDB, dbFactory util.DBFactory, addresses []common.Address, allowances []*crossdomain.Allowance, expDiff *big.Int, noCheck bool) error {
// We'll need to maintain a list of all addresses that we've seen along with all of the storage
// slots based on the witness data.
slotsAddrs := make(map[common.Hash]common.Address)
slotsInp := make(map[common.Hash]int)
// For each known address, compute its balance key and add it to the list of addresses.
// Mint events are instrumented as regular ETH events in the witness data, so we no longer
// need to iterate over mint events during the migration.
for _, addr := range addresses {
sk := CalcOVMETHStorageKey(addr)
slotsAddrs[sk] = addr
slotsInp[sk] = BalanceSlot
}
// For each known allowance, compute its storage key and add it to the list of addresses.
for _, allowance := range allowances {
sk := CalcAllowanceStorageKey(allowance.From, allowance.To)
slotsAddrs[sk] = allowance.From
slotsInp[sk] = AllowanceSlot
}
// Add the old SequencerEntrypoint because someone sent it ETH a long time ago and it has a
// balance but none of our instrumentation could easily find it. Special case.
entrySK := CalcOVMETHStorageKey(sequencerEntrypointAddr)
slotsAddrs[entrySK] = sequencerEntrypointAddr
slotsInp[entrySK] = BalanceSlot
// Channel to receive storage slot keys and values from each iteration job.
outCh := make(chan accountData)
// Channel that gets closed when the collector is done.
doneCh := make(chan struct{})
// Create a map of accounts we've seen so that we can filter out duplicates.
seenAccounts := make(map[common.Address]bool)
// Keep track of the total migrated supply.
totalFound := new(big.Int)
// Kick off a background process to collect
// values from the channel and add them to the map.
var count int
var dups int
progress := util.ProgressLogger(1000, "Migrated OVM_ETH storage slot")
go func() {
defer func() { doneCh <- struct{}{} }()
for account := range outCh {
progress()
// Filter out duplicate accounts. See the below note about keyspace iteration for
// why we may have to filter out duplicates.
if seenAccounts[account.address] {
log.Info("skipping duplicate account during iteration", "addr", account.address)
dups++
continue
}
// Accumulate addresses and total supply.
totalFound = new(big.Int).Add(totalFound, account.balance)
mutableDB.SetBalance(account.address, account.balance)
mutableDB.SetState(predeploys.LegacyERC20ETHAddr, account.legacySlot, common.Hash{})
count++
seenAccounts[account.address] = true
}
}()
err := util.IterateState(dbFactory, predeploys.LegacyERC20ETHAddr, func(db *state.StateDB, key, value common.Hash) error {
// We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] {
return nil
}
slotType, ok := slotsInp[key]
if !ok {
log.Error("unknown storage slot in state", "slot", key.String())
if !noCheck {
return fmt.Errorf("unknown storage slot in state: %s", key.String())
}
}
// No accounts should have a balance in state. If they do, bail.
addr, ok := slotsAddrs[key]
if !ok {
log.Crit("could not find address in map - should never happen")
}
bal := db.GetBalance(addr)
if bal.Sign() != 0 {
log.Error(
"account has non-zero balance in state - should never happen",
"addr", addr,
"balance", bal.String(),
)
if !noCheck {
return fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr.String())
}
}
// Add balances to the total found.
switch slotType {
case BalanceSlot:
// Send the data to the channel.
outCh <- accountData{
balance: value.Big(),
legacySlot: key,
address: addr,
}
case AllowanceSlot:
// Allowance slot. Do nothing here.
default:
// Should never happen.
if noCheck {
log.Error("unknown slot type", "slot", key, "type", slotType)
} else {
log.Crit("unknown slot type, should never happen", "type", slotType)
}
}
return nil
}, checkJobs)
if err != nil {
return err
}
// Close the outCh to cancel the collector. The collector will signal that it's done
// using doneCh. Any values waiting to be read from outCh will be read before the
// collector exits.
close(outCh)
<-doneCh
// Log how many slots were iterated over.
log.Info("Iterated legacy balances", "count", count, "dups", dups, "total", count+dups)
log.Info("Comparison to input list of legacy accounts",
"total_input", len(addresses),
"diff_count", len(addresses)-count,
"diff_total", len(addresses)-(count+dups),
)
// Print first 10 accounts without balance
aleft := 10
log.Info("Listing first accounts without balance", "num", aleft)
for i, a := range addresses {
if !seenAccounts[a] {
log.Info("Account without balance", "idx", i, "addr", a)
aleft--
}
if aleft == 0 {
break
}
}
// Verify the supply delta. Recorded total supply in the LegacyERC20ETH contract may be higher
// than the actual migrated amount because self-destructs will remove ETH supply in a way that
// cannot be reflected in the contract. This is fine because self-destructs just mean the L2 is
// actually *overcollateralized* by some tiny amount.
db, err := dbFactory()
if err != nil {
log.Crit("cannot get database", "err", err)
}
totalSupply := getOVMETHTotalSupply(db)
delta := new(big.Int).Sub(totalSupply, totalFound)
if delta.Cmp(expDiff) != 0 {
log.Error(
"supply mismatch",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expDiff.String(),
)
if !noCheck {
return fmt.Errorf("supply mismatch: %s", delta.String())
}
}
// Supply is verified.
log.Info(
"supply verified OK",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expDiff.String(),
)
// Set the total supply to 0. We do this because the total supply is necessarily going to be
// different than the sum of all balances since we no longer track balances inside the contract
// itself. The total supply is going to be weird no matter what, might as well set it to zero
// so it's explicitly weird instead of implicitly weird.
mutableDB.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
log.Info("Set the totalSupply to 0")
return nil
}
This diff is collapsed.
package ether
import (
"github.com/ethereum/go-ethereum/common"
"golang.org/x/crypto/sha3"
)
// BytesBacked is a re-export of the same interface in Geth,
// which is unfortunately private.
type BytesBacked interface {
Bytes() []byte
}
// CalcAllowanceStorageKey calculates the storage key of an allowance in OVM ETH.
func CalcAllowanceStorageKey(owner common.Address, spender common.Address) common.Hash {
inner := CalcStorageKey(owner, common.Big1)
return CalcStorageKey(spender, inner)
}
// CalcOVMETHStorageKey calculates the storage key of an OVM ETH balance.
func CalcOVMETHStorageKey(addr common.Address) common.Hash {
return CalcStorageKey(addr, common.Big0)
}
// CalcStorageKey is a helper method to calculate storage keys.
func CalcStorageKey(a, b BytesBacked) common.Hash {
hasher := sha3.NewLegacyKeccak256()
hasher.Write(common.LeftPadBytes(a.Bytes(), 32))
hasher.Write(common.LeftPadBytes(b.Bytes(), 32))
digest := hasher.Sum(nil)
return common.BytesToHash(digest)
}
This diff is collapsed.
This diff is collapsed.
......@@ -18,6 +18,9 @@ import (
// defaultL2GasLimit represents the default gas limit for an L2 block.
const defaultL2GasLimit = 30_000_000
// BedrockTransitionBlockExtraData represents the default extra data for the bedrock transition block.
var BedrockTransitionBlockExtraData = []byte("BEDROCK")
// NewL2Genesis will create a new L2 genesis
func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, error) {
if config.L2ChainID == 0 {
......
package migration_action
import (
"context"
"math/big"
"path/filepath"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethclient"
)
type Config struct {
DeployConfig *genesis.DeployConfig
OVMAddressesPath string
EVMAddressesPath string
OVMAllowancesPath string
OVMMessagesPath string
EVMMessagesPath string
Network string
HardhatDeployments []string
L1URL string
StartingL1BlockNumber uint64
L2DBPath string
DryRun bool
NoCheck bool
}
func Migrate(cfg *Config) (*genesis.MigrationResult, error) {
deployConfig := cfg.DeployConfig
ovmAddresses, err := crossdomain.NewAddresses(cfg.OVMAddressesPath)
if err != nil {
return nil, err
}
evmAddresess, err := crossdomain.NewAddresses(cfg.EVMAddressesPath)
if err != nil {
return nil, err
}
ovmAllowances, err := crossdomain.NewAllowances(cfg.OVMAllowancesPath)
if err != nil {
return nil, err
}
ovmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.OVMMessagesPath)
if err != nil {
return nil, err
}
evmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.EVMMessagesPath)
if err != nil {
return nil, err
}
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresess,
OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
l1Client, err := ethclient.Dial(cfg.L1URL)
if err != nil {
return nil, err
}
var blockNumber *big.Int
bnum := cfg.StartingL1BlockNumber
if bnum != 0 {
blockNumber = new(big.Int).SetUint64(bnum)
}
block, err := l1Client.BlockByNumber(context.Background(), blockNumber)
if err != nil {
return nil, err
}
chaindataPath := filepath.Join(cfg.L2DBPath, "geth", "chaindata")
ancientPath := filepath.Join(chaindataPath, "ancient")
ldb, err := rawdb.Open(
rawdb.OpenOptions{
Type: "leveldb",
Directory: chaindataPath,
Cache: 4096,
Handles: 120,
AncientsDirectory: ancientPath,
Namespace: "",
ReadOnly: false,
})
if err != nil {
return nil, err
}
defer ldb.Close()
return genesis.MigrateDB(ldb, deployConfig, block, &migrationData, !cfg.DryRun, cfg.NoCheck)
}
......@@ -26,33 +26,6 @@ var (
predeploys.GovernanceTokenAddr: true,
predeploys.WETH9Addr: true,
}
// UntouchableCodeHashes represent the bytecode hashes of contracts
// that should not be touched by the migration process.
UntouchableCodeHashes = map[common.Address]ChainHashMap{
predeploys.GovernanceTokenAddr: {
1: common.HexToHash("0x8551d935f4e67ad3c98609f0d9f0f234740c4c4599f82674633b55204393e07f"),
5: common.HexToHash("0xc4a213cf5f06418533e5168d8d82f7ccbcc97f27ab90197c2c051af6a4941cf9"),
},
predeploys.WETH9Addr: {
1: common.HexToHash("0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173"),
5: common.HexToHash("0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173"),
},
}
// FrozenStoragePredeploys represents the set of predeploys that
// will not have their storage wiped during the migration process.
// It is very explicitly set in its own mapping to ensure that
// changes elsewhere in the codebase do no alter the predeploys
// that do not have their storage wiped. It is safe for all other
// predeploys to have their storage wiped.
FrozenStoragePredeploys = map[common.Address]bool{
predeploys.GovernanceTokenAddr: true,
predeploys.WETH9Addr: true,
predeploys.LegacyMessagePasserAddr: true,
predeploys.LegacyERC20ETHAddr: true,
predeploys.DeployerWhitelistAddr: true,
}
)
// FundDevAccounts will fund each of the development accounts.
......@@ -79,32 +52,6 @@ func SetL1Proxies(db vm.StateDB, proxyAdminAddr common.Address) error {
return setProxies(db, proxyAdminAddr, bigL1PredeployNamespace, 2048)
}
// WipePredeployStorage will wipe the storage of all L2 predeploys expect
// for predeploys that must not have their storage altered.
func WipePredeployStorage(db vm.StateDB) error {
for name, addr := range predeploys.Predeploys {
if addr == nil {
return fmt.Errorf("nil address in predeploys mapping for %s", name)
}
if FrozenStoragePredeploys[*addr] {
log.Trace("skipping wiping of storage", "name", name, "address", *addr)
continue
}
log.Info("wiping storage", "name", name, "address", *addr)
// We need to make sure that we preserve nonces.
oldNonce := db.GetNonce(*addr)
db.CreateAccount(*addr)
if oldNonce > 0 {
db.SetNonce(*addr, oldNonce)
}
}
return nil
}
func setProxies(db vm.StateDB, proxyAdminAddr common.Address, namespace *big.Int, count uint64) error {
depBytecode, err := bindings.GetDeployedBytecode("Proxy")
if err != nil {
......
package genesis
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/trie"
"github.com/stretchr/testify/require"
)
func TestWipePredeployStorage(t *testing.T) {
rawDB := rawdb.NewMemoryDatabase()
rawStateDB := state.NewDatabaseWithConfig(rawDB, &trie.Config{
Preimages: true,
Cache: 1024,
})
stateDB, err := state.New(common.Hash{}, rawStateDB, nil)
require.NoError(t, err)
storeVal := common.Hash{31: 0xff}
for _, addr := range predeploys.Predeploys {
a := *addr
stateDB.SetState(a, storeVal, storeVal)
stateDB.SetBalance(a, big.NewInt(99))
stateDB.SetNonce(a, 99)
}
root, err := stateDB.Commit(false)
require.NoError(t, err)
err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
require.NoError(t, WipePredeployStorage(stateDB))
for _, addr := range predeploys.Predeploys {
a := *addr
if FrozenStoragePredeploys[a] {
require.Equal(t, storeVal, stateDB.GetState(a, storeVal))
} else {
require.Equal(t, common.Hash{}, stateDB.GetState(a, storeVal))
}
require.Equal(t, big.NewInt(99), stateDB.GetBalance(a))
require.Equal(t, uint64(99), stateDB.GetNonce(a))
}
}
package genesis
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
)
func Untar(tarball, target string) error {
f, err := os.Open(tarball)
if err != nil {
return err
}
defer f.Close()
r, err := gzip.NewReader(f)
if err != nil {
return err
}
tarReader := tar.NewReader(r)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return err
}
path := filepath.Join(target, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err = os.MkdirAll(path, info.Mode()); err != nil {
return err
}
continue
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, tarReader)
if err != nil {
return err
}
}
return nil
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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