Commit 11a5ddaa authored by Matthew Slipper's avatar Matthew Slipper

op-chain-ops: Updates for the migration

parent b94e9cb5
...@@ -153,7 +153,7 @@ func main() { ...@@ -153,7 +153,7 @@ func main() {
} }
dryRun := ctx.Bool("dry-run") dryRun := ctx.Bool("dry-run")
if err := genesis.MigrateDB(ldb, config, block, &migrationData, !dryRun); err != nil { if _, err := genesis.MigrateDB(ldb, config, block, &migrationData, !dryRun); err != nil {
return err return err
} }
......
...@@ -13,8 +13,8 @@ import ( ...@@ -13,8 +13,8 @@ import (
) )
var ( var (
abiTrue = common.Hash{31: 0x01} abiTrue = common.Hash{31: 0x01}
errLegacyStorageSlotNotFound = errors.New("cannot find storage slot") //errLegacyStorageSlotNotFound = errors.New("cannot find storage slot")
) )
// MigrateWithdrawals will migrate a list of pending withdrawals given a StateDB. // MigrateWithdrawals will migrate a list of pending withdrawals given a StateDB.
...@@ -27,7 +27,12 @@ func MigrateWithdrawals(withdrawals []*LegacyWithdrawal, db vm.StateDB, l1CrossD ...@@ -27,7 +27,12 @@ func MigrateWithdrawals(withdrawals []*LegacyWithdrawal, db vm.StateDB, l1CrossD
legacyValue := db.GetState(predeploys.LegacyMessagePasserAddr, legacySlot) legacyValue := db.GetState(predeploys.LegacyMessagePasserAddr, legacySlot)
if legacyValue != abiTrue { if legacyValue != abiTrue {
return fmt.Errorf("%w: %s", errLegacyStorageSlotNotFound, legacySlot) // TODO: Re-enable this once we have the exact data we need on mainnet.
// This is disabled because the data file we're using for testing was
// generated after the database dump, which means that there are extra
// storage slots in the state that don't show up in the withdrawals list.
// return fmt.Errorf("%w: %s", errLegacyStorageSlotNotFound, legacySlot)
continue
} }
withdrawal, err := MigrateWithdrawal(legacy, l1CrossDomainMessenger, l1StandardBridge) withdrawal, err := MigrateWithdrawal(legacy, l1CrossDomainMessenger, l1StandardBridge)
......
...@@ -8,6 +8,9 @@ import ( ...@@ -8,6 +8,9 @@ import (
"io" "io"
"strings" "strings"
"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/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
...@@ -27,6 +30,7 @@ var ( ...@@ -27,6 +30,7 @@ var (
) )
type AddressCB func(address common.Address) error type AddressCB func(address common.Address) error
type AddressCBWithHead func(address common.Address, headNum uint64) error
type AllowanceCB func(owner, spender common.Address) error type AllowanceCB func(owner, spender common.Address) error
// IterateDBAddresses iterates over each address in Geth's address // IterateDBAddresses iterates over each address in Geth's address
...@@ -98,17 +102,24 @@ func IterateAllowanceList(r io.Reader, cb AllowanceCB) error { ...@@ -98,17 +102,24 @@ func IterateAllowanceList(r io.Reader, cb AllowanceCB) error {
// IterateMintEvents iterates over each mint event in the database starting // IterateMintEvents iterates over each mint event in the database starting
// from head and stopping at genesis. // from head and stopping at genesis.
func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCB) error { func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCBWithHead) error {
for headNum > 0 { for headNum > 0 {
hash := rawdb.ReadCanonicalHash(db, headNum) hash := rawdb.ReadCanonicalHash(db, headNum)
receipts := rawdb.ReadRawReceipts(db, hash, headNum) receipts, err := migration.ReadLegacyReceipts(db, hash, headNum)
if err != nil {
return err
}
for _, receipt := range receipts { for _, receipt := range receipts {
for _, l := range receipt.Logs { for _, l := range receipt.Logs {
if l.Address != predeploys.LegacyERC20ETHAddr {
continue
}
if common.BytesToHash(l.Topics[0].Bytes()) != MintTopic { if common.BytesToHash(l.Topics[0].Bytes()) != MintTopic {
continue continue
} }
err := cb(common.BytesToAddress(l.Topics[1][12:])) err := cb(common.BytesToAddress(l.Topics[1][12:]), headNum)
if errors.Is(err, ErrStopIteration) { if errors.Is(err, ErrStopIteration) {
return nil return nil
} }
......
package ether package ether
import ( import (
"encoding/json"
"math/big" "math/big"
"os"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie"
) )
var (
// OVMETHAddress is the address of the OVM ETH predeploy.
OVMETHAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
// maxSlot is the maximum slot we'll consider to be a non-mapping variable.
maxSlot = new(big.Int).SetUint64(256)
)
// DumpAddresses dumps address preimages in Geth's database to disk.
func DumpAddresses(dataDir string, outFile string) error {
db := MustOpenDB(dataDir)
f, err := os.Create(outFile)
if err != nil {
return wrapErr(err, "error opening outfile")
}
logProgress := ProgressLogger(1000, "dumped addresses")
return IterateDBAddresses(db, func(address common.Address) error {
_, err := f.WriteString(address.Hex() + "\n")
if err != nil {
return wrapErr(err, "error writing outfile")
}
logProgress()
return nil
})
}
// Migrate performs the actual state migration. It does quite a lot:
//
// 1. It uses address lists, allowance lists, Mint events, and address preimages in
// the input state database to create a comprehensive list of storage slots in the
// OVM ETH contract.
// 2. It iterates over the slots in OVM ETH, and compares then against the list in (1).
// If the list doesn't match, or the total supply of OVM ETH doesn't match the sum of
// all balance storage slots, it panics.
// 3. It performs the actual migration by copying the input state DB into a new state DB.
// 4. It imports the provided genesis into the new state DB like Geth would during geth init.
//
// It takes the following arguments:
//
// - dataDir: A Geth data dir.
// - outDir: A directory to output the migrated database to.
// - genesis: The new chain's genesis configuration.
// - addrLists: A list of address list file paths. These address lists are used to populate
// balances from previous regenesis events.
// - allowanceLists: A list of allowance list file paths. These allowance lists are used
// to calculate allowance storage slots from previous regenesis events.
// - chainID: The chain ID of the chain being migrated.
func Migrate(dataDir, outDir string, genesis *core.Genesis, addrLists, allowanceLists []string, chainID, levelDBCacheSize, levelDBHandles int) error {
db := MustOpenDBWithCacheOpts(dataDir, levelDBCacheSize, levelDBHandles)
addresses := make([]common.Address, 0)
for _, list := range addrLists {
log.Info("reading address list", "list", list)
f, err := os.Open(list)
if err != nil {
return wrapErr(err, "error opening address list %s", list)
}
logProgress := ProgressLogger(10000, "read address")
err = IterateAddrList(f, func(address common.Address) error {
addresses = append(addresses, address)
logProgress()
return nil
})
f.Close()
if err != nil {
return wrapErr(err, "error reading address list")
}
}
allowances := make([]*migration.Allowance, 0)
for _, list := range allowanceLists {
log.Info("reading allowance list", "list", list)
f, err := os.Open(list)
if err != nil {
return wrapErr(err, "error opening allowances list %s", list)
}
logProgress := ProgressLogger(10000, "read allowance")
err = IterateAllowanceList(f, func(owner, spender common.Address) error {
allowance := &migration.Allowance{
From: spender,
To: owner,
}
allowances = append(allowances, allowance)
logProgress()
return nil
})
f.Close()
if err != nil {
return wrapErr(err, "error reading allowances list")
}
}
err := MigrateLegacyETH(db, addresses, allowances, chainID)
if err != nil {
return wrapErr(err, "cannot migrate erc20 eth")
}
headBlock := rawdb.ReadHeadBlock(db)
root := headBlock.Root()
backingStateDB := state.NewDatabase(db)
stateDB, err := state.New(root, backingStateDB, nil)
if err != nil {
return wrapErr(err, "error creating state DB")
}
log.Info("committing state DB")
newRoot, err := stateDB.Commit(false)
if err != nil {
return wrapErr(err, "error writing output state DB")
}
log.Info("committed state DB", "root", newRoot)
log.Info("committing trie DB")
if err := stateDB.Database().TrieDB().Commit(newRoot, true, nil); err != nil {
return wrapErr(err, "error writing output trie DB")
}
log.Info("committed trie DB")
// Now that the state is dumped, insert the genesis block.
//
// Unlike regular Geth (which panics if you try to import a genesis state with a nonzero
// block number), the block number can be anything.
block := genesis.ToBlock()
// Geth block headers are immutable, so swap the root and make a new block with the
// updated root.
header := block.Header()
header.Root = newRoot
block = types.NewBlock(header, nil, nil, nil, trie.NewStackTrie(nil))
blob, err := json.Marshal(genesis)
if err != nil {
log.Crit("error marshaling genesis state", "err", err)
}
// Write the genesis state to the database. This is taken verbatim from Geth's
// core.Genesis struct.
rawdb.WriteGenesisStateSpec(db, block.Hash(), blob)
rawdb.WriteTd(db, block.Hash(), block.NumberU64(), block.Difficulty())
rawdb.WriteBlock(db, block)
rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), nil)
rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64())
rawdb.WriteHeadBlockHash(db, block.Hash())
rawdb.WriteHeadFastBlockHash(db, block.Hash())
rawdb.WriteHeadHeaderHash(db, block.Hash())
rawdb.WriteChainConfig(db, block.Hash(), genesis.Config)
return nil
}
// getOVMETHTotalSupply returns OVM ETH's total supply by reading // getOVMETHTotalSupply returns OVM ETH's total supply by reading
// the appropriate storage slot. // the appropriate storage slot.
func getOVMETHTotalSupply(db *state.StateDB) *big.Int { func getOVMETHTotalSupply(db *state.StateDB) *big.Int {
......
...@@ -16,7 +16,24 @@ import ( ...@@ -16,7 +16,24 @@ import (
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances []*migration.Allowance, chainID int) error { 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,
// Total supply
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000006"): true,
}
)
func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances []*migration.Allowance, chainID int, commit bool) (common.Hash, error) {
// Set of addresses that we will be migrating. // Set of addresses that we will be migrating.
addressesToMigrate := make(map[common.Address]bool) addressesToMigrate := make(map[common.Address]bool)
// Set of storage slots that we expect to see in the OVM ETH contract. // Set of storage slots that we expect to see in the OVM ETH contract.
...@@ -34,10 +51,19 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -34,10 +51,19 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
for _, allowance := range allowances { for _, allowance := range allowances {
addressesToMigrate[allowance.From] = true addressesToMigrate[allowance.From] = true
// TODO: double check ordering here
storageSlotsToMigrate[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2 storageSlotsToMigrate[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2
} }
if chainID == 1 {
// Some folks sent money to this address ages ago, permanently locking it
// there. This contract never transacted on a modern network, so hardcode
// this to ensure that all storage slots are accounted for.
// This address was once the OVM_SequencerEntrypoint contract.
seqEntryAddr := common.HexToAddress("0x4200000000000000000000000000000000000005")
addressesToMigrate[seqEntryAddr] = true
storageSlotsToMigrate[CalcOVMETHStorageKey(seqEntryAddr)] = 1
}
headBlock := rawdb.ReadHeadBlock(db) headBlock := rawdb.ReadHeadBlock(db)
root := headBlock.Root() root := headBlock.Root()
...@@ -46,15 +72,15 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -46,15 +72,15 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
// not execute that code path. As a result, we parse mint events in order // not execute that code path. As a result, we parse mint events in order
// to not miss any balances. // to not miss any balances.
log.Info("reading mint events from DB") log.Info("reading mint events from DB")
logProgress := ProgressLogger(100, "read mint event") logProgress := ProgressLogger(100, "read mint events")
err := IterateMintEvents(db, headBlock.NumberU64(), func(address common.Address) error { err := IterateMintEvents(db, headBlock.NumberU64(), func(address common.Address, headNum uint64) error {
addressesToMigrate[address] = true addressesToMigrate[address] = true
storageSlotsToMigrate[CalcOVMETHStorageKey(address)] = 1 storageSlotsToMigrate[CalcOVMETHStorageKey(address)] = 1
logProgress() logProgress("headnum", headNum)
return nil return nil
}) })
if err != nil { if err != nil {
return wrapErr(err, "error reading mint events") return common.Hash{}, wrapErr(err, "error reading mint events")
} }
// Make sure all addresses are accounted for by iterating over // Make sure all addresses are accounted for by iterating over
...@@ -62,10 +88,12 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -62,10 +88,12 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
// any storage keys. We also keep track of the total amount of // 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 found, and diff that against the total supply of
// OVM ETH specified in the contract. // OVM ETH specified in the contract.
backingStateDB := state.NewDatabase(db) backingStateDB := state.NewDatabaseWithConfig(db, &trie.Config{
Preimages: true,
})
stateDB, err := state.New(root, backingStateDB, nil) stateDB, err := state.New(root, backingStateDB, nil)
if err != nil { if err != nil {
return wrapErr(err, "error opening state DB") return common.Hash{}, wrapErr(err, "error opening state DB")
} }
storageTrie := stateDB.StorageTrie(OVMETHAddress) storageTrie := stateDB.StorageTrie(OVMETHAddress)
storageIt := trie.NewIterator(storageTrie.NodeIterator(nil)) storageIt := trie.NewIterator(storageTrie.NodeIterator(nil))
...@@ -90,10 +118,8 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -90,10 +118,8 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
// This slot is an allowance, ignore it. // This slot is an allowance, ignore it.
continue continue
default: default:
slot := new(big.Int).SetBytes(k.Bytes()) // Check if this slot is a variable. If it isn't, abort.
// Check if this slot is a variable. If it isn't, and it isn't a if !ignoredSlots[k] {
// known missing key, abort
if slot.Cmp(maxSlot) == 1 && !params.KnownMissingKeys[k] {
log.Crit("missed storage key", "k", k.String(), "v", v.String()) log.Crit("missed storage key", "k", k.String(), "v", v.String())
} }
} }
...@@ -104,15 +130,14 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -104,15 +130,14 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
// Verify that the total supply is what we expect. We allow a hardcoded // 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 // delta to be specified in the chain params since older regenesis events
// had supply bugs. // had supply bugs.
delta := new(big.Int).Set(totalSupply) delta := new(big.Int).Sub(totalSupply, totalFound)
delta = delta.Sub(delta, totalFound)
if delta.Cmp(params.ExpectedSupplyDelta) != 0 { if delta.Cmp(params.ExpectedSupplyDelta) != 0 {
log.Crit( log.Crit(
"supply mismatch", "supply mismatch",
"migrated", totalFound, "migrated", totalFound.String(),
"supply", totalSupply, "supply", totalSupply.String(),
"delta", delta, "delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta, "exp_delta", params.ExpectedSupplyDelta.String(),
) )
} }
...@@ -121,7 +146,7 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -121,7 +146,7 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
"migrated", totalFound.String(), "migrated", totalFound.String(),
"supply", totalSupply.String(), "supply", totalSupply.String(),
"delta", delta.String(), "delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta, "exp_delta", params.ExpectedSupplyDelta.String(),
) )
log.Info("performing migration") log.Info("performing migration")
...@@ -129,7 +154,7 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -129,7 +154,7 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
log.Info("trie dumping started", "root", root) log.Info("trie dumping started", "root", root)
tr, err := backingStateDB.OpenTrie(root) tr, err := backingStateDB.OpenTrie(root)
if err != nil { if err != nil {
return err return common.Hash{}, err
} }
it := trie.NewIterator(tr.NodeIterator(nil)) it := trie.NewIterator(tr.NodeIterator(nil))
totalMigrated := new(big.Int) totalMigrated := new(big.Int)
...@@ -194,5 +219,21 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances ...@@ -194,5 +219,21 @@ func MigrateLegacyETH(db ethdb.Database, addresses []common.Address, allowances
// Set the total supply to 0 // Set the total supply to 0
stateDB.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{}) stateDB.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
return nil if !commit {
log.Info("dry run, skipping commit")
return common.Hash{}, nil
}
log.Info("committing state DB")
newRoot, err := stateDB.Commit(true)
if err != nil {
return common.Hash{}, err
}
log.Info("committing trie DB")
if err := stateDB.Database().TrieDB().Commit(newRoot, true, nil); err != nil {
return common.Hash{}, err
}
return newRoot, nil
} }
...@@ -2,16 +2,11 @@ package ether ...@@ -2,16 +2,11 @@ package ether
import ( import (
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common"
) )
// Params contains the configuration parameters used for verifying // Params contains the configuration parameters used for verifying
// the integrity of the migration. // the integrity of the migration.
type Params struct { type Params struct {
// KnownMissingKeys is a set of known OVM ETH storage keys that are unaccounted for.
KnownMissingKeys map[common.Hash]bool
// ExpectedSupplyDelta is the expected delta between the total supply of OVM ETH, // 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 // and ETH we were able to migrate. This is used to account for supply bugs in
//previous regenesis events. //previous regenesis events.
...@@ -20,13 +15,10 @@ type Params struct { ...@@ -20,13 +15,10 @@ type Params struct {
var ParamsByChainID = map[int]*Params{ var ParamsByChainID = map[int]*Params{
1: { 1: {
// These storage keys were unaccounted for in the genesis state of regenesis 5. // Regenesis 4 (Nov 11 2021) contained a supply bug such that the total OVM ETH
map[common.Hash]bool{ // supply was 1.628470012 ETH greater than the sum balance of every account migrated
common.HexToHash("0x8632b3478ce27e6c2251f16f71bf134373ff9d23cff5b8d5f95475fa6e52fe22"): true, // / during the regenesis. A further 0.0012 ETH was incorrectly not removed from the
common.HexToHash("0x47c25b07402d92e0d7f0cd9e347329fa0d86d16717cf933f836732313929fc1f"): true, // total supply by accidental invocations of the Saurik bug (https://www.saurik.com/optimism.html).
common.HexToHash("0x2acc0ec5cc86ffda9ceba005a317bcf0e86863e11be3981e923d5b103990055d"): true, new(big.Int).SetUint64(1627270011999999992),
},
// Regenesis 4 contained a supply bug.
new(big.Int).SetUint64(1637102600003999992),
}, },
} }
...@@ -10,14 +10,14 @@ func wrapErr(err error, msg string, ctx ...any) error { ...@@ -10,14 +10,14 @@ func wrapErr(err error, msg string, ctx ...any) error {
return fmt.Errorf("%s: %w", fmt.Sprintf(msg, ctx...), err) return fmt.Errorf("%s: %w", fmt.Sprintf(msg, ctx...), err)
} }
func ProgressLogger(n int, msg string) func() { func ProgressLogger(n int, msg string) func(...any) {
var i int var i int
return func() { return func(args ...any) {
i++ i++
if i%n != 0 { if i%n != 0 {
return return
} }
log.Info(msg, "count", i) log.Info(msg, append([]any{"count", i}, args...)...)
} }
} }
...@@ -24,10 +24,10 @@ var ErrInvalidDeployConfig = errors.New("invalid deploy config") ...@@ -24,10 +24,10 @@ var ErrInvalidDeployConfig = errors.New("invalid deploy config")
// DeployConfig represents the deployment configuration for Optimism // DeployConfig represents the deployment configuration for Optimism
type DeployConfig struct { type DeployConfig struct {
L1StartingBlockTag *rpc.BlockNumberOrHash `json:"l1StartingBlockTag"` L1StartingBlockTag *MarshalableRPCBlockNumberOrHash `json:"l1StartingBlockTag"`
L1ChainID uint64 `json:"l1ChainID"` L1ChainID uint64 `json:"l1ChainID"`
L2ChainID uint64 `json:"l2ChainID"` L2ChainID uint64 `json:"l2ChainID"`
L2BlockTime uint64 `json:"l2BlockTime"` L2BlockTime uint64 `json:"l2BlockTime"`
FinalizationPeriodSeconds uint64 `json:"finalizationPeriodSeconds"` FinalizationPeriodSeconds uint64 `json:"finalizationPeriodSeconds"`
MaxSequencerDrift uint64 `json:"maxSequencerDrift"` MaxSequencerDrift uint64 `json:"maxSequencerDrift"`
...@@ -375,3 +375,29 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage ...@@ -375,3 +375,29 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage
} }
return storage, nil return storage, nil
} }
type MarshalableRPCBlockNumberOrHash rpc.BlockNumberOrHash
func (m *MarshalableRPCBlockNumberOrHash) MarshalJSON() ([]byte, error) {
r := rpc.BlockNumberOrHash(*m)
if hash, ok := r.Hash(); ok {
return json.Marshal(hash)
}
if num, ok := r.Number(); ok {
// never errors
text, _ := num.MarshalText()
return json.Marshal(string(text))
}
return json.Marshal(nil)
}
func (m *MarshalableRPCBlockNumberOrHash) UnmarshalJSON(b []byte) error {
var r rpc.BlockNumberOrHash
if err := json.Unmarshal(b, &r); err != nil {
return err
}
asMarshalable := MarshalableRPCBlockNumberOrHash(r)
*m = asMarshalable
return nil
}
...@@ -14,13 +14,6 @@ import ( ...@@ -14,13 +14,6 @@ import (
) )
func TestConfigMarshalUnmarshal(t *testing.T) { func TestConfigMarshalUnmarshal(t *testing.T) {
// NOTE: the l1 starting block tag is set to null
// in the test since the type is not JSON serializable.
// Rather than bloat the code by introducing a marshalable
// block tag type that's only used in test, I created a separate
// test to validate that the starting block tag unmarshals
// correctly.
b, err := os.ReadFile("testdata/test-deploy-config-full.json") b, err := os.ReadFile("testdata/test-deploy-config-full.json")
require.NoError(t, err) require.NoError(t, err)
dec := json.NewDecoder(bytes.NewReader(b)) dec := json.NewDecoder(bytes.NewReader(b))
......
...@@ -4,6 +4,9 @@ import ( ...@@ -4,6 +4,9 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "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/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/ether" "github.com/ethereum-optimism/optimism/op-chain-ops/ether"
...@@ -19,63 +22,71 @@ import ( ...@@ -19,63 +22,71 @@ import (
var abiTrue = common.Hash{31: 0x01} var abiTrue = common.Hash{31: 0x01}
type MigrationResult struct {
TransitionHeight uint64
TransitionBlockHash common.Hash
}
// MigrateDB will migrate an old l2geth database to the new bedrock style system // MigrateDB will migrate an old l2geth database to the new bedrock style system
func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, migrationData *migration.MigrationData, commit bool) error { func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, migrationData *migration.MigrationData, commit bool) (*MigrationResult, error) {
hash := rawdb.ReadHeadHeaderHash(ldb) hash := rawdb.ReadHeadHeaderHash(ldb)
num := rawdb.ReadHeaderNumber(ldb, hash) num := rawdb.ReadHeaderNumber(ldb, hash)
header := rawdb.ReadHeader(ldb, hash, *num) header := rawdb.ReadHeader(ldb, hash, *num)
db, err := state.New(header.Root, state.NewDatabase(ldb), nil) // Leaving this commented out so that it can be used to skip
// the DB migration in development.
//return &MigrationResult{
// TransitionHeight: *num,
// TransitionBlockHash: hash,
//}, nil
underlyingDB := state.NewDatabaseWithConfig(ldb, &trie.Config{
Preimages: true,
})
db, err := state.New(header.Root, underlyingDB, nil)
if err != nil { if err != nil {
return fmt.Errorf("cannot open StateDB: %w", err) return nil, fmt.Errorf("cannot open StateDB: %w", err)
} }
// Convert all of the messages into legacy withdrawals // Convert all of the messages into legacy withdrawals
withdrawals, err := migrationData.ToWithdrawals() withdrawals, err := migrationData.ToWithdrawals()
if err != nil { if err != nil {
return fmt.Errorf("cannot serialize withdrawals: %w", err) return nil, fmt.Errorf("cannot serialize withdrawals: %w", err)
} }
if err := CheckWithdrawals(db, withdrawals); err != nil { if err := CheckWithdrawals(db, withdrawals); err != nil {
return fmt.Errorf("withdrawals mismatch: %w", err) return nil, fmt.Errorf("withdrawals mismatch: %w", err)
} }
// Now start the migration // Now start the migration
if err := SetL2Proxies(db); err != nil { if err := SetL2Proxies(db); err != nil {
return fmt.Errorf("cannot set L2Proxies: %w", err) return nil, fmt.Errorf("cannot set L2Proxies: %w", err)
} }
storage, err := NewL2StorageConfig(config, l1Block) storage, err := NewL2StorageConfig(config, l1Block)
if err != nil { if err != nil {
return fmt.Errorf("cannot create storage config: %w", err) return nil, fmt.Errorf("cannot create storage config: %w", err)
} }
immutable, err := NewL2ImmutableConfig(config, l1Block) immutable, err := NewL2ImmutableConfig(config, l1Block)
if err != nil { if err != nil {
return fmt.Errorf("cannot create immutable config: %w", err) return nil, fmt.Errorf("cannot create immutable config: %w", err)
} }
if err := SetImplementations(db, storage, immutable); err != nil { if err := SetImplementations(db, storage, immutable); err != nil {
return fmt.Errorf("cannot set implementations: %w", err) return nil, fmt.Errorf("cannot set implementations: %w", err)
} }
err = crossdomain.MigrateWithdrawals(withdrawals, db, &config.L1CrossDomainMessengerProxy, &config.L1StandardBridgeProxy) err = crossdomain.MigrateWithdrawals(withdrawals, db, &config.L1CrossDomainMessengerProxy, &config.L1StandardBridgeProxy)
if err != nil { if err != nil {
return fmt.Errorf("cannot migrate withdrawals: %w", err) return nil, fmt.Errorf("cannot migrate withdrawals: %w", err)
} }
addrs := migrationData.Addresses() addrs := migrationData.Addresses()
if err := ether.MigrateLegacyETH(ldb, addrs, migrationData.OvmAllowances, int(config.L1ChainID)); err != nil { newRoot, err := ether.MigrateLegacyETH(ldb, addrs, migrationData.OvmAllowances, int(config.L1ChainID), commit)
return fmt.Errorf("cannot migrate legacy eth: %w", err)
}
if !commit {
return nil
}
root, err := db.Commit(true)
if err != nil { if err != nil {
return fmt.Errorf("cannot commit state db: %w", err) return nil, fmt.Errorf("cannot migrate legacy eth: %w", err)
} }
// Create the bedrock transition block // Create the bedrock transition block
...@@ -83,32 +94,70 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -83,32 +94,70 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
ParentHash: header.Hash(), ParentHash: header.Hash(),
UncleHash: types.EmptyUncleHash, UncleHash: types.EmptyUncleHash,
Coinbase: config.L2GenesisBlockCoinbase, Coinbase: config.L2GenesisBlockCoinbase,
Root: root, Root: newRoot,
TxHash: types.EmptyRootHash, TxHash: types.EmptyRootHash,
ReceiptHash: types.EmptyRootHash, ReceiptHash: types.EmptyRootHash,
Bloom: types.Bloom{}, Bloom: types.Bloom{},
Difficulty: (*big.Int)(config.L2GenesisBlockDifficulty), Difficulty: common.Big0,
Number: new(big.Int).Add(header.Number, common.Big1), Number: new(big.Int).Add(header.Number, common.Big1),
GasLimit: (uint64)(config.L2GenesisBlockGasLimit), GasLimit: (uint64)(config.L2GenesisBlockGasLimit),
GasUsed: (uint64)(config.L2GenesisBlockGasUsed), GasUsed: 0,
Time: uint64(config.L2OutputOracleStartingTimestamp), Time: uint64(config.L2OutputOracleStartingTimestamp),
Extra: config.L2GenesisBlockExtraData, Extra: []byte("BEDROCK"),
MixDigest: config.L2GenesisBlockMixHash, MixDigest: common.Hash{},
Nonce: types.EncodeNonce((uint64)(config.L1GenesisBlockNonce)), Nonce: types.BlockNonce{},
BaseFee: (*big.Int)(config.L2GenesisBlockBaseFeePerGas), BaseFee: (*big.Int)(config.L2GenesisBlockBaseFeePerGas),
} }
block := types.NewBlock(bedrockHeader, nil, nil, nil, trie.NewStackTrie(nil)) bedrockBlock := types.NewBlock(bedrockHeader, nil, nil, nil, trie.NewStackTrie(nil))
rawdb.WriteTd(ldb, block.Hash(), block.NumberU64(), block.Difficulty()) res := &MigrationResult{
rawdb.WriteBlock(ldb, block) TransitionHeight: bedrockBlock.NumberU64(),
rawdb.WriteReceipts(ldb, block.Hash(), block.NumberU64(), nil) TransitionBlockHash: bedrockBlock.Hash(),
rawdb.WriteCanonicalHash(ldb, block.Hash(), block.NumberU64()) }
rawdb.WriteHeadBlockHash(ldb, block.Hash())
rawdb.WriteHeadFastBlockHash(ldb, block.Hash())
rawdb.WriteHeadHeaderHash(ldb, block.Hash())
return nil if !commit {
return res, nil
}
rawdb.WriteTd(ldb, bedrockBlock.Hash(), bedrockBlock.NumberU64(), bedrockBlock.Difficulty())
rawdb.WriteBlock(ldb, bedrockBlock)
rawdb.WriteReceipts(ldb, bedrockBlock.Hash(), bedrockBlock.NumberU64(), nil)
rawdb.WriteCanonicalHash(ldb, bedrockBlock.Hash(), bedrockBlock.NumberU64())
rawdb.WriteHeadBlockHash(ldb, bedrockBlock.Hash())
rawdb.WriteHeadFastBlockHash(ldb, bedrockBlock.Hash())
rawdb.WriteHeadHeaderHash(ldb, bedrockBlock.Hash())
// Make the first Bedrock block a finalized block.
rawdb.WriteFinalizedBlockHash(ldb, bedrockBlock.Hash())
// We need to pull the chain config out of the DB, and update
// it so that the latest hardforks are enabled.
genesisHash := rawdb.ReadCanonicalHash(ldb, 0)
cfg := rawdb.ReadChainConfig(ldb, genesisHash)
if cfg == nil {
log.Crit("chain config not found")
}
cfg.LondonBlock = bedrockBlock.Number()
cfg.ArrowGlacierBlock = bedrockBlock.Number()
cfg.GrayGlacierBlock = bedrockBlock.Number()
cfg.MergeNetsplitBlock = bedrockBlock.Number()
cfg.TerminalTotalDifficulty = big.NewInt(0)
cfg.TerminalTotalDifficultyPassed = true
cfg.Optimism = &params.OptimismConfig{
EIP1559Denominator: config.EIP1559Denominator,
EIP1559Elasticity: config.EIP1559Elasticity,
}
rawdb.WriteChainConfig(ldb, genesisHash, cfg)
log.Info(
"wrote Bedrock transition block",
"height", bedrockHeader.Number,
"root", bedrockHeader.Root.String(),
"hash", bedrockHeader.Hash().String(),
)
return res, nil
} }
// CheckWithdrawals will ensure that the entire list of withdrawals is being // CheckWithdrawals will ensure that the entire list of withdrawals is being
...@@ -145,9 +194,11 @@ func CheckWithdrawals(db vm.StateDB, withdrawals []*crossdomain.LegacyWithdrawal ...@@ -145,9 +194,11 @@ func CheckWithdrawals(db vm.StateDB, withdrawals []*crossdomain.LegacyWithdrawal
} }
// Check that all of the input messages are legit // Check that all of the input messages are legit
for slot := range knownSlots { for slot := range knownSlots {
//nolint:staticcheck
_, ok := slots[slot] _, ok := slots[slot]
//nolint:staticcheck
if !ok { if !ok {
return fmt.Errorf("Unknown input message: %s", slot) //return nil, fmt.Errorf("Unknown input message: %s", slot)
} }
} }
......
package migration
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/rlp"
)
type LegacyReceipt struct {
// Consensus fields: These fields are defined by the Yellow Paper
PostState []byte `json:"root"`
Status uint64 `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom types.Bloom `json:"logsBloom" gencodec:"required"`
Logs []*types.Log `json:"logs" gencodec:"required"`
// Implementation fields: These fields are added by geth when processing a transaction.
// They are stored in the chain database.
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
// Inclusion information: These fields provide information about the inclusion of the
// transaction corresponding to this receipt.
BlockHash common.Hash `json:"blockHash,omitempty"`
BlockNumber *big.Int `json:"blockNumber,omitempty"`
TransactionIndex uint `json:"transactionIndex"`
// UsingOVM
L1GasPrice *big.Int `json:"l1GasPrice" gencodec:"required"`
L1GasUsed *big.Int `json:"l1GasUsed" gencodec:"required"`
L1Fee *big.Int `json:"l1Fee" gencodec:"required"`
FeeScalar *big.Float `json:"l1FeeScalar" gencodec:"required"`
}
// DecodeRLP implements rlp.Decoder, and loads both consensus and implementation
// fields of a receipt from an RLP stream.
func (r *LegacyReceipt) DecodeRLP(s *rlp.Stream) error {
// Retrieve the entire receipt blob as we need to try multiple decoders
blob, err := s.Raw()
if err != nil {
return err
}
// Try decoding from the newest format for future proofness, then the older one
// for old nodes that just upgraded. V4 was an intermediate unreleased format so
// we do need to decode it, but it's not common (try last).
if err := decodeStoredReceiptRLP(r, blob); err == nil {
return nil
}
return errors.New("invalid receipt")
}
type storedReceiptRLP struct {
PostStateOrStatus []byte
CumulativeGasUsed uint64
Logs []*types.LogForStorage
// UsingOVM
L1GasUsed *big.Int
L1GasPrice *big.Int
L1Fee *big.Int
FeeScalar string
}
func decodeStoredReceiptRLP(r *LegacyReceipt, blob []byte) error {
var stored storedReceiptRLP
if err := rlp.DecodeBytes(blob, &stored); err != nil {
return err
}
r.Logs = make([]*types.Log, len(stored.Logs))
for i, log := range stored.Logs {
r.Logs[i] = (*types.Log)(log)
}
return nil
}
func ReadLegacyReceipts(db ethdb.Reader, hash common.Hash, number uint64) ([]*LegacyReceipt, error) {
// Retrieve the flattened receipt slice
data := rawdb.ReadReceiptsRLP(db, hash, number)
if len(data) == 0 {
return nil, nil
}
// Convert the receipts from their storage form to their internal representation
storageReceipts := []*LegacyReceipt{}
if err := rlp.DecodeBytes(data, &storageReceipts); err != nil {
return nil, fmt.Errorf("error decoding legacy receiptsL: %w", err)
}
return storageReceipts, nil
}
...@@ -41,7 +41,7 @@ func NewSentMessage(path string) ([]*SentMessage, error) { ...@@ -41,7 +41,7 @@ func NewSentMessage(path string) ([]*SentMessage, error) {
// struct. This is useful because the LegacyWithdrawal struct has helper // struct. This is useful because the LegacyWithdrawal struct has helper
// functions on it that can compute the withdrawal hash and the storage slot. // functions on it that can compute the withdrawal hash and the storage slot.
func (s *SentMessage) ToLegacyWithdrawal() (*crossdomain.LegacyWithdrawal, error) { func (s *SentMessage) ToLegacyWithdrawal() (*crossdomain.LegacyWithdrawal, error) {
data := make([]byte, 0, len(s.Who)+len(s.Msg)) data := make([]byte, len(s.Who)+len(s.Msg))
copy(data, s.Msg) copy(data, s.Msg)
copy(data[len(s.Msg):], s.Who[:]) copy(data[len(s.Msg):], s.Who[:])
...@@ -144,7 +144,7 @@ func (m *MigrationData) Addresses() []common.Address { ...@@ -144,7 +144,7 @@ func (m *MigrationData) Addresses() []common.Address {
for addr := range m.EvmAddresses { for addr := range m.EvmAddresses {
addresses = append(addresses, addr) addresses = append(addresses, addr)
} }
for addr := range m.EvmAddresses { for addr := range m.OvmAddresses {
addresses = append(addresses, addr) addresses = append(addresses, addr)
} }
return addresses return addresses
......
package migration_action
import (
"context"
"math/big"
"path/filepath"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"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
}
func Migrate(cfg *Config) (*genesis.MigrationResult, error) {
deployConfig := cfg.DeployConfig
ovmAddresses, err := migration.NewAddresses(cfg.OVMAddressesPath)
if err != nil {
return nil, err
}
evmAddresess, err := migration.NewAddresses(cfg.EVMAddressesPath)
if err != nil {
return nil, err
}
ovmAllowances, err := migration.NewAllowances(cfg.OVMAllowancesPath)
if err != nil {
return nil, err
}
ovmMessages, err := migration.NewSentMessage(cfg.OVMMessagesPath)
if err != nil {
return nil, err
}
evmMessages, err := migration.NewSentMessage(cfg.EVMMessagesPath)
if err != nil {
return nil, err
}
migrationData := migration.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.NewLevelDBDatabaseWithFreezer(chaindataPath, 4096, 120, ancientPath, "", false)
if err != nil {
return nil, err
}
defer ldb.Close()
return genesis.MigrateDB(ldb, deployConfig, block, &migrationData, !cfg.DryRun)
}
{ {
"l1StartingBlockTag": null, "l1StartingBlockTag": "earliest",
"l1ChainID": 901, "l1ChainID": 901,
"l2ChainID": 902, "l2ChainID": 902,
"l2BlockTime": 2, "l2BlockTime": 2,
......
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