Commit 919d19b9 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

op-chain-ops: ether migration (#3809)

* op-chain-ops: ether migration

Updates the migration script to also migrate
eth balances from their legacy erc20 format.
It will find the balances given the input generated
from `l2geth` and the JS package responsible for parsing
that input and then migrate the balance and delete
the balance in the erc20 contract.

It also adds some additional sanity checks to ensure
that the data is completely accounted for so that
the script cannot be ran with insufficient or malicious
inputs.

fixup

* op-chain-ops: fix linting

lint

* op-chain-ops: update go.mod

* op-chain-ops: cleanup code

* op-chain-ops: fix go mods

* op-chain-ops: pass through chain id

* op-chain-ops: properly encode nonce
Co-authored-by: default avatarMatthew Slipper <me@matthewslipper.com>
parent e92dbae6
......@@ -8,12 +8,12 @@ import (
"strings"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"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-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/mattn/go-isatty"
......@@ -84,28 +84,28 @@ func main() {
return err
}
ovmAddresses, err := genesis.NewAddresses(ctx.String("ovm-addresses"))
ovmAddresses, err := migration.NewAddresses(ctx.String("ovm-addresses"))
if err != nil {
return err
}
evmAddresess, err := genesis.NewAddresses(ctx.String("evm-addresses"))
evmAddresess, err := migration.NewAddresses(ctx.String("evm-addresses"))
if err != nil {
return err
}
ovmAllowances, err := genesis.NewAllowances(ctx.String("ovm-allowances"))
ovmAllowances, err := migration.NewAllowances(ctx.String("ovm-allowances"))
if err != nil {
return err
}
ovmMessages, err := genesis.NewSentMessage(ctx.String("ovm-messages"))
ovmMessages, err := migration.NewSentMessage(ctx.String("ovm-messages"))
if err != nil {
return err
}
evmMessages, err := genesis.NewSentMessage(ctx.String("evm-messages"))
evmMessages, err := migration.NewSentMessage(ctx.String("evm-messages"))
if err != nil {
return err
}
migrationData := genesis.MigrationData{
migrationData := migration.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresess,
OvmAllowances: ovmAllowances,
......@@ -143,18 +143,6 @@ func main() {
return err
}
hash := rawdb.ReadHeadHeaderHash(ldb)
if err != nil {
return err
}
num := rawdb.ReadHeaderNumber(ldb, hash)
header := rawdb.ReadHeader(ldb, hash, *num)
sdb, err := state.New(header.Root, state.NewDatabase(ldb), nil)
if err != nil {
return err
}
// Get the addresses from the hardhat deploy artifacts
l1StandardBridgeProxyDeployment, err := hh.GetDeployment("Proxy__OVM_L1StandardBridge")
if err != nil {
......@@ -176,20 +164,10 @@ func main() {
L1ERC721BridgeProxy: l1ERC721BridgeProxyDeployment.Address,
}
if err := genesis.MigrateDB(sdb, config, block, &l2Addrs, &migrationData); err != nil {
return err
}
if ctx.Bool("dry-run") {
log.Info("Dry run complete")
return nil
}
root, err := sdb.Commit(true)
if err != nil {
dryRun := ctx.Bool("dry-run")
if err := genesis.MigrateDB(ldb, config, block, &l2Addrs, &migrationData, !dryRun); err != nil {
return err
}
log.Info("Migration complete", "root", root)
return nil
},
......
......@@ -8,8 +8,8 @@ import (
"io"
"strings"
l2grawdb "github.com/ethereum-optimism/optimism/l2geth/core/rawdb"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
)
......@@ -31,8 +31,8 @@ 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(inDB ethdb.Database, cb AddressCB) error {
iter := inDB.NewIterator(AddressPreimagePrefix, nil)
func IterateDBAddresses(db ethdb.Database, cb AddressCB) error {
iter := db.NewIterator(AddressPreimagePrefix, nil)
for iter.Next() {
if iter.Error() != nil {
return iter.Error()
......@@ -98,10 +98,10 @@ func IterateAllowanceList(r io.Reader, cb AllowanceCB) error {
// IterateMintEvents iterates over each mint event in the database starting
// from head and stopping at genesis.
func IterateMintEvents(inDB ethdb.Database, headNum uint64, cb AddressCB) error {
func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCB) error {
for headNum > 0 {
hash := l2grawdb.ReadCanonicalHash(inDB, headNum)
receipts := l2grawdb.ReadRawReceipts(inDB, hash, headNum)
hash := rawdb.ReadCanonicalHash(db, headNum)
receipts := rawdb.ReadRawReceipts(db, hash, headNum)
for _, receipt := range receipts {
for _, l := range receipt.Logs {
if common.BytesToHash(l.Topics[0].Bytes()) != MintTopic {
......
This diff is collapsed.
package ether
import (
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"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/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances []*migration.Allowance, chainID int) error {
// Set of addresses that we will be migrating.
addressesToMigrate := make(map[common.Address]bool)
// Set of storage slots that we expect to see in the OVM ETH contract.
storageSlotsToMigrate := make(map[common.Hash]int)
// Chain params to use for integrity checking.
params := ParamsByChainID[chainID]
// Iterate over each address list, and read the addresses they
// contain into memory. Also calculate the storage slots for each
// address.
for _, addr := range addresses {
addressesToMigrate[addr] = true
storageSlotsToMigrate[CalcOVMETHStorageKey(addr)] = 1
}
for _, allowance := range allowances {
addressesToMigrate[allowance.From] = true
// TODO: double check ordering here
storageSlotsToMigrate[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2
}
headBlock := rawdb.ReadHeadBlock(db)
root := headBlock.Root()
// Read mint events from the database. Even though Geth's balance methods
// are instrumented, mints from the bridge happen in the EVM and so do
// not execute that code path. As a result, we parse mint events in order
// to not miss any balances.
log.Info("reading mint events from DB")
logProgress := ProgressLogger(100, "read mint event")
err := IterateMintEvents(db, headBlock.NumberU64(), func(address common.Address) error {
addressesToMigrate[address] = true
storageSlotsToMigrate[CalcOVMETHStorageKey(address)] = 1
logProgress()
return nil
})
if err != nil {
return wrapErr(err, "error reading mint events")
}
// Make sure all addresses are accounted for by iterating over
// the OVM ETH contract's state, and panicking if we miss
// any storage keys. We also keep track of the total amount of
// OVM ETH found, and diff that against the total supply of
// OVM ETH specified in the contract.
backingStateDB := state.NewDatabase(db)
stateDB, err := state.New(root, backingStateDB, nil)
if err != nil {
return wrapErr(err, "error opening state DB")
}
storageTrie := stateDB.StorageTrie(OVMETHAddress)
storageIt := trie.NewIterator(storageTrie.NodeIterator(nil))
logProgress = ProgressLogger(10000, "iterating storage keys")
totalFound := new(big.Int)
totalSupply := getOVMETHTotalSupply(stateDB)
for storageIt.Next() {
_, content, _, err := rlp.Split(storageIt.Value)
if err != nil {
panic(err)
}
k := common.BytesToHash(storageTrie.GetKey(storageIt.Key))
v := common.BytesToHash(content)
sType := storageSlotsToMigrate[k]
switch sType {
case 1:
// This slot is a balance, increment totalFound.
totalFound = totalFound.Add(totalFound, v.Big())
case 2:
// This slot is an allowance, ignore it.
continue
default:
slot := new(big.Int).SetBytes(k.Bytes())
// Check if this slot is a variable. If it isn't, and it isn't a
// known missing key, abort
if slot.Cmp(maxSlot) == 1 && !params.KnownMissingKeys[k] {
log.Crit("missed storage key", "k", k.String(), "v", v.String())
}
}
logProgress()
}
// Verify that the total supply is what we expect. We allow a hardcoded
// delta to be specified in the chain params since older regenesis events
// had supply bugs.
delta := new(big.Int).Set(totalSupply)
delta = delta.Sub(delta, totalFound)
if delta.Cmp(params.ExpectedSupplyDelta) != 0 {
log.Crit(
"supply mismatch",
"migrated", totalFound,
"supply", totalSupply,
"delta", delta,
"exp_delta", params.ExpectedSupplyDelta,
)
}
log.Info(
"supply verified OK",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta,
)
log.Info("performing migration")
log.Info("trie dumping started", "root", root)
tr, err := backingStateDB.OpenTrie(root)
if err != nil {
return err
}
it := trie.NewIterator(tr.NodeIterator(nil))
totalMigrated := new(big.Int)
logAccountProgress := ProgressLogger(1000, "imported accounts")
migratedAccounts := make(map[common.Address]bool)
for it.Next() {
// It's up to us to decode trie data.
var data types.StateAccount
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
panic(err)
}
addrBytes := tr.GetKey(it.Key)
addr := common.BytesToAddress(addrBytes)
migratedAccounts[addr] = true
// Get the OVM ETH balance based on the address's storage key.
ovmBalance := getOVMETHBalance(stateDB, addr)
// No accounts should have a balance in state. If they do, bail.
if data.Balance.Sign() > 0 {
log.Crit("account has non-zero balance in state - should never happen", "addr", addr)
}
// Actually perform the migration by setting the appropriate values in state.
stateDB.SetBalance(addr, ovmBalance)
stateDB.SetState(predeploys.LegacyERC20ETHAddr, CalcOVMETHStorageKey(addr), common.Hash{})
// Bump the total OVM balance.
totalMigrated = totalMigrated.Add(totalMigrated, ovmBalance)
logAccountProgress()
}
// Take care of nonce zero accounts with balances. These are accounts
// that received OVM ETH as part of the regenesis, but never actually
// transacted on-chain.
logNonceZeroProgress := ProgressLogger(1000, "imported zero nonce accounts")
log.Info("importing accounts with zero-nonce balances")
for addr := range addressesToMigrate {
if migratedAccounts[addr] {
continue
}
ovmBalance := getOVMETHBalance(stateDB, addr)
totalMigrated = totalMigrated.Add(totalMigrated, ovmBalance)
stateDB.AddBalance(addr, ovmBalance)
stateDB.SetState(predeploys.LegacyERC20ETHAddr, CalcOVMETHStorageKey(addr), common.Hash{})
logNonceZeroProgress()
}
// Make sure that the amount we migrated matches the amount in
// our original state.
if totalMigrated.Cmp(totalFound) != 0 {
log.Crit(
"total migrated does not equal total OVM eth found",
"migrated", totalMigrated,
"found", totalFound,
)
}
// Set the total supply to 0
stateDB.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
return nil
}
package genesis
import (
"fmt"
"math/big"
"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/ether"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"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/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/trie"
)
var abiTrue = common.Hash{31: 0x01}
// MigrateDB will migrate an old l2geth database to the new bedrock style system
func MigrateDB(db vm.StateDB, config *DeployConfig, l1Block *types.Block, l2Addrs *L2Addresses, migrationData *MigrationData) error {
func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, l2Addrs *L2Addresses, migrationData *migration.MigrationData, commit bool) error {
hash := rawdb.ReadHeadHeaderHash(ldb)
num := rawdb.ReadHeaderNumber(ldb, hash)
header := rawdb.ReadHeader(ldb, hash, *num)
db, err := state.New(header.Root, state.NewDatabase(ldb), nil)
if err != nil {
return err
}
// Convert all of the messages into legacy withdrawals
withdrawals, err := migrationData.ToWithdrawals()
if err != nil {
return err
}
if err := CheckWithdrawals(db, withdrawals); err != nil {
return err
}
// Now start the migration
if err := SetL2Proxies(db); err != nil {
return err
}
......@@ -26,28 +59,97 @@ func MigrateDB(db vm.StateDB, config *DeployConfig, l1Block *types.Block, l2Addr
return err
}
// Convert all of the messages into legacy withdrawals
messages := make([]*crossdomain.LegacyWithdrawal, 0)
for _, msg := range migrationData.OvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
return err
}
messages = append(messages, wd)
err = crossdomain.MigrateWithdrawals(withdrawals, db, &l2Addrs.L1CrossDomainMessengerProxy, &l2Addrs.L1StandardBridgeProxy)
if err != nil {
return err
}
for _, msg := range migrationData.EvmMessages {
wd, err := msg.ToLegacyWithdrawal()
addrs := migrationData.Addresses()
if err := ether.MigrateLegacyETH(ldb, addrs, migrationData.OvmAllowances, int(config.L1ChainID)); err != nil {
return err
}
if !commit {
return nil
}
root, err := db.Commit(true)
if err != nil {
return err
}
// Create the bedrock transition block
bedrockHeader := &types.Header{
ParentHash: header.Hash(),
UncleHash: types.EmptyUncleHash,
Coinbase: config.L2GenesisBlockCoinbase,
Root: root,
TxHash: types.EmptyRootHash,
ReceiptHash: types.EmptyRootHash,
Bloom: types.Bloom{},
Difficulty: (*big.Int)(config.L2GenesisBlockDifficulty),
Number: new(big.Int).Add(header.Number, common.Big1),
GasLimit: (uint64)(config.L2GenesisBlockGasLimit),
GasUsed: (uint64)(config.L2GenesisBlockGasUsed),
Time: uint64(config.L2OutputOracleStartingTimestamp),
Extra: config.L2GenesisBlockExtraData,
MixDigest: config.L2GenesisBlockMixHash,
Nonce: types.EncodeNonce((uint64)(config.L1GenesisBlockNonce)),
BaseFee: (*big.Int)(config.L2GenesisBlockBaseFeePerGas),
}
block := types.NewBlock(bedrockHeader, nil, nil, nil, trie.NewStackTrie(nil))
rawdb.WriteTd(ldb, block.Hash(), block.NumberU64(), block.Difficulty())
rawdb.WriteBlock(ldb, block)
rawdb.WriteReceipts(ldb, block.Hash(), block.NumberU64(), nil)
rawdb.WriteCanonicalHash(ldb, block.Hash(), block.NumberU64())
rawdb.WriteHeadBlockHash(ldb, block.Hash())
rawdb.WriteHeadFastBlockHash(ldb, block.Hash())
rawdb.WriteHeadHeaderHash(ldb, block.Hash())
return nil
}
// CheckWithdrawals will ensure that the entire list of withdrawals is being
// operated on during the database migration.
func CheckWithdrawals(db vm.StateDB, withdrawals []*crossdomain.LegacyWithdrawal) error {
// Create a mapping of all of their storage slots
knownSlots := make(map[common.Hash]bool)
for _, wd := range withdrawals {
slot, err := wd.StorageSlot()
if err != nil {
return err
}
messages = append(messages, wd)
knownSlots[slot] = true
}
if err := crossdomain.MigrateWithdrawals(messages, db, &l2Addrs.L1CrossDomainMessengerProxy, &l2Addrs.L1StandardBridgeProxy); err != nil {
// Build a map of all the slots in the LegacyMessagePasser
slots := make(map[common.Hash]bool)
err := db.ForEachStorage(predeploys.LegacyMessagePasserAddr, func(key, value common.Hash) bool {
if value != abiTrue {
return false
}
slots[key] = true
return true
})
if err != nil {
return err
}
// TODO: use migration data to double check things
// Check that all of the slots from storage correspond to a known message
for slot := range slots {
_, ok := knownSlots[slot]
if !ok {
return fmt.Errorf("Unknown storage slot in state: %s", slot)
}
}
// Check that all of the input messages are legit
for slot := range knownSlots {
_, ok := slots[slot]
if !ok {
return fmt.Errorf("Unknown input message: %s", slot)
}
}
return nil
}
package genesis
package migration
import (
"encoding/json"
......@@ -116,3 +116,36 @@ type MigrationData struct {
// L2CrossDomainMessenger from after the evm equivalence upgrade
EvmMessages []*SentMessage
}
func (m *MigrationData) ToWithdrawals() ([]*crossdomain.LegacyWithdrawal, error) {
messages := make([]*crossdomain.LegacyWithdrawal, 0)
for _, msg := range m.OvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
return nil, err
}
messages = append(messages, wd)
if err != nil {
return nil, err
}
}
for _, msg := range m.EvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
return nil, err
}
messages = append(messages, wd)
}
return messages, 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.EvmAddresses {
addresses = append(addresses, addr)
}
return addresses
}
......@@ -3,7 +3,6 @@ module github.com/ethereum-optimism/optimism/op-chain-ops
go 1.18
require (
github.com/ethereum-optimism/optimism/l2geth v0.0.0-20220820030939-de38b6f6f77e
github.com/ethereum-optimism/optimism/op-bindings v0.8.10
github.com/ethereum/go-ethereum v1.10.23
github.com/holiman/uint256 v1.2.0
......@@ -16,45 +15,35 @@ require (
require (
github.com/VictoriaMetrics/fastcache v1.9.0 // indirect
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847 // indirect
github.com/btcsuite/btcd v0.22.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/elastic/gosigar v0.12.0 // indirect
github.com/edsrzf/mmap-go v1.0.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/go-bexpr v0.1.11 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/tsdb v0.10.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rjeczalik/notify v0.9.2 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 // indirect
github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
golang.org/x/sys v0.0.0-20220701225701-179beb0bd1a1 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
......
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