l2_engine_api_tests.go 15.3 KB
Newer Older
1 2 3 4 5 6
package test

import (
	"context"
	"testing"

7
	"github.com/ethereum-optimism/optimism/op-node/rollup"
8
	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
9
	"github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi"
10
	"github.com/ethereum-optimism/optimism/op-service/eth"
11
	"github.com/ethereum-optimism/optimism/op-service/testlog"
12 13 14
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/log"
15
	"github.com/ethereum/go-ethereum/params"
16 17 18 19 20 21
	"github.com/stretchr/testify/require"
)

var gasLimit = eth.Uint64Quantity(30_000_000)
var feeRecipient = common.Address{}

22
func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) {
23 24 25 26 27 28 29
	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")
	})

30 31 32 33
	zero := uint64(0)
	rollupCfg := &rollup.Config{
		RegolithTime: &zero, // activate Regolith upgrade
	}
34 35
	t.Run("IncludeRequiredTransactions", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
36
		genesis := api.backend.CurrentHeader()
37

38
		txData, err := derive.L1InfoDeposit(rollupCfg, eth.SystemConfig{}, 1, eth.HeaderBlockInfo(genesis), 0)
39 40 41 42 43 44
		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")
45 46 47 48 49 50 51 52 53

		api.assert.NotEqual(genesis.Root, block.StateRoot)
		newState, err := api.backend.StateAt(common.Hash(block.StateRoot))
		require.NoError(t, err, "imported block state should be available")
		require.NotNil(t, newState)
	})

	t.Run("RejectCreatingBlockWithInvalidRequiredTransaction", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
54
		genesis := api.backend.CurrentHeader()
55

56
		txData, err := derive.L1InfoDeposit(rollupCfg, eth.SystemConfig{}, 1, eth.HeaderBlockInfo(genesis), 0)
57 58 59 60 61 62
		api.assert.NoError(err)
		txData.Gas = uint64(gasLimit + 1)
		tx := types.NewTx(txData)
		txRlp, err := tx.MarshalBinary()
		api.assert.NoError(err)

Danyal Prout's avatar
Danyal Prout committed
63 64
		nextBlockTime := eth.Uint64Quantity(genesis.Time + 1)

65
		var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
66
		if api.backend.Config().IsCanyon(uint64(nextBlockTime)) {
67
			w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
68 69 70
		}

		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
71 72 73 74
			HeadBlockHash:      genesis.Hash(),
			SafeBlockHash:      genesis.Hash(),
			FinalizedBlockHash: genesis.Hash(),
		}, &eth.PayloadAttributes{
Danyal Prout's avatar
Danyal Prout committed
75
			Timestamp:             nextBlockTime,
76 77 78 79 80
			PrevRandao:            eth.Bytes32(genesis.MixDigest),
			SuggestedFeeRecipient: feeRecipient,
			Transactions:          []eth.Data{txRlp},
			NoTxPool:              true,
			GasLimit:              &gasLimit,
Danyal Prout's avatar
Danyal Prout committed
81
			Withdrawals:           w,
82 83 84
		})
		api.assert.Error(err)
		api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status)
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
	})

	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)
100
		genesis := api.backend.CurrentHeader()
101 102 103 104 105 106 107
		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")

108 109
		envelope := api.getPayload(payloadID)
		payload := envelope.ExecutionPayload
110 111 112 113 114 115 116 117 118 119
		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)

120
		var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
121
		if api.backend.Config().IsCanyon(uint64(0)) {
122
			w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
123 124
		}

125
		// Invalid because BlockHash won't be correct (among many other reasons)
Danyal Prout's avatar
Danyal Prout committed
126 127 128 129
		block := &eth.ExecutionPayload{
			Withdrawals: w,
		}
		r, err := api.engine.NewPayloadV2(api.ctx, block)
130 131 132 133 134 135
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalidBlockHash, r.Status)
	})

	t.Run("RejectBlockWithInvalidStateTransition", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
136
		genesis := api.backend.CurrentHeader()
137 138 139

		// Build a valid block
		payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+2))
140 141
		envelope := api.getPayload(payloadID)
		newBlock := envelope.ExecutionPayload
142 143 144

		// But then make it invalid by changing the state root
		newBlock.StateRoot = eth.Bytes32(genesis.TxHash)
145
		updateBlockHash(envelope)
146

Danyal Prout's avatar
Danyal Prout committed
147
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
148 149 150 151 152 153
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

	t.Run("RejectBlockWithSameTimeAsParent", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
154
		genesis := api.backend.CurrentHeader()
155

156 157
		// Start with a valid time
		payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
158 159
		envelope := api.getPayload(payloadID)
		newBlock := envelope.ExecutionPayload
160

161 162
		// Then make it invalid to check NewPayload rejects it
		newBlock.Timestamp = eth.Uint64Quantity(genesis.Time)
163
		updateBlockHash(envelope)
164

Danyal Prout's avatar
Danyal Prout committed
165
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
166 167 168 169 170 171
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

	t.Run("RejectBlockWithTimeBeforeParent", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
172
		genesis := api.backend.CurrentHeader()
173

174 175
		// Start with a valid time
		payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
176 177
		envelope := api.getPayload(payloadID)
		newBlock := envelope.ExecutionPayload
178

179 180
		// Then make it invalid to check NewPayload rejects it
		newBlock.Timestamp = eth.Uint64Quantity(genesis.Time - 1)
181
		updateBlockHash(envelope)
182

Danyal Prout's avatar
Danyal Prout committed
183
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
184 185 186 187
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

188 189
	t.Run("RejectCreateBlockWithSameTimeAsParent", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
190
		genesis := api.backend.CurrentHeader()
191

Danyal Prout's avatar
Danyal Prout committed
192
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
			HeadBlockHash:      genesis.Hash(),
			SafeBlockHash:      genesis.Hash(),
			FinalizedBlockHash: genesis.Hash(),
		}, &eth.PayloadAttributes{
			Timestamp:             eth.Uint64Quantity(genesis.Time),
			PrevRandao:            eth.Bytes32(genesis.MixDigest),
			SuggestedFeeRecipient: feeRecipient,
			Transactions:          nil,
			NoTxPool:              true,
			GasLimit:              &gasLimit,
		})
		api.assert.Error(err)
		api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status)
	})

	t.Run("RejectCreateBlockWithTimeBeforeParent", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
210
		genesis := api.backend.CurrentHeader()
211

Danyal Prout's avatar
Danyal Prout committed
212
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
			HeadBlockHash:      genesis.Hash(),
			SafeBlockHash:      genesis.Hash(),
			FinalizedBlockHash: genesis.Hash(),
		}, &eth.PayloadAttributes{
			Timestamp:             eth.Uint64Quantity(genesis.Time - 1),
			PrevRandao:            eth.Bytes32(genesis.MixDigest),
			SuggestedFeeRecipient: feeRecipient,
			Transactions:          nil,
			NoTxPool:              true,
			GasLimit:              &gasLimit,
		})
		api.assert.Error(err)
		api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status)
	})

	t.Run("RejectCreateBlockWithGasLimitAboveMax", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
230
		genesis := api.backend.CurrentHeader()
231 232 233

		gasLimit := eth.Uint64Quantity(params.MaxGasLimit + 1)

Danyal Prout's avatar
Danyal Prout committed
234
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
			HeadBlockHash:      genesis.Hash(),
			SafeBlockHash:      genesis.Hash(),
			FinalizedBlockHash: genesis.Hash(),
		}, &eth.PayloadAttributes{
			Timestamp:             eth.Uint64Quantity(genesis.Time + 1),
			PrevRandao:            eth.Bytes32(genesis.MixDigest),
			SuggestedFeeRecipient: feeRecipient,
			Transactions:          nil,
			NoTxPool:              true,
			GasLimit:              &gasLimit,
		})
		api.assert.Error(err)
		api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status)
	})

250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
	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)
265
		genesis := api.backend.CurrentHeader()
266 267 268 269 270 271 272

		api.addBlock()
		chainA2 := api.addBlock()
		chainA3 := api.addBlock()

		chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))

Danyal Prout's avatar
Danyal Prout committed
273
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
274 275 276 277 278 279 280 281 282 283 284
			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)
285
		genesis := api.backend.CurrentHeader()
286 287 288 289 290 291 292

		api.addBlock()
		chainA2 := api.addBlock()
		chainA3 := api.addBlock()

		chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))

Danyal Prout's avatar
Danyal Prout committed
293
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
294 295 296 297 298 299 300 301 302 303 304
			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
305
func updateBlockHash(envelope *eth.ExecutionPayloadEnvelope) {
306
	// And fix up the block hash
307 308
	newHash, _ := envelope.CheckBlockHash()
	envelope.ExecutionPayload.BlockHash = newHash
309 310 311 312 313 314 315 316 317 318
}

type testHelper struct {
	t       *testing.T
	ctx     context.Context
	engine  *engineapi.L2EngineAPI
	backend engineapi.EngineBackend
	assert  *require.Assertions
}

319
func newTestHelper(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) *testHelper {
320 321
	logger := testlog.Logger(t, log.LvlDebug)
	ctx := context.Background()
322
	backend := createBackend(t)
323
	api := engineapi.NewL2EngineAPI(logger, backend, nil)
324 325 326 327 328 329 330 331 332 333 334
	test := &testHelper{
		t:       t,
		ctx:     ctx,
		engine:  api,
		backend: backend,
		assert:  require.New(t),
	}
	return test
}

func (h *testHelper) headHash() common.Hash {
335
	return h.backend.CurrentHeader().Hash()
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
}

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 {
351
	head := h.backend.CurrentHeader()
352 353 354 355
	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 {
356
	prevHead := h.backend.CurrentHeader()
357 358
	id := h.startBlockBuilding(head, timestamp, txs...)

359 360 361
	envelope := h.getPayload(id)
	block := envelope.ExecutionPayload

362 363 364 365 366 367 368
	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
369
	h.assert.Equal(prevHead, h.backend.CurrentHeader())
370 371

	h.forkChoiceUpdated(block.BlockHash, head.Hash(), head.Hash())
372
	h.assert.Equal(block.BlockHash, h.backend.CurrentHeader().Hash())
373 374 375 376 377
	return block
}

func (h *testHelper) forkChoiceUpdated(head common.Hash, safe common.Hash, finalized common.Hash) {
	h.Log("forkChoiceUpdated", "head", head, "safe", safe, "finalized", finalized)
Danyal Prout's avatar
Danyal Prout committed
378
	result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
		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)
	}
Danyal Prout's avatar
Danyal Prout committed
397 398

	canyonTime := h.backend.Config().CanyonTime
399
	var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
400
	if canyonTime != nil && *canyonTime <= uint64(newBlockTimestamp) {
401
		w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
402 403 404
	}

	result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
405 406 407 408 409 410 411 412 413 414
		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,
Danyal Prout's avatar
Danyal Prout committed
415
		Withdrawals:           w,
416 417 418 419 420 421 422 423
	})
	h.assert.NoError(err)
	h.assert.Equal(eth.ExecutionValid, result.PayloadStatus.Status)
	id := result.PayloadID
	h.assert.NotNil(id)
	return id
}

424
func (h *testHelper) getPayload(id *eth.PayloadID) *eth.ExecutionPayloadEnvelope {
425
	h.Log("getPayload", "id", id)
Danyal Prout's avatar
Danyal Prout committed
426
	envelope, err := h.engine.GetPayloadV2(h.ctx, *id)
427
	h.assert.NoError(err)
Danyal Prout's avatar
Danyal Prout committed
428 429
	h.assert.NotNil(envelope)
	h.assert.NotNil(envelope.ExecutionPayload)
430
	return envelope
431 432 433 434
}

func (h *testHelper) newPayload(block *eth.ExecutionPayload) {
	h.Log("newPayload", "hash", block.BlockHash)
Danyal Prout's avatar
Danyal Prout committed
435
	r, err := h.engine.NewPayloadV2(h.ctx, block)
436 437 438 439
	h.assert.NoError(err)
	h.assert.Equal(eth.ExecutionValid, r.Status)
	h.assert.Nil(r.ValidationError)
}