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

import (
	"context"
	"testing"

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

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

21
func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) {
22 23 24 25 26 27 28 29 30
	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)
31
		genesis := api.backend.CurrentHeader()
32 33 34 35 36 37 38 39

		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")
40 41 42 43 44 45 46 47 48

		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)
49
		genesis := api.backend.CurrentHeader()
50 51 52 53 54 55 56 57

		txData, err := derive.L1InfoDeposit(1, eth.HeaderBlockInfo(genesis), eth.SystemConfig{}, true)
		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
58 59
		nextBlockTime := eth.Uint64Quantity(genesis.Time + 1)

60
		var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
61
		if api.backend.Config().IsCanyon(uint64(nextBlockTime)) {
62
			w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
63 64 65
		}

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

	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)
95
		genesis := api.backend.CurrentHeader()
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
		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)

114
		var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
115
		if api.backend.Config().IsCanyon(uint64(0)) {
116
			w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
117 118
		}

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

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

		// 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)

Danyal Prout's avatar
Danyal Prout committed
140
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
141 142 143 144 145 146
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

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

149 150
		// Start with a valid time
		payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
151 152
		newBlock := api.getPayload(payloadID)

153 154 155 156
		// Then make it invalid to check NewPayload rejects it
		newBlock.Timestamp = eth.Uint64Quantity(genesis.Time)
		updateBlockHash(newBlock)

Danyal Prout's avatar
Danyal Prout committed
157
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
158 159 160 161 162 163
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

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

166 167
		// Start with a valid time
		payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
168 169
		newBlock := api.getPayload(payloadID)

170 171 172 173
		// Then make it invalid to check NewPayload rejects it
		newBlock.Timestamp = eth.Uint64Quantity(genesis.Time - 1)
		updateBlockHash(newBlock)

Danyal Prout's avatar
Danyal Prout committed
174
		r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
175 176 177 178
		api.assert.NoError(err)
		api.assert.Equal(eth.ExecutionInvalid, r.Status)
	})

179 180
	t.Run("RejectCreateBlockWithSameTimeAsParent", func(t *testing.T) {
		api := newTestHelper(t, createBackend)
181
		genesis := api.backend.CurrentHeader()
182

Danyal Prout's avatar
Danyal Prout committed
183
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
			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)
201
		genesis := api.backend.CurrentHeader()
202

Danyal Prout's avatar
Danyal Prout committed
203
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
			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)
221
		genesis := api.backend.CurrentHeader()
222 223 224

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

Danyal Prout's avatar
Danyal Prout committed
225
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
			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)
	})

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
	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)
256
		genesis := api.backend.CurrentHeader()
257 258 259 260 261 262 263

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

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

Danyal Prout's avatar
Danyal Prout committed
264
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
265 266 267 268 269 270 271 272 273 274 275
			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)
276
		genesis := api.backend.CurrentHeader()
277 278 279 280 281 282 283

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

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

Danyal Prout's avatar
Danyal Prout committed
284
		result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
			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
}

310
func newTestHelper(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) *testHelper {
311 312
	logger := testlog.Logger(t, log.LvlDebug)
	ctx := context.Background()
313
	backend := createBackend(t)
314 315 316 317 318 319 320 321 322 323 324 325
	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 {
326
	return h.backend.CurrentHeader().Hash()
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
}

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 {
342
	head := h.backend.CurrentHeader()
343 344 345 346
	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 {
347
	prevHead := h.backend.CurrentHeader()
348 349 350 351 352 353 354 355 356 357
	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
358
	h.assert.Equal(prevHead, h.backend.CurrentHeader())
359 360

	h.forkChoiceUpdated(block.BlockHash, head.Hash(), head.Hash())
361
	h.assert.Equal(block.BlockHash, h.backend.CurrentHeader().Hash())
362 363 364 365 366
	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
367
	result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
		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
386 387

	canyonTime := h.backend.Config().CanyonTime
388
	var w *types.Withdrawals
Danyal Prout's avatar
Danyal Prout committed
389
	if canyonTime != nil && *canyonTime <= uint64(newBlockTimestamp) {
390
		w = &types.Withdrawals{}
Danyal Prout's avatar
Danyal Prout committed
391 392 393
	}

	result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
394 395 396 397 398 399 400 401 402 403
		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
404
		Withdrawals:           w,
405 406 407 408 409 410 411 412 413 414
	})
	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)
Danyal Prout's avatar
Danyal Prout committed
415
	envelope, err := h.engine.GetPayloadV2(h.ctx, *id)
416
	h.assert.NoError(err)
Danyal Prout's avatar
Danyal Prout committed
417 418 419
	h.assert.NotNil(envelope)
	h.assert.NotNil(envelope.ExecutionPayload)
	return envelope.ExecutionPayload
420 421 422 423
}

func (h *testHelper) newPayload(block *eth.ExecutionPayload) {
	h.Log("newPayload", "hash", block.BlockHash)
Danyal Prout's avatar
Danyal Prout committed
424
	r, err := h.engine.NewPayloadV2(h.ctx, block)
425 426 427 428
	h.assert.NoError(err)
	h.assert.Equal(eth.ExecutionValid, r.Status)
	h.assert.Nil(r.ValidationError)
}