Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions op-e2e/actions/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ type defaultTesting struct {
state ActionStatus
}

type StatefulTesting interface {
Testing
Reset(actionCtx context.Context)
State() ActionStatus
}

// NewDefaultTesting returns a new testing obj.
// Returns an interface, we're likely changing the behavior here as we build more action tests.
func NewDefaultTesting(tb e2eutils.TestingBase) StatefulTesting {
return &defaultTesting{
TestingBase: tb,
ctx: context.Background(),
state: ActionOK,
}
}

// Ctx shares a context to execute an action with, the test runner may interrupt the action without stopping the test.
func (st *defaultTesting) Ctx() context.Context {
return st.ctx
Expand Down
142 changes: 142 additions & 0 deletions op-e2e/actions/l1_miner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package actions

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
)

// L1Miner wraps a L1Replica with instrumented block building ability.
type L1Miner struct {
L1Replica

// L1 block building data
l1BuildingHeader *types.Header // block header that we add txs to for block building
l1BuildingState *state.StateDB // state used for block building
l1GasPool *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.
l1Transactions []*types.Transaction // collects txs that were successfully included into current block build
l1Receipts []*types.Receipt // collect receipts of ongoing building
l1Building bool
l1TxFailed []*types.Transaction // log of failed transactions which could not be included
}

// NewL1Miner creates a new L1Replica that can also build blocks.
func NewL1Miner(log log.Logger, genesis *core.Genesis) *L1Miner {
rep := NewL1Replica(log, genesis)
return &L1Miner{
L1Replica: *rep,
}
}

// ActL1StartBlock returns an action to build a new L1 block on top of the head block,
// with timeDelta added to the head block time.
func (s *L1Miner) ActL1StartBlock(timeDelta uint64) Action {
return func(t Testing) {
if s.l1Building {
t.InvalidAction("not valid if we already started building a block")
}
if timeDelta == 0 {
t.Fatalf("invalid time delta: %d", timeDelta)
}

parent := s.l1Chain.CurrentHeader()
parentHash := parent.Hash()
statedb, err := state.New(parent.Root, state.NewDatabase(s.l1Database), nil)
if err != nil {
t.Fatalf("failed to init state db around block %s (state %s): %w", parentHash, parent.Root, err)
}
header := &types.Header{
ParentHash: parentHash,
Coinbase: parent.Coinbase,
Difficulty: common.Big0,
Number: new(big.Int).Add(parent.Number, common.Big1),
GasLimit: parent.GasLimit,
Time: parent.Time + timeDelta,
Extra: []byte("L1 was here"),
MixDigest: common.Hash{}, // TODO: maybe randomize this (prev-randao value)
}
if s.l1Cfg.Config.IsLondon(header.Number) {
header.BaseFee = misc.CalcBaseFee(s.l1Cfg.Config, parent)
// At the transition, double the gas limit so the gas target is equal to the old gas limit.
if !s.l1Cfg.Config.IsLondon(parent.Number) {
header.GasLimit = parent.GasLimit * params.ElasticityMultiplier
}
}

s.l1Building = true
s.l1BuildingHeader = header
s.l1BuildingState = statedb
s.l1Receipts = make([]*types.Receipt, 0)
s.l1Transactions = make([]*types.Transaction, 0)
s.pendingIndices = make(map[common.Address]uint64)

s.l1GasPool = new(core.GasPool).AddGas(header.GasLimit)
}
}

// ActL1IncludeTx includes the next tx from L1 tx pool from the given account
func (s *L1Miner) ActL1IncludeTx(from common.Address) Action {
return func(t Testing) {
if !s.l1Building {
t.InvalidAction("no tx inclusion when not building l1 block")
return
}
i := s.pendingIndices[from]
txs, q := s.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() > s.l1BuildingHeader.GasLimit {
t.Fatalf("tx consumes %d gas, more than available in L1 block %d", tx.Gas(), s.l1BuildingHeader.GasLimit)
}
if tx.Gas() > uint64(*s.l1GasPool) {
t.InvalidAction("action takes too much gas: %d, only have %d", tx.Gas(), uint64(*s.l1GasPool))
return
}
s.pendingIndices[from] = i + 1 // won't retry the tx
receipt, err := core.ApplyTransaction(s.l1Cfg.Config, s.l1Chain, &s.l1BuildingHeader.Coinbase,
s.l1GasPool, s.l1BuildingState, s.l1BuildingHeader, tx, &s.l1BuildingHeader.GasUsed, *s.l1Chain.GetVMConfig())
if err != nil {
s.l1TxFailed = append(s.l1TxFailed, tx)
t.Fatalf("failed to apply transaction to L1 block (tx %d): %w", len(s.l1Transactions), err)
}
s.l1Receipts = append(s.l1Receipts, receipt)
s.l1Transactions = append(s.l1Transactions, tx)
}
}

// ActL1EndBlock finishes the new L1 block, and applies it to the chain as unsafe block
func (s *L1Miner) ActL1EndBlock(t Testing) {
if !s.l1Building {
t.InvalidAction("cannot end L1 block when not building block")
return
}

s.l1Building = false
s.l1BuildingHeader.GasUsed = s.l1BuildingHeader.GasLimit - uint64(*s.l1GasPool)
s.l1BuildingHeader.Root = s.l1BuildingState.IntermediateRoot(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number))
block := types.NewBlock(s.l1BuildingHeader, s.l1Transactions, nil, s.l1Receipts, trie.NewStackTrie(nil))

// Write state changes to db
root, err := s.l1BuildingState.Commit(s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number))
if err != nil {
t.Fatalf("l1 state write error: %v", err)
}
if err := s.l1BuildingState.Database().TrieDB().Commit(root, false, nil); err != nil {
t.Fatalf("l1 trie write error: %v", err)
}

_, err = s.l1Chain.InsertChain(types.Blocks{block})
if err != nil {
t.Fatalf("failed to insert block into l1 chain")
}
}
59 changes: 59 additions & 0 deletions op-e2e/actions/l1_miner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package actions

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"

"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/testlog"
)

func TestL1Miner_BuildBlock(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
miner := NewL1Miner(log, sd.L1Cfg)

cl := miner.EthClient()
signer := types.LatestSigner(sd.L1Cfg.Config)

// send a tx to the miner
tx := types.MustSignNewTx(dp.Secrets.Alice, signer, &types.DynamicFeeTx{
ChainID: sd.L1Cfg.Config.ChainID,
Nonce: 0,
GasTipCap: big.NewInt(2 * params.GWei),
GasFeeCap: new(big.Int).Add(miner.l1Chain.CurrentBlock().BaseFee(), big.NewInt(2*params.GWei)),
Gas: params.TxGas,
To: &dp.Addresses.Bob,
Value: e2eutils.Ether(2),
})
require.NoError(gt, cl.SendTransaction(t.Ctx(), tx))

// make an empty block, even though a tx may be waiting
miner.ActL1StartBlock(10)(t)
miner.ActL1EndBlock(t)
bl := miner.l1Chain.CurrentBlock()
require.Equal(t, uint64(1), bl.NumberU64())
require.Zero(gt, bl.Transactions().Len())

// now include the tx when we want it to
miner.ActL1StartBlock(10)(t)
miner.ActL1IncludeTx(dp.Addresses.Alice)(t)
miner.ActL1EndBlock(t)
bl = miner.l1Chain.CurrentBlock()
require.Equal(t, uint64(2), bl.NumberU64())
require.Equal(t, 1, bl.Transactions().Len())
require.Equal(t, tx.Hash(), bl.Transactions()[0].Hash())

// now make a replica that syncs these two blocks from the miner
replica := NewL1Replica(log, sd.L1Cfg)
replica.ActL1Sync(miner.CanonL1Chain())(t)
replica.ActL1Sync(miner.CanonL1Chain())(t)
require.Equal(t, replica.l1Chain.CurrentBlock().Hash(), miner.l1Chain.CurrentBlock().Hash())
}
Loading