Commit f208b022 authored by Andreas Bigger's avatar Andreas Bigger

Merge branch 'develop' into refcell/batcher/tests

parents 404cdd79 e251dac4
---
'@eth-optimism/common-ts': patch
---
Fix BaseServiceV2 configuration for caseCase options
---
'@eth-optimism/contracts-bedrock': patch
---
Print tenderly simulation links during deployment
......@@ -20,13 +20,3 @@ func getOVMETHTotalSupplySlot() common.Hash {
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()
}
......@@ -17,7 +17,7 @@ var (
// OVMETHAddress is the address of the OVM ETH predeploy.
OVMETHAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
ignoredSlots = map[common.Hash]bool{
OVMETHIgnoredSlots = map[common.Hash]bool{
// Total Supply
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002"): true,
// Name
......@@ -29,7 +29,14 @@ var (
}
)
func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int, noCheck bool) error {
// MigrateLegacyETH checks that the given list of addresses and allowances represents all storage
// slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for
// withdrawals because we'll simply carry the balance of a given address to the new system, if the
// account is extra then it won't have any balance and nothing will happen. For each valid balance,
// this method will migrate into state. This method does the checking as part of the migration loop
// in order to avoid having to iterate over state twice. This saves approximately 40 minutes during
// the mainnet migration.
func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool, commit bool) error {
// Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
......@@ -39,38 +46,99 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int
// Log the chain params for debugging purposes.
log.Info("Chain params", "chain-id", chainID, "supply-delta", params.ExpectedSupplyDelta)
// Deduplicate the list of addresses by converting to a map.
deduped := make(map[common.Address]bool)
return doMigration(db, addresses, allowances, params.ExpectedSupplyDelta, noCheck, commit)
}
func doMigration(db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, expSupplyDiff *big.Int, noCheck bool, commit 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)
slotTypes := 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 {
deduped[addr] = true
sk := CalcOVMETHStorageKey(addr)
slotTypes[sk] = 1
slotsAddrs[sk] = addr
}
// For each known allowance, compute its storage key and add it to the list of addresses.
for _, allowance := range allowances {
slotTypes[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2
}
// Migrate the legacy ETH to ETH.
// 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.
sequencerEntrypointAddr := common.HexToAddress("0x4200000000000000000000000000000000000005")
slotTypes[CalcOVMETHStorageKey(sequencerEntrypointAddr)] = 1
// Migrate the OVM_ETH to ETH.
log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses))
totalMigrated := new(big.Int)
logAccountProgress := util.ProgressLogger(1000, "imported accounts")
for addr := range deduped {
// No accounts should have a balance in state. If they do, bail.
if db.GetBalance(addr).Sign() > 0 {
if noCheck {
log.Error("account has non-zero balance in state - should never happen", "addr", addr)
} else {
log.Crit("account has non-zero balance in state - should never happen", "addr", addr)
logAccountProgress := util.ProgressLogger(1000, "imported OVM_ETH storage slot")
var innerErr error
err := db.ForEachStorage(predeploys.LegacyERC20ETHAddr, func(key, value common.Hash) bool {
defer logAccountProgress()
// We can safely ignore specific slots (totalSupply, name, symbol).
if OVMETHIgnoredSlots[key] {
return true
}
// Look up the slot type.
slotType, ok := slotTypes[key]
if !ok {
log.Error("unknown storage slot in state", "slot", key.String())
if !noCheck {
innerErr = fmt.Errorf("unknown storage slot in state: %s", key.String())
return false
}
}
// Pull out the OVM ETH balance.
ovmBalance := getOVMETHBalance(db, addr)
switch slotType {
case 1:
// Balance slot.
bal := value.Big()
totalMigrated.Add(totalMigrated, bal)
addr := slotsAddrs[key]
// Actually perform the migration by setting the appropriate values in state.
db.SetBalance(addr, ovmBalance)
db.SetState(predeploys.LegacyERC20ETHAddr, CalcOVMETHStorageKey(addr), common.Hash{})
// There should never be any balances in state, so verify that here.
if db.GetBalance(addr).Sign() > 0 {
log.Error("account has non-zero balance in state - should never happen", "addr", addr)
if !noCheck {
innerErr = fmt.Errorf("account has non-zero balance in state - should never happen: %s", addr)
return false
}
}
if !commit {
return true
}
// Bump the total OVM balance.
totalMigrated = totalMigrated.Add(totalMigrated, ovmBalance)
// Set the balance, and delete the legacy slot.
db.SetBalance(addr, bal)
db.SetState(predeploys.LegacyERC20ETHAddr, key, common.Hash{})
case 2:
// Allowance slot. Nothing to do here.
return true
default:
// Should never happen.
log.Error("unknown slot type", "slot", key.String(), "type", slotType)
if !noCheck {
innerErr = fmt.Errorf("unknown slot type: %d", slotType)
return false
}
}
// Log progress.
logAccountProgress()
return true
})
if err != nil {
return fmt.Errorf("failed to iterate over OVM_ETH storage: %w", err)
}
if innerErr != nil {
return fmt.Errorf("error in migration: %w", innerErr)
}
// Make sure that the total supply delta matches the expected delta. This is equivalent to
......@@ -78,33 +146,44 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int
// same check against the total found (a = b, b = c => a = c).
totalSupply := getOVMETHTotalSupply(db)
delta := new(big.Int).Sub(totalSupply, totalMigrated)
if delta.Cmp(params.ExpectedSupplyDelta) != 0 {
if delta.Cmp(expSupplyDiff) != 0 {
if noCheck {
log.Error(
"supply mismatch",
"migrated", totalMigrated.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta.String(),
"exp_delta", expSupplyDiff.String(),
)
} else {
log.Crit(
log.Error(
"supply mismatch",
"migrated", totalMigrated.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta.String(),
"exp_delta", expSupplyDiff.String(),
)
return fmt.Errorf("supply mismatch: exp delta %s != %s", expSupplyDiff.String(), delta.String())
}
}
// Supply is verified.
log.Info(
"supply verified OK",
"migrated", totalMigrated.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", expSupplyDiff.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.
db.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
log.Info("Set the totalSupply to 0")
if commit {
db.SetState(predeploys.LegacyERC20ETHAddr, getOVMETHTotalSupplySlot(), common.Hash{})
log.Info("Set the totalSupply to 0")
}
// Fin.
return nil
}
package ether
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"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 TestMigrateLegacyETH(t *testing.T) {
tests := []struct {
name string
totalSupply *big.Int
expDiff *big.Int
stateBalances map[common.Address]*big.Int
stateAllowances map[common.Address]common.Address
inputAddresses []common.Address
inputAllowances []*crossdomain.Allowance
check func(t *testing.T, db *state.StateDB, err error)
}{
{
name: "everything matches",
totalSupply: big.NewInt(3),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
common.HexToAddress("0x456"): big.NewInt(2),
},
stateAllowances: map[common.Address]common.Address{
common.HexToAddress("0x123"): common.HexToAddress("0x456"),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
common.HexToAddress("0x456"),
},
inputAllowances: []*crossdomain.Allowance{
{
From: common.HexToAddress("0x123"),
To: common.HexToAddress("0x456"),
},
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1))
require.Equal(t, db.GetBalance(common.HexToAddress("0x456")), big.NewInt(2))
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x456"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
},
},
{
name: "extra input addresses",
totalSupply: big.NewInt(1),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
common.HexToAddress("0x456"),
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1))
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
},
},
{
name: "extra input allowances",
totalSupply: big.NewInt(1),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
},
stateAllowances: map[common.Address]common.Address{
common.HexToAddress("0x123"): common.HexToAddress("0x456"),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
common.HexToAddress("0x456"),
},
inputAllowances: []*crossdomain.Allowance{
{
From: common.HexToAddress("0x123"),
To: common.HexToAddress("0x456"),
},
{
From: common.HexToAddress("0x123"),
To: common.HexToAddress("0x789"),
},
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1))
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
},
},
{
name: "missing input addresses",
totalSupply: big.NewInt(2),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
common.HexToAddress("0x456"): big.NewInt(1),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.Error(t, err)
require.ErrorContains(t, err, "unknown storage slot")
},
},
{
name: "missing input allowances",
totalSupply: big.NewInt(2),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
},
stateAllowances: map[common.Address]common.Address{
common.HexToAddress("0x123"): common.HexToAddress("0x456"),
common.HexToAddress("0x123"): common.HexToAddress("0x789"),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
},
inputAllowances: []*crossdomain.Allowance{
{
From: common.HexToAddress("0x123"),
To: common.HexToAddress("0x456"),
},
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.Error(t, err)
require.ErrorContains(t, err, "unknown storage slot")
},
},
{
name: "bad supply diff",
totalSupply: big.NewInt(4),
expDiff: big.NewInt(0),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
common.HexToAddress("0x456"): big.NewInt(2),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
common.HexToAddress("0x456"),
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.Error(t, err)
require.ErrorContains(t, err, "supply mismatch")
},
},
{
name: "good supply diff",
totalSupply: big.NewInt(4),
expDiff: big.NewInt(1),
stateBalances: map[common.Address]*big.Int{
common.HexToAddress("0x123"): big.NewInt(1),
common.HexToAddress("0x456"): big.NewInt(2),
},
inputAddresses: []common.Address{
common.HexToAddress("0x123"),
common.HexToAddress("0x456"),
},
check: func(t *testing.T, db *state.StateDB, err error) {
require.NoError(t, err)
require.Equal(t, db.GetBalance(common.HexToAddress("0x123")), big.NewInt(1))
require.Equal(t, db.GetBalance(common.HexToAddress("0x456")), big.NewInt(2))
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x123"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, CalcOVMETHStorageKey(common.HexToAddress("0x456"))), common.Hash{})
require.Equal(t, db.GetState(OVMETHAddress, getOVMETHTotalSupplySlot()), common.Hash{})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := makeLegacyETH(t, tt.totalSupply, tt.stateBalances, tt.stateAllowances)
err := doMigration(db, tt.inputAddresses, tt.inputAllowances, tt.expDiff, false, true)
tt.check(t, db, err)
})
}
}
func makeLegacyETH(t *testing.T, totalSupply *big.Int, balances map[common.Address]*big.Int, allowances map[common.Address]common.Address) *state.StateDB {
db, err := state.New(common.Hash{}, state.NewDatabaseWithConfig(rawdb.NewMemoryDatabase(), &trie.Config{
Preimages: true,
Cache: 1024,
}), nil)
require.NoError(t, err)
db.CreateAccount(OVMETHAddress)
db.SetState(OVMETHAddress, getOVMETHTotalSupplySlot(), common.BigToHash(totalSupply))
for slot := range OVMETHIgnoredSlots {
if slot == getOVMETHTotalSupplySlot() {
continue
}
db.SetState(OVMETHAddress, slot, common.Hash{31: 0xff})
}
for addr, balance := range balances {
db.SetState(OVMETHAddress, CalcOVMETHStorageKey(addr), common.BigToHash(balance))
}
for from, to := range allowances {
db.SetState(OVMETHAddress, CalcAllowanceStorageKey(from, to), common.BigToHash(big.NewInt(1)))
}
root, err := db.Commit(false)
require.NoError(t, err)
err = db.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
return db
}
package ether
import (
"errors"
"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/ethdb"
"github.com/ethereum/go-ethereum/log"
)
// PreCheckBalances checks that the given list of addresses and allowances represents all storage
// slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for
// withdrawals because we'll simply carry the balance of a given address to the new system, if the
// account is extra then it won't have any balance and nothing will happen.
func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) ([]common.Address, error) {
// Chain params to use for integrity checking.
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
return nil, fmt.Errorf("no chain params for %d", chainID)
}
// 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.
addrs := make([]common.Address, 0)
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 {
addrs = append(addrs, addr)
slotsInp[CalcOVMETHStorageKey(addr)] = 1
}
// For each known allowance, compute its storage key and add it to the list of addresses.
for _, allowance := range allowances {
addrs = append(addrs, allowance.From)
slotsInp[CalcAllowanceStorageKey(allowance.From, allowance.To)] = 2
}
// 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.
sequencerEntrypointAddr := common.HexToAddress("0x4200000000000000000000000000000000000005")
addrs = append(addrs, sequencerEntrypointAddr)
slotsInp[CalcOVMETHStorageKey(sequencerEntrypointAddr)] = 1
// Build a mapping of every storage slot in the LegacyERC20ETH contract, except the list of
// slots that we know we can ignore (totalSupply, name, symbol).
var count int
slotsAct := make(map[common.Hash]common.Hash)
progress := util.ProgressLogger(1000, "Read OVM_ETH storage slot")
err := db.ForEachStorage(predeploys.LegacyERC20ETHAddr, func(key, value common.Hash) bool {
progress()
// We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] {
return true
}
// Slot exists, so add it to the map.
slotsAct[key] = value
count++
return true
})
if err != nil {
return nil, fmt.Errorf("cannot iterate over LegacyERC20ETHAddr: %w", err)
}
// Log how many slots were iterated over.
log.Info("Iterated legacy balances", "count", count)
// Iterate over the list of known slots and check that we have a slot for each one. We'll also
// keep track of the total balance to be migrated and throw if the total supply exceeds the
// expected supply delta.
totalFound := new(big.Int)
var unknown bool
for slot := range slotsAct {
slotType, ok := slotsInp[slot]
if !ok {
if noCheck {
log.Error("ignoring unknown storage slot in state", "slot", slot.String())
} else {
unknown = true
log.Error("unknown storage slot in state", "slot", slot.String())
continue
}
}
// Add balances to the total found.
switch slotType {
case 1:
// Balance slot.
totalFound.Add(totalFound, slotsAct[slot].Big())
case 2:
// Allowance slot.
continue
default:
// Should never happen.
if noCheck {
log.Error("unknown slot type", "slot", slot, "type", slotType)
} else {
log.Crit("unknown slot type: %d", slotType)
}
}
}
if unknown {
return nil, errors.New("unknown storage slots in state (see logs for details)")
}
// 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.
totalSupply := getOVMETHTotalSupply(db)
delta := new(big.Int).Sub(totalSupply, totalFound)
if delta.Cmp(params.ExpectedSupplyDelta) != 0 {
if noCheck {
log.Error(
"supply mismatch",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta.String(),
)
} else {
log.Crit(
"supply mismatch",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta.String(),
)
}
}
// Supply is verified.
log.Info(
"supply verified OK",
"migrated", totalFound.String(),
"supply", totalSupply.String(),
"delta", delta.String(),
"exp_delta", params.ExpectedSupplyDelta.String(),
)
// We know we have at least a superset of all addresses here since we know that we have every
// storage slot. It's fine to have extras because they won't have any balance.
return addrs, nil
}
......@@ -31,7 +31,6 @@ const MaxSlotChecks = 1000
type StorageCheckMap = map[common.Hash]common.Hash
var (
L2XDMOwnerSlot = common.Hash{31: 0x33}
ProxyAdminOwnerSlot = common.Hash{}
LegacyETHCheckSlots = map[common.Hash]common.Hash{
......@@ -53,10 +52,6 @@ var (
predeploys.L2CrossDomainMessengerAddr: {
// Slot 0x00 (0) is a combination of spacer_0_0_20, _initialized, and _initializing
common.Hash{}: common.HexToHash("0x0000000000000000000000010000000000000000000000000000000000000000"),
// Slot 0x33 (51) is _owner. Requires custom check, so set to a garbage value
L2XDMOwnerSlot: common.HexToHash("0xbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad0"),
// Slot 0x97 (151) is _status
common.Hash{31: 0x97}: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
// Slot 0xcc (204) is xDomainMsgSender
common.Hash{31: 0xcc}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000dead"),
// EIP-1967 storage slots
......@@ -346,7 +341,7 @@ func PostCheckPredeployStorage(db vm.StateDB, finalSystemOwner common.Address, p
for key, value := range expSlots {
// The owner slots for the L2XDM and ProxyAdmin are special cases.
// They are set to the final system owner in the config.
if (*addr == predeploys.L2CrossDomainMessengerAddr && key == L2XDMOwnerSlot) || (*addr == predeploys.ProxyAdminAddr && key == ProxyAdminOwnerSlot) {
if *addr == predeploys.ProxyAdminAddr && key == ProxyAdminOwnerSlot {
actualOwner := common.BytesToAddress(slots[key].Bytes())
if actualOwner != proxyAdminOwner {
return fmt.Errorf("expected owner for %s to be %s but got %s", name, proxyAdminOwner, actualOwner)
......
......@@ -134,16 +134,6 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
filteredWithdrawals = crossdomain.SafeFilteredWithdrawals(unfilteredWithdrawals)
}
// We also need to verify that we have all of the storage slots for the LegacyERC20ETH contract
// that we expect to have. An error will be thrown if there are any missing storage slots.
// Unlike with withdrawals, we do not need to filter out extra addresses because their balances
// would necessarily be zero and therefore not affect the migration.
log.Info("Checking addresses...", "no-check", noCheck)
addrs, err := ether.PreCheckBalances(ldb, db, migrationData.Addresses(), migrationData.OvmAllowances, int(config.L1ChainID), noCheck)
if err != nil {
return nil, fmt.Errorf("addresses mismatch: %w", err)
}
// At this point we've fully verified the witness data for the migration, so we can begin the
// actual migration process. This involves modifying parts of the legacy database and inserting
// a transition block.
......@@ -192,10 +182,15 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
return nil, fmt.Errorf("cannot migrate withdrawals: %w", err)
}
// Finally we migrate the balances held inside the LegacyERC20ETH contract into the state trie.
// We also need to verify that we have all of the storage slots for the LegacyERC20ETH contract
// that we expect to have. An error will be thrown if there are any missing storage slots.
// Unlike with withdrawals, we do not need to filter out extra addresses because their balances
// would necessarily be zero and therefore not affect the migration.
//
// Once verified, we migrate the balances held inside the LegacyERC20ETH contract into the state trie.
// We also delete the balances from the LegacyERC20ETH contract.
log.Info("Starting to migrate ERC20 ETH")
err = ether.MigrateLegacyETH(db, addrs, int(config.L1ChainID), noCheck)
err = ether.MigrateLegacyETH(db, migrationData.Addresses(), migrationData.OvmAllowances, int(config.L1ChainID), noCheck, commit)
if err != nil {
return nil, fmt.Errorf("cannot migrate legacy eth: %w", err)
}
......
......@@ -649,9 +649,8 @@ func TestSystemMockP2P(t *testing.T) {
require.Contains(t, received, receiptVerif.BlockHash)
}
// TestSystemMockPeerScoring sets up a L1 Geth node, a rollup node, and a L2 geth node and then confirms that
// the nodes can sync L2 blocks before they are confirmed on L1.
func TestSystemMockPeerScoring(t *testing.T) {
// TestSystemDenseTopology sets up a dense p2p topology with 3 verifier nodes and 1 sequencer node.
func TestSystemDenseTopology(t *testing.T) {
parallel(t)
if !verboseGethNodes {
log.Root().SetHandler(log.DiscardHandler())
......@@ -682,31 +681,11 @@ func TestSystemMockPeerScoring(t *testing.T) {
cfg.Loggers["verifier2"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier")
cfg.Loggers["verifier3"] = testlog.Logger(t, log.LvlInfo).New("role", "verifier")
// Construct a new sequencer with an invalid privkey to produce invalid gossip
// We can then test that the peer scoring system will ban the node
sequencer2PrivateKey := cfg.Secrets.Mallory
cfg.Nodes["sequencer2"] = &rollupNode.Config{
Driver: driver.Config{
VerifierConfDepth: 0,
SequencerConfDepth: 0,
SequencerEnabled: true,
},
// Submitter PrivKey is set in system start for rollup nodes where sequencer = true
RPC: rollupNode.RPCConfig{
ListenAddr: "127.0.0.1",
ListenPort: 0,
EnableAdmin: true,
},
L1EpochPollInterval: time.Second * 4,
P2PSigner: &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(sequencer2PrivateKey)},
}
cfg.Loggers["sequencer2"] = testlog.Logger(t, log.LvlInfo).New("role", "sequencer")
// connect the nodes
cfg.P2PTopology = map[string][]string{
"verifier": {"sequencer", "sequencer2", "verifier2", "verifier3"},
"verifier2": {"sequencer", "sequencer2", "verifier", "verifier3"},
"verifier3": {"sequencer", "sequencer2", "verifier", "verifier2"},
"verifier": {"sequencer", "verifier2", "verifier3"},
"verifier2": {"sequencer", "verifier", "verifier3"},
"verifier3": {"sequencer", "verifier", "verifier2"},
}
// Set peer scoring for each node, but without banning
......@@ -719,15 +698,11 @@ func TestSystemMockPeerScoring(t *testing.T) {
}
}
var published, published2, received1, received2, received3 []common.Hash
var published, received1, received2, received3 []common.Hash
seqTracer, verifTracer, verifTracer2, verifTracer3 := new(FnTracer), new(FnTracer), new(FnTracer), new(FnTracer)
seq2Tracer := new(FnTracer)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash)
}
seq2Tracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published2 = append(published2, payload.BlockHash)
}
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received1 = append(received1, payload.BlockHash)
}
......@@ -738,7 +713,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
received3 = append(received3, payload.BlockHash)
}
cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["sequencer2"].Tracer = seq2Tracer
cfg.Nodes["verifier"].Tracer = verifTracer
cfg.Nodes["verifier2"].Tracer = verifTracer2
cfg.Nodes["verifier3"].Tracer = verifTracer3
......@@ -748,7 +722,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
defer sys.Close()
l2Seq := sys.Clients["sequencer"]
// l2Seq2 := sys.Clients["sequencer2"]
l2Verif := sys.Clients["verifier"]
l2Verif2 := sys.Clients["verifier2"]
l2Verif3 := sys.Clients["verifier3"]
......@@ -768,23 +741,23 @@ func TestSystemMockPeerScoring(t *testing.T) {
Gas: 21000,
})
err = l2Seq.SendTransaction(context.Background(), tx)
require.Nil(t, err, "Sending L2 tx to sequencer")
require.NoError(t, err, "Sending L2 tx to sequencer")
// Wait for tx to be mined on the L2 sequencer chain
receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on sequencer")
receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.NoError(t, err, "Waiting for L2 tx on sequencer")
// Wait until the block it was first included in shows up in the safe chain on the verifier
receiptVerif, err := waitForTransaction(tx.Hash(), l2Verif, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier")
receiptVerif, err := waitForTransaction(tx.Hash(), l2Verif, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.NoError(t, err, "Waiting for L2 tx on verifier")
require.Equal(t, receiptSeq, receiptVerif)
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier2")
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.NoError(t, err, "Waiting for L2 tx on verifier2")
require.Equal(t, receiptSeq, receiptVerif)
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier3")
receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 10*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.NoError(t, err, "Waiting for L2 tx on verifier3")
require.Equal(t, receiptSeq, receiptVerif)
// Verify that everything that was received was published
......@@ -799,37 +772,6 @@ func TestSystemMockPeerScoring(t *testing.T) {
require.Contains(t, received1, receiptVerif.BlockHash)
require.Contains(t, received2, receiptVerif.BlockHash)
require.Contains(t, received3, receiptVerif.BlockHash)
// Submit TX to the second (malicious) sequencer node
// toAddr = common.Address{0xff, 0xff}
// maliciousTx := types.MustSignNewTx(ethPrivKey, types.LatestSignerForChainID(cfg.L2ChainIDBig()), &types.DynamicFeeTx{
// ChainID: cfg.L2ChainIDBig(),
// Nonce: 1,
// To: &toAddr,
// Value: big.NewInt(1_000_000_000),
// GasTipCap: big.NewInt(10),
// GasFeeCap: big.NewInt(200),
// Gas: 21000,
// })
// err = l2Seq2.SendTransaction(context.Background(), maliciousTx)
// require.Nil(t, err, "Sending L2 tx to sequencer")
// Wait for tx to be mined on the L2 sequencer chain
// receiptSeq, err = waitForTransaction(maliciousTx.Hash(), l2Seq2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on sequencer")
// Wait until the block it was first included in shows up in the safe chain on the verifier
// receiptVerif, err = waitForTransaction(maliciousTx.Hash(), l2Verif, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier")
// require.Equal(t, receiptSeq, receiptVerif)
// receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif2, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier2")
// require.Equal(t, receiptSeq, receiptVerif)
// receiptVerif, err = waitForTransaction(tx.Hash(), l2Verif3, 6*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
// require.Nil(t, err, "Waiting for L2 tx on verifier3")
// require.Equal(t, receiptSeq, receiptVerif)
}
func TestL1InfoContract(t *testing.T) {
......
......@@ -485,6 +485,7 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error {
_, errType, err = eq.ConfirmPayload(ctx)
}
if err != nil {
_ = eq.CancelPayload(ctx, true)
switch errType {
case BlockInsertTemporaryErr:
// RPC errors are recoverable, we can retry the buffered payload attributes later.
......
......@@ -211,8 +211,15 @@ export abstract class BaseServiceV2<
// Since BCFG turns everything into lower case, we're required to turn all of the input option
// names into lower case for the validation step. We'll turn the names back into their original
// names when we're done.
const lowerCaseOptions = Object.entries(params.options).reduce(
(acc, [key, val]) => {
acc[key.toLowerCase()] = val
return acc
},
{}
)
const cleaned = cleanEnv<TOptions>(
{ ...config.env, ...config.args, ...(params.options || {}) },
{ ...config.env, ...config.args, ...(lowerCaseOptions || {}) },
Object.entries(params.optionsSpec || {}).reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val.validator({
desc: val.desc,
......
import { validators } from '../dist'
import { BaseServiceV2 } from '../src'
type ServiceOptions = {
camelCase: string
}
class Service extends BaseServiceV2<ServiceOptions, {}, {}> {
constructor(options?: Partial<ServiceOptions>) {
super({
name: 'test-service',
version: '0.0',
options,
optionsSpec: {
camelCase: { validator: validators.str, desc: 'test' },
},
metricsSpec: {},
})
}
protected async main() {
/* eslint-disable @typescript-eslint/no-empty-function */
}
}
describe('BaseServiceV2', () => {
it('base service ctor does not throw on camel case options', async () => {
new Service({ camelCase: 'test' })
})
})
......@@ -3,3 +3,7 @@ L1_RPC=
# Private key for the deployer account
PRIVATE_KEY_DEPLOYER=
# Optional Tenderly details for a simulation link during deployment
TENDERLY_PROJECT=
TENDERLY_USERNAME=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
// Importing from the legacy contracts package causes issues with the build of the contract bindings
// so we just copy the library here from
// /packages/contracts/contracts/libraries/bridge/Lib_CrossDomainUtils.sol at commit
// 7866168c
/**
* @title LegacyCrossDomainUtils
*/
library LegacyCrossDomainUtils {
/**
* Generates the correct cross domain calldata for a message.
* @param _target Target contract address.
* @param _sender Message sender address.
* @param _message Message to send to the target.
* @param _messageNonce Nonce for the provided message.
* @return ABI encoded cross domain calldata.
*/
function encodeXDomainCalldata(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) internal pure returns (bytes memory) {
return
abi.encodeWithSignature(
"relayMessage(address,address,bytes,uint256)",
_target,
_sender,
_message,
_messageNonce
);
}
}
......@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { CommonTest } from "./CommonTest.t.sol";
import { Types } from "../libraries/Types.sol";
import { Encoding } from "../libraries/Encoding.sol";
import { LegacyCrossDomainUtils } from "../libraries/LegacyCrossDomainUtils.sol";
contract Encoding_Test is CommonTest {
function testFuzz_nonceVersioning_succeeds(uint240 _nonce, uint16 _version) external {
......@@ -56,6 +57,32 @@ contract Encoding_Test is CommonTest {
assertEq(encoding, _encoding);
}
function testFuzz_encodeCrossDomainMessageV0_matchesLegacy_succeeds(
uint240 _nonce,
address _sender,
address _target,
bytes memory _data
) external {
uint8 version = 0;
uint256 nonce = Encoding.encodeVersionedNonce(_nonce, version);
bytes memory legacyEncoding = LegacyCrossDomainUtils.encodeXDomainCalldata(
_target,
_sender,
_data,
nonce
);
bytes memory bedrockEncoding = Encoding.encodeCrossDomainMessageV0(
_target,
_sender,
_data,
nonce
);
assertEq(legacyEncoding, bedrockEncoding);
}
function testDiff_encodeDepositTransaction_succeeds(
address _from,
address _to,
......
......@@ -5,6 +5,7 @@ import { CommonTest } from "./CommonTest.t.sol";
import { Types } from "../libraries/Types.sol";
import { Hashing } from "../libraries/Hashing.sol";
import { Encoding } from "../libraries/Encoding.sol";
import { LegacyCrossDomainUtils } from "../libraries/LegacyCrossDomainUtils.sol";
contract Hashing_hashDepositSource_Test is CommonTest {
/**
......@@ -43,6 +44,28 @@ contract Hashing_hashCrossDomainMessage_Test is CommonTest {
ffi.hashCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data)
);
}
/**
* @notice Tests that hashCrossDomainMessageV0 matches the hash of the legacy encoding.
*/
function testFuzz_hashCrossDomainMessageV0_matchesLegacy_succeeds(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) external {
assertEq(
keccak256(
LegacyCrossDomainUtils.encodeXDomainCalldata(
_target,
_sender,
_message,
_messageNonce
)
),
Hashing.hashCrossDomainMessageV0(_target, _sender, _message, _messageNonce)
);
}
}
contract Hashing_hashWithdrawal_Test is CommonTest {
......
......@@ -41,6 +41,7 @@
"l2GenesisBlockGasLimit": "0x17D7840",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
"l2GenesisRegolithTimeOffset": "0x0",
"eip1559Denominator": 50,
"eip1559Elasticity": 10
......
......@@ -13,6 +13,7 @@ import {
getDeploymentAddress,
doStep,
jsonifyTransaction,
getTenderlySimulationLink,
} from '../src/deploy-utils'
const deployFn: DeployFunction = async (hre) => {
......@@ -97,6 +98,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
......@@ -133,6 +135,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
......@@ -169,6 +172,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
......
......@@ -13,6 +13,7 @@ import {
jsonifyTransaction,
isStep,
doStep,
getTenderlySimulationLink,
} from '../src/deploy-utils'
const deployFn: DeployFunction = async (hre) => {
......@@ -193,6 +194,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
await awaitCondition(
......@@ -303,6 +305,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`OptimismPortal address: ${OptimismPortal.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
await awaitCondition(
......@@ -331,6 +334,7 @@ const deployFn: DeployFunction = async (hre) => {
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
await awaitCondition(
......
import assert from 'assert'
import { URLSearchParams } from 'url'
import { ethers, Contract } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
......@@ -180,7 +181,6 @@ export const getContractFromArtifact = async (
} = {}
): Promise<ethers.Contract> => {
const artifact = await hre.deployments.get(name)
await hre.ethers.provider.waitForTransaction(artifact.receipt.transactionHash)
// Get the deployed contract's interface.
let iface = new hre.ethers.utils.Interface(artifact.abi)
......@@ -293,6 +293,7 @@ export const getDeploymentAddress = async (
export const jsonifyTransaction = (tx: ethers.PopulatedTransaction): string => {
return JSON.stringify(
{
from: tx.from,
to: tx.to,
data: tx.data,
value: tx.value,
......@@ -354,6 +355,9 @@ export const doStep = async (opts: {
console.log(`MSD address: ${opts.SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(
await getTenderlySimulationLink(opts.SystemDictator.provider, tx)
)
}
// Wait for the step to complete.
......@@ -368,3 +372,26 @@ export const doStep = async (opts: {
// Perform post-step checks.
await opts.checks()
}
/**
* Returns a direct link to a Tenderly simulation.
*
* @param provider Ethers Provider.
* @param tx Ethers transaction object.
* @returns the url of the tenderly simulation.
*/
export const getTenderlySimulationLink = async (
provider: ethers.providers.Provider,
tx: ethers.PopulatedTransaction
): Promise<string> => {
if (process.env.TENDERLY_PROJECT && process.env.TENDERLY_USERNAME) {
return `https://dashboard.tenderly.co/${process.env.TENDERLY_PROJECT}/${
process.env.TENDERLY_USERNAME
}/simulator/new?${new URLSearchParams({
network: (await provider.getNetwork()).chainId.toString(),
contractAddress: tx.to,
rawFunctionInput: tx.data,
from: tx.from,
}).toString()}`
}
}
......@@ -49,8 +49,9 @@ Usage: service [options]
Options:
--l1rpcprovider Provider for interacting with L1 (env: FAULT_DETECTOR__L1_RPC_PROVIDER)
--l2rpcprovider Provider for interacting with L2 (env: FAULT_DETECTOR__L2_RPC_PROVIDER)
--startbatchindex Batch index to start checking from (env: FAULT_DETECTOR__START_BATCH_INDEX)
--startbatchindex Batch index to start checking from. Setting it to -1 will cause the fault detector to find the first state batch index that has not yet passed the fault proof window (env: FAULT_DETECTOR__START_BATCH_INDEX, default value: -1)
--loopintervalms Loop interval in milliseconds (env: FAULT_DETECTOR__LOOP_INTERVAL_MS)
--bedrock Whether or not the service is running against a Bedrock chain (env: FAULT_DETECTOR__BEDROCK, default value: false)
--port Port for the app server (env: FAULT_DETECTOR__PORT)
--hostname Hostname for the app server (env: FAULT_DETECTOR__HOSTNAME)
-h, --help display help for command
......
......@@ -323,7 +323,7 @@ transaction are determined by the corresponding `TransactionDeposited` event emi
1. `from` is unchanged from the emitted value (though it may have been transformed to an alias in
the deposit feed contract).
2. `to` is any 20-byte address (including the zero address)
- In case of a contract creation (cf. `isCreation`), this address is always zero.
- In case of a contract creation (cf. `isCreation`), this address is set to `null`.
3. `mint` is set to the emitted value.
4. `value` is set to the emitted value.
5. `gaslimit` is unchanged from the emitted value. It must be at least 21000.
......
......@@ -928,10 +928,11 @@ follows:
- `timestamp` is set to the batch's timestamp.
- `random` is set to the `prev_randao` L1 block attribute.
- `suggestedFeeRecipient` is set to an address determined by the sequencer.
- `suggestedFeeRecipient` is set to the Sequencer Fee Vault address. See [Fee Vaults] specification.
- `transactions` is the array of the derived transactions: deposited transactions and sequenced transactions, all
encoded with [EIP-2718].
- `noTxPool` is set to `true`, to use the exact above `transactions` list when constructing the block.
- `gasLimit` is set to the current `gasLimit` value in the [system configuration][g-system-config] of this payload.
[extended-attributes]: exec-engine.md#extended-payloadattributesv1
[Fee Vaults]: exec-engine.md#fee-vaults
......@@ -6,6 +6,11 @@
- [Deposited transaction processing](#deposited-transaction-processing)
- [Deposited transaction boundaries](#deposited-transaction-boundaries)
- [Fees](#fees)
- [Fee Vaults](#fee-vaults)
- [Priority fees (Sequencer Fee Vault)](#priority-fees-sequencer-fee-vault)
- [Base fees (Base Fee Vault)](#base-fees-base-fee-vault)
- [L1-Cost fees (L1 Fee Vault)](#l1-cost-fees-l1-fee-vault)
- [Engine API](#engine-api)
- [`engine_forkchoiceUpdatedV1`](#engine_forkchoiceupdatedv1)
- [Extended PayloadAttributesV1](#extended-payloadattributesv1)
......@@ -46,6 +51,74 @@ To process deposited transactions safely, the deposits MUST be authenticated fir
Deposited transactions MUST never be consumed from the transaction pool.
*The transaction pool can be disabled in a deposits-only rollup*
## Fees
Sequenced transactions (i.e. not applicable to deposits) are charged with 3 types of fees:
priority fees, base fees, and L1-cost fees.
### Fee Vaults
The three types of fees are collected in 3 distinct L2 fee-vault deployments for accounting purposes:
fee payments are not registered as internal EVM calls, and thus distinguished better this way.
These are hardcoded addresses, pointing at pre-deployed proxy contracts.
The proxies are backed by vault contract deployments, based on `FeeVault`, to route vault funds to L1 securely.
| Vault Name | Predeploy |
|---------------------|----------------------------------------------------------|
| Sequencer Fee Vault | [`SequencerFeeVault`](./predeploys.md#SequencerFeeVault) |
| Base Fee Vault | [`BaseFeeVault`](./predeploys.md#BaseFeeVault) |
| L1 Fee Vault | [`L1FeeVault`](./predeploys.md#L1FeeVault) |
### Priority fees (Sequencer Fee Vault)
Priority fees follow the [eip-1559] specification, and are collected by the fee-recipient of the L2 block.
The block fee-recipient (a.k.a. coinbase address) is set to the Sequencer Fee Vault address.
### Base fees (Base Fee Vault)
Base fees largely follow the [eip-1559] specification, with the exception that base fees are not burned,
but add up to the Base Fee Vault ETH account balance.
### L1-Cost fees (L1 Fee Vault)
The protocol funds batch-submission of sequenced L2 transactions by charging L2 users an additional fee
based on the estimated batch-submission costs.
This fee is charged from the L2 transaction-sender ETH balance, and collected into the L1 Fee Vault.
The exact L1 cost function to determine the L1-cost fee component of a L2 transaction is calculated as:
`(rollupDataGas + l1FeeOverhead) * l1Basefee * l1FeeScalar / 1000000`
(big-int computation, result in Wei and `uint256` range)
Where:
- `rollupDataGas` is determined from the *full* encoded transaction
(standard EIP-2718 transaction encoding, including signature fields):
- Before Regolith fork: `rollupDataGas = zeroes * 4 + (ones + 68) * 16`
- The addition of `68` non-zero bytes is a remnant of a pre-Bedrock L1-cost accounting function,
which accounted for the worst-case non-zero bytes addition to complement unsigned transactions, unlike Bedrock.
- With Regolith fork: `rollupDataGas = zeroes * 4 + ones * 16`
- `l1FeeOverhead` is the Gas Price Oracle `overhead` value.
- `l1FeeScalar` is the Gas Price Oracle `scalar` value.
- `l1Basefee` is the L1 Base fee of the latest L1 origin registered in the L2 chain.
Note that the `rollupDataGas` uses the same byte cost accounting as defined in [eip-2028],
except the full L2 transaction now counts towards the bytes charged in the L1 calldata.
This behavior matches pre-Bedrock L1-cost estimation of L2 transactions.
Compression, batching, and intrinsic gas costs of the batch transactions are accounted for by the protocol
with the Gas Price Oracle `overhead` and `scalar` parameters.
The Gas Price Oracle `l1FeeOverhead` and `l1FeeScalar`, as well as the `l1Basefee` of the L1 origin,
can be accessed in two interchangeable ways:
- read from the deposited L1 attributes (`l1FeeOverhead`, `l1FeeScalar`, `basefee`) of the current L2 block
- read from the L1 Block Info contract (`0x4200000000000000000000000000000000000015`)
- using the respective solidity `uint256`-getter functions (`l1FeeOverhead`, `l1FeeScalar`, `basefee`)
- using direct storage-reads:
- L1 basefee as big-endian `uint256` in slot `1`
- Overhead as big-endian `uint256` in slot `5`
- Scalar as big-endian `uint256` in slot `6`
## Engine API
<!--
......@@ -190,6 +263,8 @@ the operation within the engine is the exact same as with L1 (although with an E
[rollup node spec]: rollup-node.md
[eip-1559]: https://eips.ethereum.org/EIPS/eip-1559
[eip-2028]: https://eips.ethereum.org/EIPS/eip-2028
[eip-2718]: https://eips.ethereum.org/EIPS/eip-2718
[eip-2718-transactions]: https://eips.ethereum.org/EIPS/eip-2718#transactions
[exec-api-data]: https://github.com/ethereum/execution-apis/blob/769c53c94c4e487337ad0edea9ee0dce49c79bfa/src/engine/specification.md#structures
......
......@@ -80,8 +80,10 @@ Summary of changes:
including the `contractAddress` field of deposits that deploy contracts.
- The `gas` and `depositNonce` data is committed to as part of the consensus-representation of the receipt,
enabling the data to be safely synced between independent L2 nodes.
- The L1-cost function was corrected to more closely match pre-Bedrock behavior.
The [deposit specification](./deposits.md) specifies the changes of the Regolith upgrade in more detail.
The [deposit specification](./deposits.md) specifies the deposit changes of the Regolith upgrade in more detail.
The [execution engine specification](./exec-engine.md) specifies the L1 cost function difference.
The Regolith upgrade uses a *L2 block-timestamp* activation-rule, and is specified in both the
rollup-node (`regolith_time`) and execution engine (`config.regolithTime`).
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