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

Merge branch 'develop' into jg/extended_metrics

parents fd399a9f cb8fbc4c
......@@ -6,7 +6,7 @@ require (
github.com/btcsuite/btcd v0.23.3
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0
github.com/docker/docker v20.10.21+incompatible
github.com/docker/docker v20.10.24+incompatible
github.com/docker/go-connections v0.4.0
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3
github.com/ethereum/go-ethereum v1.11.5
......
......@@ -158,8 +158,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=
github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
......
......@@ -51,9 +51,10 @@ func Main(version string, cliCtx *cli.Context) error {
return err
}
}
defer batchSubmitter.StopIfRunning(context.Background())
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Stop pprof and metrics only after main loop returns
defer batchSubmitter.StopIfRunning(context.Background())
pprofConfig := cfg.PprofConfig
if pprofConfig.Enabled {
......@@ -106,7 +107,8 @@ func Main(version string, cliCtx *cli.Context) error {
syscall.SIGQUIT,
}...)
<-interruptChannel
cancel()
_ = server.Stop()
if err := server.Stop(); err != nil {
l.Error("Error shutting down http server: %w", err)
}
return nil
}
......@@ -447,7 +447,7 @@ func TestBigL2Txs(gt *testing.T) {
require.NoError(t, err)
gas, err := core.IntrinsicGas(data, nil, false, true, true, false)
require.NoError(t, err)
if gas > engine.l2GasPool.Gas() {
if gas > engine.engineApi.RemainingBlockGas() {
break
}
tx := types.MustSignNewTx(dp.Secrets.Alice, signer, &types.DynamicFeeTx{
......
......@@ -3,12 +3,12 @@ package actions
import (
"errors"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
geth "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
......@@ -38,22 +38,10 @@ type L2Engine struct {
rollupGenesis *rollup.Genesis
// L2 evm / chain
l2Chain *core.BlockChain
l2Database ethdb.Database
l2Cfg *core.Genesis
l2Signer types.Signer
// L2 block building data
l2BuildingHeader *types.Header // block header that we add txs to for block building
l2BuildingState *state.StateDB // state used for block building
l2GasPool *core.GasPool // track gas used of ongoing building
pendingIndices map[common.Address]uint64 // per account, how many txs from the pool were already included in the block, since the pool is lagging behind block mining.
l2Transactions []*types.Transaction // collects txs that were successfully included into current block build
l2Receipts []*types.Receipt // collect receipts of ongoing building
l2ForceEmpty bool // when no additional txs may be processed (i.e. when sequencer drift runs out)
l2TxFailed []*types.Transaction // log of failed transactions which could not be included
payloadID engine.PayloadID // ID of payload that is currently being built
l2Chain *core.BlockChain
l2Signer types.Signer
engineApi *engineapi.L2EngineAPI
failL2RPC error // mock error
}
......@@ -61,6 +49,38 @@ type L2Engine struct {
type EngineOption func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error
func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesisL1 eth.BlockID, jwtPath string, options ...EngineOption) *L2Engine {
n, ethBackend, apiBackend := newBackend(t, genesis, jwtPath, options)
engineApi := engineapi.NewL2EngineAPI(log, apiBackend)
chain := ethBackend.BlockChain()
genesisBlock := chain.Genesis()
eng := &L2Engine{
log: log,
node: n,
eth: ethBackend,
rollupGenesis: &rollup.Genesis{
L1: rollupGenesisL1,
L2: eth.BlockID{Hash: genesisBlock.Hash(), Number: genesisBlock.NumberU64()},
L2Time: genesis.Timestamp,
},
l2Chain: chain,
l2Signer: types.LatestSigner(genesis.Config),
engineApi: engineApi,
}
// register the custom engine API, so we can serve engine requests while having more control
// over sequencing of individual txs.
n.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Service: eng.engineApi,
Authenticated: true,
},
})
require.NoError(t, n.Start(), "failed to start L2 op-geth node")
return eng
}
func newBackend(t e2eutils.TestingBase, genesis *core.Genesis, jwtPath string, options []EngineOption) (*node.Node, *geth.Ethereum, *engineApiBackend) {
ethCfg := &ethconfig.Config{
NetworkId: genesis.Config.ChainID.Uint64(),
Genesis: genesis,
......@@ -89,33 +109,26 @@ func NewL2Engine(t Testing, log log.Logger, genesis *core.Genesis, rollupGenesis
chain := backend.BlockChain()
db := backend.ChainDb()
genesisBlock := chain.Genesis()
eng := &L2Engine{
log: log,
node: n,
eth: backend,
rollupGenesis: &rollup.Genesis{
L1: rollupGenesisL1,
L2: eth.BlockID{Hash: genesisBlock.Hash(), Number: genesisBlock.NumberU64()},
L2Time: genesis.Timestamp,
},
l2Chain: chain,
l2Database: db,
l2Cfg: genesis,
l2Signer: types.LatestSigner(genesis.Config),
apiBackend := &engineApiBackend{
BlockChain: chain,
db: db,
genesis: genesis,
}
// register the custom engine API, so we can serve engine requests while having more control
// over sequencing of individual txs.
n.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Service: (*L2EngineAPI)(eng),
Authenticated: true,
},
})
require.NoError(t, n.Start(), "failed to start L2 op-geth node")
return n, backend, apiBackend
}
return eng
type engineApiBackend struct {
*core.BlockChain
db ethdb.Database
genesis *core.Genesis
}
func (e *engineApiBackend) Database() ethdb.Database {
return e.db
}
func (e *engineApiBackend) Genesis() *core.Genesis {
return e.genesis
}
func (s *L2Engine) EthClient() *ethclient.Client {
......@@ -158,39 +171,25 @@ func (e *L2Engine) ActL2RPCFail(t Testing) {
// ActL2IncludeTx includes the next transaction from the given address in the block that is being built
func (e *L2Engine) ActL2IncludeTx(from common.Address) Action {
return func(t Testing) {
if e.l2BuildingHeader == nil {
t.InvalidAction("not currently building a block, cannot include tx from queue")
return
}
if e.l2ForceEmpty {
if e.engineApi.ForcedEmpty() {
e.log.Info("Skipping including a transaction because e.L2ForceEmpty is true")
// t.InvalidAction("cannot include any sequencer txs")
return
}
i := e.pendingIndices[from]
i := e.engineApi.PendingIndices(from)
txs, q := e.eth.TxPool().ContentFrom(from)
if uint64(len(txs)) <= i {
t.Fatalf("no pending txs from %s, and have %d unprocessable queued txs from this account", from, len(q))
}
tx := txs[i]
if tx.Gas() > e.l2BuildingHeader.GasLimit {
t.Fatalf("tx consumes %d gas, more than available in L2 block %d", tx.Gas(), e.l2BuildingHeader.GasLimit)
}
if tx.Gas() > uint64(*e.l2GasPool) {
t.InvalidAction("action takes too much gas: %d, only have %d", tx.Gas(), uint64(*e.l2GasPool))
return
}
e.pendingIndices[from] = i + 1 // won't retry the tx
e.l2BuildingState.SetTxContext(tx.Hash(), len(e.l2Transactions))
receipt, err := core.ApplyTransaction(e.l2Cfg.Config, e.l2Chain, &e.l2BuildingHeader.Coinbase,
e.l2GasPool, e.l2BuildingState, e.l2BuildingHeader, tx, &e.l2BuildingHeader.GasUsed, *e.l2Chain.GetVMConfig())
if err != nil {
e.l2TxFailed = append(e.l2TxFailed, tx)
t.Fatalf("failed to apply transaction to L2 block (tx %d): %v", len(e.l2Transactions), err)
err := e.engineApi.IncludeTx(tx, from)
if errors.Is(err, engineapi.ErrNotBuildingBlock) {
t.InvalidAction(err.Error())
} else if errors.Is(err, engineapi.ErrUsesTooMuchGas) {
t.InvalidAction("included tx uses too much gas: %v", err)
} else if err != nil {
t.Fatalf("include tx: %v", err)
}
e.l2Receipts = append(e.l2Receipts, receipt)
e.l2Transactions = append(e.l2Transactions, tx)
}
}
......
......@@ -4,6 +4,8 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi/test"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
......@@ -187,3 +189,15 @@ func TestL2EngineAPIFail(gt *testing.T) {
require.NoError(t, err)
require.Equal(gt, sd.L2Cfg.ToBlock().Hash(), head.Hash(), "expecting engine to start at genesis")
}
func TestEngineAPITests(t *testing.T) {
test.RunEngineAPITests(t, func() engineapi.EngineBackend {
jwtPath := e2eutils.WriteDefaultJWT(t)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
n, _, apiBackend := newBackend(t, sd.L2Cfg, jwtPath, nil)
err := n.Start()
require.NoError(t, err)
return apiBackend
})
}
......@@ -98,7 +98,7 @@ func TestL2Sequencer_SequencerDrift(gt *testing.T) {
// We passed the sequencer drift: we can still keep the old origin, but can't include any txs
sequencer.ActL2KeepL1Origin(t)
sequencer.ActL2StartBlock(t)
require.True(t, engine.l2ForceEmpty, "engine should not be allowed to include anything after sequencer drift is surpassed")
require.True(t, engine.engineApi.ForcedEmpty(), "engine should not be allowed to include anything after sequencer drift is surpassed")
}
// TestL2Sequencer_SequencerOnlyReorg regression-tests a Goerli halt where the sequencer
......
package actions
package engineapi
import (
"context"
......@@ -11,26 +11,74 @@ import (
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"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/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum-optimism/optimism/op-node/eth"
)
type EngineBackend interface {
CurrentBlock() *types.Header
CurrentSafeBlock() *types.Header
CurrentFinalBlock() *types.Header
GetHeaderByHash(hash common.Hash) *types.Header
GetBlockByHash(hash common.Hash) *types.Block
GetBlock(hash common.Hash, number uint64) *types.Block
// GetHeader returns the header corresponding to the hash/number argument pair.
GetHeader(common.Hash, uint64) *types.Header
HasBlockAndState(hash common.Hash, number uint64) bool
GetCanonicalHash(n uint64) common.Hash
GetVMConfig() *vm.Config
Config() *params.ChainConfig
// Engine retrieves the chain's consensus engine.
Engine() consensus.Engine
StateAt(root common.Hash) (*state.StateDB, error)
InsertBlockWithoutSetHead(block *types.Block) error
SetCanonical(head *types.Block) (common.Hash, error)
SetFinalized(header *types.Header)
SetSafe(header *types.Header)
}
// L2EngineAPI wraps an engine actor, and implements the RPC backend required to serve the engine API.
// This re-implements some of the Geth API work, but changes the API backend so we can deterministically
// build and control the L2 block contents to reach very specific edge cases as desired for testing.
type L2EngineAPI L2Engine
type L2EngineAPI struct {
log log.Logger
backend EngineBackend
// L2 block building data
l2BuildingHeader *types.Header // block header that we add txs to for block building
l2BuildingState *state.StateDB // state used for block building
l2GasPool *core.GasPool // track gas used of ongoing building
pendingIndices map[common.Address]uint64 // per account, how many txs from the pool were already included in the block, since the pool is lagging behind block mining.
l2Transactions []*types.Transaction // collects txs that were successfully included into current block build
l2Receipts []*types.Receipt // collect receipts of ongoing building
l2ForceEmpty bool // when no additional txs may be processed (i.e. when sequencer drift runs out)
l2TxFailed []*types.Transaction // log of failed transactions which could not be included
payloadID engine.PayloadID // ID of payload that is currently being built
}
func NewL2EngineAPI(log log.Logger, backend EngineBackend) *L2EngineAPI {
return &L2EngineAPI{
log: log,
backend: backend,
}
}
var (
STATUS_INVALID = &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, PayloadID: nil}
STATUS_SYNCING = &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionSyncing}, PayloadID: nil}
INVALID_TERMINAL_BLOCK = eth.PayloadStatusV1{Status: eth.ExecutionInvalid, LatestValidHash: &common.Hash{}}
STATUS_INVALID = &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, PayloadID: nil}
STATUS_SYNCING = &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionSyncing}, PayloadID: nil}
)
// computePayloadId computes a pseudo-random payloadid, based on the parameters.
......@@ -53,16 +101,64 @@ func computePayloadId(headBlockHash common.Hash, params *eth.PayloadAttributes)
return out
}
func (ea *L2EngineAPI) RemainingBlockGas() uint64 {
return ea.l2GasPool.Gas()
}
func (ea *L2EngineAPI) ForcedEmpty() bool {
return ea.l2ForceEmpty
}
func (ea *L2EngineAPI) PendingIndices(from common.Address) uint64 {
return ea.pendingIndices[from]
}
var (
ErrNotBuildingBlock = errors.New("not currently building a block, cannot include tx from queue")
ErrExceedsGasLimit = errors.New("tx gas exceeds block gas limit")
ErrUsesTooMuchGas = errors.New("action takes too much gas")
)
func (ea *L2EngineAPI) IncludeTx(tx *types.Transaction, from common.Address) error {
if ea.l2BuildingHeader == nil {
return ErrNotBuildingBlock
}
if ea.l2ForceEmpty {
ea.log.Info("Skipping including a transaction because e.L2ForceEmpty is true")
// t.InvalidAction("cannot include any sequencer txs")
return nil
}
if tx.Gas() > ea.l2BuildingHeader.GasLimit {
return fmt.Errorf("%w tx gas: %d, block gas limit: %d", ErrExceedsGasLimit, tx.Gas(), ea.l2BuildingHeader.GasLimit)
}
if tx.Gas() > uint64(*ea.l2GasPool) {
return fmt.Errorf("%w: %d, only have %d", ErrUsesTooMuchGas, tx.Gas(), uint64(*ea.l2GasPool))
}
ea.pendingIndices[from] = ea.pendingIndices[from] + 1 // won't retry the tx
ea.l2BuildingState.SetTxContext(tx.Hash(), len(ea.l2Transactions))
receipt, err := core.ApplyTransaction(ea.backend.Config(), ea.backend, &ea.l2BuildingHeader.Coinbase,
ea.l2GasPool, ea.l2BuildingState, ea.l2BuildingHeader, tx, &ea.l2BuildingHeader.GasUsed, *ea.backend.GetVMConfig())
if err != nil {
ea.l2TxFailed = append(ea.l2TxFailed, tx)
return fmt.Errorf("invalid L2 block (tx %d): %w", len(ea.l2Transactions), err)
}
ea.l2Receipts = append(ea.l2Receipts, receipt)
ea.l2Transactions = append(ea.l2Transactions, tx)
return nil
}
func (ea *L2EngineAPI) startBlock(parent common.Hash, params *eth.PayloadAttributes) error {
if ea.l2BuildingHeader != nil {
ea.log.Warn("started building new block without ending previous block", "previous", ea.l2BuildingHeader, "prev_payload_id", ea.payloadID)
}
parentHeader := ea.l2Chain.GetHeaderByHash(parent)
parentHeader := ea.backend.GetHeaderByHash(parent)
if parentHeader == nil {
return fmt.Errorf("uknown parent block: %s", parent)
}
statedb, err := state.New(parentHeader.Root, state.NewDatabase(ea.l2Database), nil)
statedb, err := ea.backend.StateAt(parentHeader.Root)
if err != nil {
return fmt.Errorf("failed to init state db around block %s (state %s): %w", parent, parentHeader.Root, err)
}
......@@ -78,7 +174,7 @@ func (ea *L2EngineAPI) startBlock(parent common.Hash, params *eth.PayloadAttribu
MixDigest: common.Hash(params.PrevRandao),
}
header.BaseFee = misc.CalcBaseFee(ea.l2Cfg.Config, parentHeader)
header.BaseFee = misc.CalcBaseFee(ea.backend.Config(), parentHeader)
ea.l2BuildingHeader = header
ea.l2BuildingState = statedb
......@@ -96,8 +192,8 @@ func (ea *L2EngineAPI) startBlock(parent common.Hash, params *eth.PayloadAttribu
return fmt.Errorf("transaction %d is not valid: %w", i, err)
}
ea.l2BuildingState.SetTxContext(tx.Hash(), i)
receipt, err := core.ApplyTransaction(ea.l2Cfg.Config, ea.l2Chain, &ea.l2BuildingHeader.Coinbase,
ea.l2GasPool, ea.l2BuildingState, ea.l2BuildingHeader, &tx, &ea.l2BuildingHeader.GasUsed, *ea.l2Chain.GetVMConfig())
receipt, err := core.ApplyTransaction(ea.backend.Config(), ea.backend, &ea.l2BuildingHeader.Coinbase,
ea.l2GasPool, ea.l2BuildingState, ea.l2BuildingHeader, &tx, &ea.l2BuildingHeader.GasUsed, *ea.backend.GetVMConfig())
if err != nil {
ea.l2TxFailed = append(ea.l2TxFailed, &tx)
return fmt.Errorf("failed to apply deposit transaction to L2 block (tx %d): %w", i, err)
......@@ -116,17 +212,8 @@ func (ea *L2EngineAPI) endBlock() (*types.Block, error) {
ea.l2BuildingHeader = nil
header.GasUsed = header.GasLimit - uint64(*ea.l2GasPool)
header.Root = ea.l2BuildingState.IntermediateRoot(ea.l2Cfg.Config.IsEIP158(header.Number))
header.Root = ea.l2BuildingState.IntermediateRoot(ea.backend.Config().IsEIP158(header.Number))
block := types.NewBlock(header, ea.l2Transactions, nil, ea.l2Receipts, trie.NewStackTrie(nil))
// Write state changes to db
root, err := ea.l2BuildingState.Commit(ea.l2Cfg.Config.IsEIP158(header.Number))
if err != nil {
return nil, fmt.Errorf("l2 state write error: %w", err)
}
if err := ea.l2BuildingState.Database().TrieDB().Commit(root, false); err != nil {
return nil, fmt.Errorf("l2 trie write error: %w", err)
}
return block, nil
}
......@@ -153,31 +240,16 @@ func (ea *L2EngineAPI) ForkchoiceUpdatedV1(ctx context.Context, state *eth.Forkc
// Check whether we have the block yet in our database or not. If not, we'll
// need to either trigger a sync, or to reject this forkchoice update for a
// reason.
block := ea.l2Chain.GetBlockByHash(state.HeadBlockHash)
block := ea.backend.GetBlockByHash(state.HeadBlockHash)
if block == nil {
// TODO: syncing not supported yet
return STATUS_SYNCING, nil
}
// Block is known locally, just sanity check that the beacon client does not
// attempt to push us back to before the merge.
if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 {
var (
td = ea.l2Chain.GetTd(state.HeadBlockHash, block.NumberU64())
ptd = ea.l2Chain.GetTd(block.ParentHash(), block.NumberU64()-1)
ttd = ea.l2Chain.Config().TerminalTotalDifficulty
)
if td == nil || (block.NumberU64() > 0 && ptd == nil) {
ea.log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", state.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd)
return STATUS_INVALID, errors.New("TDs unavailable for TDD check")
}
if td.Cmp(ttd) < 0 {
ea.log.Error("Refusing beacon update to pre-merge", "number", block.NumberU64(), "hash", state.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
return &eth.ForkchoiceUpdatedResult{PayloadStatus: INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil
}
if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 {
ea.log.Error("Parent block is already post-ttd", "number", block.NumberU64(), "hash", state.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
return &eth.ForkchoiceUpdatedResult{PayloadStatus: INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil
}
// Note: Differs from op-geth implementation as pre-merge blocks are never supported here
if block.Difficulty().BitLen() > 0 {
return STATUS_INVALID, errors.New("pre-merge blocks not supported")
}
valid := func(id *engine.PayloadID) *eth.ForkchoiceUpdatedResult {
return &eth.ForkchoiceUpdatedResult{
......@@ -185,16 +257,16 @@ func (ea *L2EngineAPI) ForkchoiceUpdatedV1(ctx context.Context, state *eth.Forkc
PayloadID: id,
}
}
if rawdb.ReadCanonicalHash(ea.l2Database, block.NumberU64()) != state.HeadBlockHash {
if ea.backend.GetCanonicalHash(block.NumberU64()) != state.HeadBlockHash {
// Block is not canonical, set head.
if latestValid, err := ea.l2Chain.SetCanonical(block); err != nil {
if latestValid, err := ea.backend.SetCanonical(block); err != nil {
return &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionInvalid, LatestValidHash: &latestValid}}, err
}
} else if ea.l2Chain.CurrentBlock().Hash() == state.HeadBlockHash {
} else if ea.backend.CurrentBlock().Hash() == state.HeadBlockHash {
// If the specified head matches with our local head, do nothing and keep
// generating the payload. It's a special corner case that a few slots are
// missing and we are requested to generate the payload in slot.
} else if ea.l2Chain.Config().Optimism == nil { // minor L2Engine API divergence: allow proposers to reorg their own chain
} else if ea.backend.Config().Optimism == nil { // minor L2Engine API divergence: allow proposers to reorg their own chain
panic("engine not configured as optimism engine")
}
......@@ -202,30 +274,30 @@ func (ea *L2EngineAPI) ForkchoiceUpdatedV1(ctx context.Context, state *eth.Forkc
// chain final and completely in PoS mode.
if state.FinalizedBlockHash != (common.Hash{}) {
// If the finalized block is not in our canonical tree, somethings wrong
finalHeader := ea.l2Chain.GetHeaderByHash(state.FinalizedBlockHash)
finalHeader := ea.backend.GetHeaderByHash(state.FinalizedBlockHash)
if finalHeader == nil {
ea.log.Warn("Final block not available in database", "hash", state.FinalizedBlockHash)
return STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not available in database"))
} else if rawdb.ReadCanonicalHash(ea.l2Database, finalHeader.Number.Uint64()) != state.FinalizedBlockHash {
} else if ea.backend.GetCanonicalHash(finalHeader.Number.Uint64()) != state.FinalizedBlockHash {
ea.log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", state.HeadBlockHash)
return STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not in canonical chain"))
}
// Set the finalized block
ea.l2Chain.SetFinalized(finalHeader)
ea.backend.SetFinalized(finalHeader)
}
// Check if the safe block hash is in our canonical tree, if not somethings wrong
if state.SafeBlockHash != (common.Hash{}) {
safeHeader := ea.l2Chain.GetHeaderByHash(state.SafeBlockHash)
safeHeader := ea.backend.GetHeaderByHash(state.SafeBlockHash)
if safeHeader == nil {
ea.log.Warn("Safe block not available in database")
return STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not available in database"))
}
if rawdb.ReadCanonicalHash(ea.l2Database, safeHeader.Number.Uint64()) != state.SafeBlockHash {
if ea.backend.GetCanonicalHash(safeHeader.Number.Uint64()) != state.SafeBlockHash {
ea.log.Warn("Safe block not in canonical chain")
return STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain"))
}
// Set the safe block
ea.l2Chain.SetSafe(safeHeader)
ea.backend.SetSafe(safeHeader)
}
// If payload generation was requested, create a new block to be potentially
// sealed by the beacon client. The payload will be requested later, and we
......@@ -270,7 +342,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP
}
// If we already have the block locally, ignore the entire execution and just
// return a fake success.
if block := ea.l2Chain.GetBlockByHash(payload.BlockHash); block != nil {
if block := ea.backend.GetBlockByHash(payload.BlockHash); block != nil {
ea.log.Warn("Ignoring already known beacon payload", "number", payload.BlockNumber, "hash", payload.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
hash := block.Hash()
return &eth.PayloadStatusV1{Status: eth.ExecutionValid, LatestValidHash: &hash}, nil
......@@ -278,16 +350,23 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP
// TODO: skipping invalid ancestor check (i.e. not remembering previously failed blocks)
parent := ea.l2Chain.GetBlock(block.ParentHash(), block.NumberU64()-1)
parent := ea.backend.GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
// TODO: hack, saying we accepted if we don't know the parent block. Might want to return critical error if we can't actually sync.
return &eth.PayloadStatusV1{Status: eth.ExecutionAccepted, LatestValidHash: nil}, nil
}
if !ea.l2Chain.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
if block.Time() <= parent.Time() {
log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
return ea.invalid(errors.New("invalid timestamp"), parent.Header()), nil
}
if !ea.backend.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
ea.log.Warn("State not available, ignoring new payload")
return &eth.PayloadStatusV1{Status: eth.ExecutionAccepted}, nil
}
if err := ea.l2Chain.InsertBlockWithoutSetHead(block); err != nil {
log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number)
if err := ea.backend.InsertBlockWithoutSetHead(block); err != nil {
ea.log.Warn("NewPayloadV1: inserting block failed", "error", err)
// TODO not remembering the payload as invalid
return ea.invalid(err, parent.Header()), nil
......@@ -297,7 +376,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP
}
func (ea *L2EngineAPI) invalid(err error, latestValid *types.Header) *eth.PayloadStatusV1 {
currentHash := ea.l2Chain.CurrentBlock().Hash()
currentHash := ea.backend.CurrentBlock().Hash()
if latestValid != nil {
// Set latest valid hash to 0x0 if parent is PoW block
currentHash = common.Hash{}
......
package test
import (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var gasLimit = eth.Uint64Quantity(30_000_000)
var feeRecipient = common.Address{}
func RunEngineAPITests(t *testing.T, createBackend func() engineapi.EngineBackend) {
t.Run("CreateBlock", func(t *testing.T) {
api := newTestHelper(t, createBackend)
block := api.addBlock()
api.assert.Equal(block.BlockHash, api.headHash(), "should create and import new block")
})
t.Run("IncludeRequiredTransactions", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
txData, err := derive.L1InfoDeposit(1, eth.HeaderBlockInfo(genesis), eth.SystemConfig{}, true)
api.assert.NoError(err)
tx := types.NewTx(txData)
block := api.addBlock(tx)
api.assert.Equal(block.BlockHash, api.headHash(), "should create and import new block")
imported := api.backend.GetBlockByHash(block.BlockHash)
api.assert.Len(imported.Transactions(), 1, "should include transaction")
})
t.Run("IgnoreUpdateHeadToOlderBlock", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesisHash := api.headHash()
api.addBlock()
block := api.addBlock()
api.assert.Equal(block.BlockHash, api.headHash(), "should have extended chain")
api.forkChoiceUpdated(genesisHash, genesisHash, genesisHash)
api.assert.Equal(block.BlockHash, api.headHash(), "should not have reset chain head")
})
t.Run("AllowBuildingOnOlderBlock", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
api.addBlock()
block := api.addBlock()
api.assert.Equal(block.BlockHash, api.headHash(), "should have extended chain")
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+3))
api.assert.Equal(block.BlockHash, api.headHash(), "should not reset chain head when building starts")
payload := api.getPayload(payloadID)
api.assert.Equal(genesis.Hash(), payload.ParentHash, "should have old block as parent")
api.newPayload(payload)
api.forkChoiceUpdated(payload.BlockHash, genesis.Hash(), genesis.Hash())
api.assert.Equal(payload.BlockHash, api.headHash(), "should reorg to block built on old parent")
})
t.Run("RejectInvalidBlockHash", func(t *testing.T) {
api := newTestHelper(t, createBackend)
// Invalid because BlockHash won't be correct (among many other reasons)
block := &eth.ExecutionPayload{}
r, err := api.engine.NewPayloadV1(api.ctx, block)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalidBlockHash, r.Status)
})
t.Run("RejectBlockWithInvalidStateTransition", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
// Build a valid block
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+2))
newBlock := api.getPayload(payloadID)
// But then make it invalid by changing the state root
newBlock.StateRoot = eth.Bytes32(genesis.TxHash)
updateBlockHash(newBlock)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
t.Run("RejectBlockWithSameTimeAsParent", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time))
newBlock := api.getPayload(payloadID)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
t.Run("RejectBlockWithTimeBeforeParent", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time-1))
newBlock := api.getPayload(payloadID)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
t.Run("UpdateSafeAndFinalizedHead", func(t *testing.T) {
api := newTestHelper(t, createBackend)
finalized := api.addBlock()
safe := api.addBlock()
head := api.addBlock()
api.forkChoiceUpdated(head.BlockHash, safe.BlockHash, finalized.BlockHash)
api.assert.Equal(head.BlockHash, api.headHash(), "should update head block")
api.assert.Equal(safe.BlockHash, api.safeHash(), "should update safe block")
api.assert.Equal(finalized.BlockHash, api.finalHash(), "should update finalized block")
})
t.Run("RejectSafeHeadWhenNotAncestor", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
api.addBlock()
chainA2 := api.addBlock()
chainA3 := api.addBlock()
chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: chainA3.BlockHash,
SafeBlockHash: chainB1.BlockHash,
FinalizedBlockHash: chainA2.BlockHash,
}, nil)
api.assert.ErrorContains(err, "Invalid forkchoice state", "should return error from forkChoiceUpdated")
api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status, "forkChoiceUpdated should return invalid")
api.assert.Nil(result.PayloadID, "should not provide payload ID when invalid")
})
t.Run("RejectFinalizedHeadWhenNotAncestor", func(t *testing.T) {
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentBlock()
api.addBlock()
chainA2 := api.addBlock()
chainA3 := api.addBlock()
chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: chainA3.BlockHash,
SafeBlockHash: chainA2.BlockHash,
FinalizedBlockHash: chainB1.BlockHash,
}, nil)
api.assert.ErrorContains(err, "Invalid forkchoice state", "should return error from forkChoiceUpdated")
api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status, "forkChoiceUpdated should return invalid")
api.assert.Nil(result.PayloadID, "should not provide payload ID when invalid")
})
}
// Updates the block hash to the expected value based on the other fields in the payload
func updateBlockHash(newBlock *eth.ExecutionPayload) {
// And fix up the block hash
newHash, _ := newBlock.CheckBlockHash()
newBlock.BlockHash = newHash
}
type testHelper struct {
t *testing.T
ctx context.Context
engine *engineapi.L2EngineAPI
backend engineapi.EngineBackend
assert *require.Assertions
}
func newTestHelper(t *testing.T, createBackend func() engineapi.EngineBackend) *testHelper {
logger := testlog.Logger(t, log.LvlDebug)
ctx := context.Background()
backend := createBackend()
api := engineapi.NewL2EngineAPI(logger, backend)
test := &testHelper{
t: t,
ctx: ctx,
engine: api,
backend: backend,
assert: require.New(t),
}
return test
}
func (h *testHelper) headHash() common.Hash {
return h.backend.CurrentBlock().Hash()
}
func (h *testHelper) safeHash() common.Hash {
return h.backend.CurrentSafeBlock().Hash()
}
func (h *testHelper) finalHash() common.Hash {
return h.backend.CurrentFinalBlock().Hash()
}
func (h *testHelper) Log(args ...any) {
h.t.Log(args...)
}
func (h *testHelper) addBlock(txs ...*types.Transaction) *eth.ExecutionPayload {
head := h.backend.CurrentBlock()
return h.addBlockWithParent(head, eth.Uint64Quantity(head.Time+2), txs...)
}
func (h *testHelper) addBlockWithParent(head *types.Header, timestamp eth.Uint64Quantity, txs ...*types.Transaction) *eth.ExecutionPayload {
prevHead := h.backend.CurrentBlock()
id := h.startBlockBuilding(head, timestamp, txs...)
block := h.getPayload(id)
h.assert.Equal(timestamp, block.Timestamp, "should create block with correct timestamp")
h.assert.Equal(head.Hash(), block.ParentHash, "should have correct parent")
h.assert.Len(block.Transactions, len(txs))
h.newPayload(block)
// Should not have changed the chain head yet
h.assert.Equal(prevHead, h.backend.CurrentBlock())
h.forkChoiceUpdated(block.BlockHash, head.Hash(), head.Hash())
h.assert.Equal(block.BlockHash, h.backend.CurrentBlock().Hash())
return block
}
func (h *testHelper) forkChoiceUpdated(head common.Hash, safe common.Hash, finalized common.Hash) {
h.Log("forkChoiceUpdated", "head", head, "safe", safe, "finalized", finalized)
result, err := h.engine.ForkchoiceUpdatedV1(h.ctx, &eth.ForkchoiceState{
HeadBlockHash: head,
SafeBlockHash: safe,
FinalizedBlockHash: finalized,
}, nil)
h.assert.NoError(err)
h.assert.Equal(eth.ExecutionValid, result.PayloadStatus.Status, "forkChoiceUpdated should return valid")
h.assert.Nil(result.PayloadStatus.ValidationError, "should not have validation error when valid")
h.assert.Nil(result.PayloadID, "should not provide payload ID when block building not requested")
}
func (h *testHelper) startBlockBuilding(head *types.Header, newBlockTimestamp eth.Uint64Quantity, txs ...*types.Transaction) *eth.PayloadID {
h.Log("Start block building", "head", head.Hash(), "timestamp", newBlockTimestamp)
var txData []eth.Data
for _, tx := range txs {
rlp, err := tx.MarshalBinary()
h.assert.NoError(err, "Failed to marshall tx %v", tx)
txData = append(txData, rlp)
}
result, err := h.engine.ForkchoiceUpdatedV1(h.ctx, &eth.ForkchoiceState{
HeadBlockHash: head.Hash(),
SafeBlockHash: head.Hash(),
FinalizedBlockHash: head.Hash(),
}, &eth.PayloadAttributes{
Timestamp: newBlockTimestamp,
PrevRandao: eth.Bytes32(head.MixDigest),
SuggestedFeeRecipient: feeRecipient,
Transactions: txData,
NoTxPool: true,
GasLimit: &gasLimit,
})
h.assert.NoError(err)
h.assert.Equal(eth.ExecutionValid, result.PayloadStatus.Status)
id := result.PayloadID
h.assert.NotNil(id)
return id
}
func (h *testHelper) getPayload(id *eth.PayloadID) *eth.ExecutionPayload {
h.Log("getPayload", "id", id)
block, err := h.engine.GetPayloadV1(h.ctx, *id)
h.assert.NoError(err)
h.assert.NotNil(block)
return block
}
func (h *testHelper) newPayload(block *eth.ExecutionPayload) {
h.Log("newPayload", "hash", block.BlockHash)
r, err := h.engine.NewPayloadV1(h.ctx, block)
h.assert.NoError(err)
h.assert.Equal(eth.ExecutionValid, r.Status)
h.assert.Nil(r.ValidationError)
}
......@@ -215,7 +215,7 @@ contract SystemDictator is OwnableUpgradeable {
/**
* @notice Configures the ProxyAdmin contract.
*/
function step1() external onlyOwner step(1) {
function step1() public onlyOwner step(1) {
// Set the AddressManager in the ProxyAdmin.
config.globalConfig.proxyAdmin.setAddressManager(config.globalConfig.addressManager);
......@@ -260,7 +260,7 @@ contract SystemDictator is OwnableUpgradeable {
* @notice Pauses the system by shutting down the L1CrossDomainMessenger and setting the
* deposit halt flag to tell the Sequencer's DTL to stop accepting deposits.
*/
function step2() external onlyOwner step(2) {
function step2() public onlyOwner step(2) {
// Store the address of the old L1CrossDomainMessenger implementation. We will need this
// address in the case that we have to exit early.
oldL1CrossDomainMessenger = config.globalConfig.addressManager.getAddress(
......@@ -410,6 +410,14 @@ contract SystemDictator is OwnableUpgradeable {
);
}
/**
* @notice Calls the first 2 steps of the migration process.
*/
function phase1() external onlyOwner {
step1();
step2();
}
/**
* @notice Tranfers admin ownership to the final owner.
*/
......
......@@ -2,6 +2,7 @@
"finalSystemOwner": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"portalGuardian": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"controller": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"proxyAdminOwner": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"l1StartingBlockTag": "earliest",
"l1ChainID": 900,
......@@ -16,12 +17,22 @@
"batchSenderAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"l2OutputOracleSubmissionInterval": 6,
"l2OutputOracleStartingTimestamp": -1,
"l2OutputOracleStartingTimestamp": 0,
"l2OutputOracleStartingBlockNumber": 0,
"l2OutputOracleProposer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"l2OutputOracleChallenger": "0x6925B8704Ff96DEe942623d6FB5e946EF5884b63",
"l2GenesisBlockBaseFeePerGas": "0x3B9ACA00",
"baseFeeVaultRecipient": "0xBcd4042DE499D14e55001CcbB24a551F3b954096",
"l2GenesisBlockGasLimit": "0x17D7840",
"baseFeeVaultRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"l1FeeVaultRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"sequencerFeeVaultRecipient": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"governanceTokenName": "Optimism",
"governanceTokenSymbol": "OP",
"governanceTokenOwner": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"l1FeeVaultRecipient": "0x71bE63f3384f5fb98995898A86B02Fb2426c5788",
"sequencerFeeVaultRecipient": "0xfabb0ac9d68b0b445fb7357272ff202c5651694a",
......
......@@ -11,10 +11,8 @@ import {
assertContractVariable,
getContractsFromArtifacts,
getDeploymentAddress,
doStep,
jsonifyTransaction,
getTenderlySimulationLink,
getCastCommand,
doOwnershipTransfer,
doPhase,
} from '../src/deploy-utils'
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
......@@ -73,10 +71,13 @@ const deployFn: DeployFunction = async (hre) => {
// Transfer ownership of the ProxyAdmin to the SystemDictator.
if ((await ProxyAdmin.owner()) !== SystemDictator.address) {
console.log(`Setting ProxyAdmin owner to MSD`)
await ProxyAdmin.transferOwnership(SystemDictator.address)
} else {
console.log(`Proxy admin already owned by MSD`)
await doOwnershipTransfer({
isLiveDeployer,
proxy: ProxyAdmin,
name: 'ProxyAdmin',
transferFunc: 'transferOwnership',
dictator: SystemDictator,
})
}
// We don't need to transfer proxy addresses if we're already beyond the proxy transfer step.
......@@ -89,31 +90,13 @@ const deployFn: DeployFunction = async (hre) => {
needsProxyTransfer &&
(await AddressManager.owner()) !== SystemDictator.address
) {
if (isLiveDeployer) {
console.log(`Setting AddressManager owner to MSD`)
await AddressManager.transferOwnership(SystemDictator.address)
} else {
const tx = await AddressManager.populateTransaction.transferOwnership(
SystemDictator.address
)
console.log(`Please transfer AddressManager owner to MSD`)
console.log(`AddressManager address: ${AddressManager.address}`)
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
await awaitCondition(
async () => {
const owner = await AddressManager.owner()
return owner === SystemDictator.address
},
5000,
1000
)
await doOwnershipTransfer({
isLiveDeployer,
proxy: AddressManager,
name: 'AddressManager',
transferFunc: 'transferOwnership',
dictator: SystemDictator,
})
} else {
console.log(`AddressManager already owned by the SystemDictator`)
}
......@@ -125,35 +108,13 @@ const deployFn: DeployFunction = async (hre) => {
from: ethers.constants.AddressZero,
})) !== SystemDictator.address
) {
if (isLiveDeployer) {
console.log(`Setting L1StandardBridge owner to MSD`)
await L1StandardBridgeProxyWithSigner.setOwner(SystemDictator.address)
} else {
const tx = await L1StandardBridgeProxy.populateTransaction.setOwner(
SystemDictator.address
)
console.log(`Please transfer L1StandardBridge (proxy) owner to MSD`)
console.log(
`L1StandardBridgeProxy address: ${L1StandardBridgeProxy.address}`
)
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
await awaitCondition(
async () => {
const owner = await L1StandardBridgeProxy.callStatic.getOwner({
from: ethers.constants.AddressZero,
})
return owner === SystemDictator.address
},
5000,
1000
)
await doOwnershipTransfer({
isLiveDeployer,
proxy: L1StandardBridgeProxyWithSigner,
name: 'L1StandardBridgeProxy',
transferFunc: 'setOwner',
dictator: SystemDictator,
})
} else {
console.log(`L1StandardBridge already owned by MSD`)
}
......@@ -165,47 +126,58 @@ const deployFn: DeployFunction = async (hre) => {
from: ethers.constants.AddressZero,
})) !== SystemDictator.address
) {
if (isLiveDeployer) {
console.log(`Setting L1ERC721Bridge owner to MSD`)
await L1ERC721BridgeProxyWithSigner.changeAdmin(SystemDictator.address)
} else {
const tx = await L1ERC721BridgeProxy.populateTransaction.changeAdmin(
SystemDictator.address
)
console.log(`Please transfer L1ERC721Bridge (proxy) owner to MSD`)
console.log(`L1ERC721BridgeProxy address: ${L1ERC721BridgeProxy.address}`)
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
}
// Wait for the ownership transfer to complete.
await awaitCondition(
async () => {
const owner = await L1ERC721BridgeProxy.callStatic.admin({
from: ethers.constants.AddressZero,
})
return owner === SystemDictator.address
},
5000,
1000
)
await doOwnershipTransfer({
isLiveDeployer,
proxy: L1ERC721BridgeProxyWithSigner,
name: 'L1ERC721BridgeProxy',
transferFunc: 'changeAdmin',
dictator: SystemDictator,
})
} else {
console.log(`L1ERC721Bridge already owned by MSD`)
}
// Step 1 is a freebie, it doesn't impact the system.
await doStep({
// Wait for the ownership transfers to complete before continuing.
await awaitCondition(
async (): Promise<boolean> => {
const proxyAdminOwner = await ProxyAdmin.owner()
const addressManagerOwner = await AddressManager.owner()
const l1StandardBridgeOwner =
await L1StandardBridgeProxy.callStatic.getOwner({
from: ethers.constants.AddressZero,
})
const l1Erc721BridgeOwner = await L1ERC721BridgeProxy.callStatic.admin({
from: ethers.constants.AddressZero,
})
return (
proxyAdminOwner === SystemDictator.address &&
addressManagerOwner === SystemDictator.address &&
l1StandardBridgeOwner === SystemDictator.address &&
l1Erc721BridgeOwner === SystemDictator.address
)
},
5000,
1000
)
await doPhase({
isLiveDeployer,
SystemDictator,
step: 1,
phase: 1,
message: `
Phase 1 includes the following steps:
Step 1 will configure the ProxyAdmin contract, you can safely execute this step at any time
without impacting the functionality of the rest of the system.
Step 2 will stop deposits and withdrawals via the L1CrossDomainMessenger and will stop the
DTL from syncing new deposits via the CTC, effectively shutting down the legacy system. Once
this step has been executed, you should immediately begin the L2 migration process. If you
need to restart the system, run exit1() followed by finalize().
`,
checks: async () => {
// Step 1 checks
await assertContractVariable(
ProxyAdmin,
'addressManager',
......@@ -264,21 +236,8 @@ const deployFn: DeployFunction = async (hre) => {
assert(config.systemTxMaxGas === 1_000_000)
assert(ethers.utils.parseUnits('1', 'gwei').eq(config.minimumBaseFee))
assert(config.maximumBaseFee.eq(uint128Max))
},
})
// Step 2 shuts down the system.
await doStep({
isLiveDeployer,
SystemDictator,
step: 2,
message: `
Step 2 will stop deposits and withdrawals via the L1CrossDomainMessenger and will stop the
DTL from syncing new deposits via the CTC, effectively shutting down the legacy system. Once
this step has been executed, you should immediately begin the L2 migration process. If you
need to restart the system, run exit1() followed by finalize().
`,
checks: async () => {
// Step 2 checks
const messenger = await AddressManager.getAddress(
'OVM_L1CrossDomainMessenger'
)
......
......@@ -10,11 +10,11 @@ import '@nomiclabs/hardhat-ethers'
import {
assertContractVariable,
getContractsFromArtifacts,
jsonifyTransaction,
printJsonTransaction,
isStep,
doStep,
getTenderlySimulationLink,
getCastCommand,
printTenderlySimulationLink,
printCastCommand,
} from '../src/deploy-utils'
const deployFn: DeployFunction = async (hre) => {
......@@ -206,10 +206,9 @@ const deployFn: DeployFunction = async (hre) => {
)
)
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(SystemDictator.provider, tx)
}
await awaitCondition(
......@@ -318,10 +317,9 @@ const deployFn: DeployFunction = async (hre) => {
const tx = await OptimismPortal.populateTransaction.unpause()
console.log(`Please unpause the OptimismPortal...`)
console.log(`OptimismPortal address: ${OptimismPortal.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(SystemDictator.provider, tx)
}
await awaitCondition(
......@@ -348,10 +346,9 @@ const deployFn: DeployFunction = async (hre) => {
const tx = await SystemDictator.populateTransaction.finalize()
console.log(`Please finalize deployment...`)
console.log(`MSD address: ${SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(getCastCommand(tx))
console.log(await getTenderlySimulationLink(SystemDictator.provider, tx))
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(SystemDictator.provider, tx)
}
await awaitCondition(
......
......@@ -305,20 +305,56 @@ export const getDeploymentAddress = async (
* @param tx Ethers transaction object.
* @returns JSON-ified transaction object.
*/
export const jsonifyTransaction = (tx: ethers.PopulatedTransaction): string => {
return JSON.stringify(
{
from: tx.from,
to: tx.to,
data: tx.data,
value: tx.value,
chainId: tx.chainId,
},
null,
2
export const printJsonTransaction = (tx: ethers.PopulatedTransaction): void => {
console.log(
'JSON transaction parameters:\n' +
JSON.stringify(
{
from: tx.from,
to: tx.to,
data: tx.data,
value: tx.value,
chainId: tx.chainId,
},
null,
2
)
)
}
/**
* Mini helper for transferring a Proxy to the MSD
*
* @param opts Options for executing the step.
* @param opts.isLiveDeployer True if the deployer is live.
* @param opts.proxy proxy contract.
* @param opts.dictator dictator contract.
*/
export const doOwnershipTransfer = async (opts: {
isLiveDeployer?: boolean
proxy: ethers.Contract
name: string
transferFunc: string
dictator: ethers.Contract
}): Promise<void> => {
if (opts.isLiveDeployer) {
console.log(`Setting ${opts.name} owner to MSD`)
await opts.proxy[opts.transferFunc](opts.dictator.address)
} else {
const tx = await opts.proxy.populateTransaction[opts.transferFunc](
opts.dictator.address
)
console.log(`
Please transfer ${opts.name} (proxy) owner to MSD
- ${opts.name} address: ${opts.proxy.address}
- MSD address: ${opts.dictator.address}
`)
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(opts.dictator.provider, tx)
}
}
/**
* Mini helper for checking if the current step is a target step.
*
......@@ -333,6 +369,25 @@ export const isStep = async (
return (await dictator.currentStep()) === step
}
/**
* Mini helper for checking if the current step is the first step in target phase.
*
* @param dictator SystemDictator contract.
* @param phase Target phase.
* @returns True if the current step is the first step in target phase.
*/
export const isStartOfPhase = async (
dictator: ethers.Contract,
phase: number
): Promise<boolean> => {
const phaseToStep = {
1: 1,
2: 3,
3: 6,
}
return (await dictator.currentStep()) === phaseToStep[phase]
}
/**
* Mini helper for executing a given step.
*
......@@ -350,7 +405,8 @@ export const doStep = async (opts: {
message: string
checks: () => Promise<void>
}): Promise<void> => {
if (!(await isStep(opts.SystemDictator, opts.step))) {
const isStepVal = await isStep(opts.SystemDictator, opts.step)
if (!isStepVal) {
console.log(`Step already completed: ${opts.step}`)
return
}
......@@ -368,11 +424,8 @@ export const doStep = async (opts: {
]()
console.log(`Please execute step ${opts.step}...`)
console.log(`MSD address: ${opts.SystemDictator.address}`)
console.log(`JSON:`)
console.log(jsonifyTransaction(tx))
console.log(
await getTenderlySimulationLink(opts.SystemDictator.provider, tx)
)
printJsonTransaction(tx)
await printTenderlySimulationLink(opts.SystemDictator.provider, tx)
}
// Wait for the step to complete.
......@@ -389,36 +442,91 @@ export const doStep = async (opts: {
}
/**
* Returns a direct link to a Tenderly simulation.
* Mini helper for executing a given phase.
*
* @param opts Options for executing the step.
* @param opts.isLiveDeployer True if the deployer is live.
* @param opts.SystemDictator SystemDictator contract.
* @param opts.step Step to execute.
* @param opts.message Message to print before executing the step.
* @param opts.checks Checks to perform after executing the step.
*/
export const doPhase = async (opts: {
isLiveDeployer?: boolean
SystemDictator: ethers.Contract
phase: number
message: string
checks: () => Promise<void>
}): Promise<void> => {
const isStart = await isStartOfPhase(opts.SystemDictator, opts.phase)
if (!isStart) {
console.log(`Start of phase ${opts.phase} already completed`)
return
}
// Extra message to help the user understand what's going on.
console.log(opts.message)
// Either automatically or manually execute the step.
if (opts.isLiveDeployer) {
console.log(`Executing phase ${opts.phase}...`)
await opts.SystemDictator[`phase${opts.phase}`]()
} else {
const tx = await opts.SystemDictator.populateTransaction[
`phase${opts.phase}`
]()
console.log(`Please execute phase ${opts.phase}...`)
console.log(`MSD address: ${opts.SystemDictator.address}`)
printJsonTransaction(tx)
await printTenderlySimulationLink(opts.SystemDictator.provider, tx)
}
// Wait for the step to complete.
await awaitCondition(
async () => {
return isStartOfPhase(opts.SystemDictator, opts.phase + 1)
},
30000,
1000
)
// Perform post-step checks.
await opts.checks()
}
/**
* Prints 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 (
export const printTenderlySimulationLink = async (
provider: ethers.providers.Provider,
tx: ethers.PopulatedTransaction
): Promise<string> => {
): Promise<void> => {
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()}`
console.log(
`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()}`
)
}
}
/**
* Returns a cast commmand for submitting a given transaction.
* Prints a cast commmand for submitting a given transaction.
*
* @param tx Ethers transaction object.
* @returns the cast command
*/
export const getCastCommand = (tx: ethers.PopulatedTransaction): string => {
export const printCastCommand = (tx: ethers.PopulatedTransaction): void => {
if (process.env.CAST_COMMANDS) {
return `cast send ${tx.to} ${tx.data} --from ${tx.from} --value ${tx.value}`
console.log(
`cast send ${tx.to} ${tx.data} --from ${tx.from} --value ${tx.value}`
)
}
}
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