diff --git a/op-sync-tester/example_config.yaml b/op-sync-tester/example_config.yaml index 121fd6a60bb0b..04092229c8ff5 100644 --- a/op-sync-tester/example_config.yaml +++ b/op-sync-tester/example_config.yaml @@ -1,7 +1,10 @@ synctesters: local: chain_id: 2151908 - el_rpc: http://localhost:32988/ + el_rpc: http://localhost:62654/ sepolia: chain_id: 11155420 - el_rpc: https://sepolia.optimism.io + el_rpc: https://sepolia.optimism.io + mainnet: + chain_id: 10 + el_rpc: https://mainnet.optimism.io diff --git a/op-sync-tester/synctester/backend/backend.go b/op-sync-tester/synctester/backend/backend.go index 80ae7b04b0e9c..1e370b9743ba2 100644 --- a/op-sync-tester/synctester/backend/backend.go +++ b/op-sync-tester/synctester/backend/backend.go @@ -35,6 +35,14 @@ func SessionFromContext(ctx context.Context) (*Session, bool) { type Session struct { SessionID string + + // Canonical view of the chain + CurrentState FCUState + + InitialState FCUState +} + +type FCUState struct { Latest uint64 Safe uint64 Finalized uint64 diff --git a/op-sync-tester/synctester/backend/el_reader.go b/op-sync-tester/synctester/backend/el_reader.go new file mode 100644 index 0000000000000..ee7ad6e8a442e --- /dev/null +++ b/op-sync-tester/synctester/backend/el_reader.go @@ -0,0 +1,82 @@ +package backend + +import ( + "context" + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" +) + +// ReadOnlyELBackend defines the minimal, read-only execution layer +// interface used by the sync tester and its mock backends. +// The interface exposes two flavors of block accessors: +// - JSON-returning methods (GetBlockByNumberJSON, GetBlockByHashJSON) +// which return the raw RPC payload exactly as delivered by the EL. +// These are useful for relaying the response from read-only exec layer directly +// - Typed methods (GetBlockByNumber, GetBlockByHash) which decode +// the RPC response into geth *types.Block for structured +// inspection in code. +// - Additional helpers include GetBlockReceipts and ChainId +// +// Implementation wraps ethclient.Client to forward RPC +// calls. For testing, a mock implementation can be provided to return +// deterministic values without requiring a live execution layer node. +type ReadOnlyELBackend interface { + GetBlockByNumberJSON(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) + GetBlockByHashJSON(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) + GetBlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) + GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) + ChainId(ctx context.Context) (hexutil.Big, error) +} + +var _ ReadOnlyELBackend = (*ELReader)(nil) + +type ELReader struct { + c *ethclient.Client +} + +func NewELReader(c *ethclient.Client) *ELReader { + return &ELReader{c: c} +} + +func (g *ELReader) GetBlockByNumberJSON(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) { + var raw json.RawMessage + if err := g.c.Client().CallContext(ctx, &raw, "eth_getBlockByNumber", number, fullTx); err != nil { + return nil, err + } + return raw, nil +} + +func (g *ELReader) GetBlockByHashJSON(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) { + var raw json.RawMessage + if err := g.c.Client().CallContext(ctx, &raw, "eth_getBlockByHash", hash, fullTx); err != nil { + return nil, err + } + return raw, nil +} + +func (g *ELReader) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + return g.c.BlockByNumber(ctx, big.NewInt(number.Int64())) +} + +func (g *ELReader) GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return g.c.BlockByHash(ctx, hash) +} + +func (g *ELReader) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) { + return g.c.BlockReceipts(ctx, blockNrOrHash) +} + +func (g *ELReader) ChainId(ctx context.Context) (hexutil.Big, error) { + chainID, err := g.c.ChainID(ctx) + if err != nil { + return hexutil.Big{}, err + } + return hexutil.Big(*chainID), nil +} diff --git a/op-sync-tester/synctester/backend/sync_tester.go b/op-sync-tester/synctester/backend/sync_tester.go index 1fbf3e226482a..3e7e34816cc5a 100644 --- a/op-sync-tester/synctester/backend/sync_tester.go +++ b/op-sync-tester/synctester/backend/sync_tester.go @@ -2,9 +2,9 @@ package backend import ( "context" + "encoding/json" "errors" "fmt" - "math/big" "sync" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -16,7 +16,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" - "github.com/holiman/uint256" "github.com/ethereum-optimism/optimism/op-sync-tester/synctester/backend/config" sttypes "github.com/ethereum-optimism/optimism/op-sync-tester/synctester/backend/types" @@ -34,13 +33,21 @@ type SyncTester struct { log log.Logger m metrics.Metricer - id sttypes.SyncTesterID - chainID eth.ChainID - elClient *ethclient.Client + id sttypes.SyncTesterID + chainID eth.ChainID + + elReader ReadOnlyELBackend sessions map[string]*Session } +// HeaderNumberOnly is a lightweight header type that only contains the +// block number field. It is useful in contexts where the full Ethereum +// block header is not needed, and only the block number is required. +type HeaderNumberOnly struct { + Number *hexutil.Big `json:"number" gencodec:"required"` +} + var _ frontend.SyncBackend = (*SyncTester)(nil) var _ frontend.EngineBackend = (*SyncTester)(nil) var _ frontend.EthBackend = (*SyncTester)(nil) @@ -51,14 +58,23 @@ func SyncTesterFromConfig(logger log.Logger, m metrics.Metricer, stID sttypes.Sy if err != nil { return nil, fmt.Errorf("failed to dial EL client: %w", err) } + elReader := NewELReader(elClient) + return NewSyncTester(logger, m, stID, stCfg.ChainID, elReader), nil +} + +func NewSyncTester(logger log.Logger, m metrics.Metricer, stID sttypes.SyncTesterID, chainID eth.ChainID, elReader ReadOnlyELBackend) *SyncTester { return &SyncTester{ log: logger, m: m, id: stID, - chainID: stCfg.ChainID, - elClient: elClient, + chainID: chainID, + elReader: elReader, sessions: make(map[string]*Session), - }, nil + } +} + +func (s *SyncTester) storeSession(session *Session) { + s.sessions[session.SessionID] = session } func (s *SyncTester) fetchSession(ctx context.Context) (*Session, error) { @@ -70,11 +86,12 @@ func (s *SyncTester) fetchSession(ctx context.Context) (*Session, error) { defer s.mu.Unlock() if existing, ok := s.sessions[session.SessionID]; ok { s.log.Info("Using existing session", "session", existing) + return existing, nil } else { - s.sessions[session.SessionID] = session + s.storeSession(session) s.log.Info("Initialized new session", "session", session) + return session, nil } - return session, nil } func (s *SyncTester) GetSession(ctx context.Context) error { @@ -103,61 +120,107 @@ func (s *SyncTester) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.Blo if err != nil { return nil, err } - - receipts, err := s.elClient.BlockReceipts(ctx, blockNrOrHash) - if err != nil { - return nil, err + number, isNumber := blockNrOrHash.Number() + var receipts []*types.Receipt + if !isNumber { + // hash + receipts, err = s.elReader.GetBlockReceipts(ctx, blockNrOrHash) + if err != nil { + return nil, err + } + } else { + var target uint64 + if target, err = s.checkBlockNumber(number, session); err != nil { + return nil, err + } + receipts, err = s.elReader.GetBlockReceipts(ctx, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(target))) + if err != nil { + return nil, err + } } - if len(receipts) == 0 { + // Should never happen since every block except genesis has at least one deposit tx return nil, ErrNoReceipts } - - if receipts[0].BlockNumber.Uint64() > session.Latest { + if receipts[0].BlockNumber.Uint64() > session.CurrentState.Latest { return nil, ethereum.NotFound } - return receipts, nil } -func (s *SyncTester) GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { +func (s *SyncTester) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) { session, err := s.fetchSession(ctx) if err != nil { return nil, err } - - block, err := s.elClient.BlockByHash(ctx, hash) - if err != nil { + var raw json.RawMessage + if raw, err = s.elReader.GetBlockByHashJSON(ctx, hash, fullTx); err != nil { return nil, err } - - if block.NumberU64() > session.Latest { + var header HeaderNumberOnly + if err := json.Unmarshal(raw, &header); err != nil { + return nil, err + } + if header.Number.ToInt().Uint64() > session.CurrentState.Latest { return nil, ethereum.NotFound } + return raw, nil +} - return block, nil +func (s *SyncTester) checkBlockNumber(number rpc.BlockNumber, session *Session) (uint64, error) { + var target uint64 + switch number { + case rpc.LatestBlockNumber: + target = session.CurrentState.Latest + case rpc.SafeBlockNumber: + target = session.CurrentState.Safe + case rpc.FinalizedBlockNumber: + target = session.CurrentState.Finalized + case rpc.PendingBlockNumber, rpc.EarliestBlockNumber: + // pending, earliest block label not supported + return 0, ethereum.NotFound + default: + if number.Int64() < 0 { + // safety guard for overflow + return 0, ethereum.NotFound + } + target = uint64(number.Int64()) + // Short circuit for numeric request beyond sync tester canonical head + if target > session.CurrentState.Latest { + return 0, ethereum.NotFound + } + } + return target, nil } -func (s *SyncTester) GetBlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { +func (s *SyncTester) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) { session, err := s.fetchSession(ctx) if err != nil { return nil, err } - - if number.Uint64() > session.Latest { - return nil, ethereum.NotFound + var target uint64 + if target, err = s.checkBlockNumber(number, session); err != nil { + return nil, err } - - return s.elClient.BlockByNumber(ctx, number) + var raw json.RawMessage + if raw, err = s.elReader.GetBlockByNumberJSON(ctx, rpc.BlockNumber(target), fullTx); err != nil { + return nil, err + } + return raw, nil } -func (s *SyncTester) ChainId(ctx context.Context) (eth.ChainID, error) { - _, err := s.fetchSession(ctx) +func (s *SyncTester) ChainId(ctx context.Context) (hexutil.Big, error) { + if _, err := s.fetchSession(ctx); err != nil { + return hexutil.Big{}, err + } + chainID, err := s.elReader.ChainId(ctx) if err != nil { - return eth.ChainID(uint256.Int{}), err + return hexutil.Big{}, err } - - return s.chainID, nil + if chainID.ToInt().Cmp(s.chainID.ToBig()) != 0 { + return hexutil.Big{}, fmt.Errorf("chainID mismatch: config: %s, backend: %s", s.chainID, chainID.ToInt()) + } + return hexutil.Big(*s.chainID.ToBig()), nil } func (s *SyncTester) GetPayloadV1(ctx context.Context, payloadID eth.PayloadID) (*eth.ExecutionPayload, error) { diff --git a/op-sync-tester/synctester/backend/sync_tester_test.go b/op-sync-tester/synctester/backend/sync_tester_test.go new file mode 100644 index 0000000000000..7aff7f169cf6c --- /dev/null +++ b/op-sync-tester/synctester/backend/sync_tester_test.go @@ -0,0 +1,479 @@ +package backend + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" + sttypes "github.com/ethereum-optimism/optimism/op-sync-tester/synctester/backend/types" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +var _ ReadOnlyELBackend = (*MockELReader)(nil) + +type MockELReader struct { + ChainID hexutil.Big + + BlocksByHash map[common.Hash]*json.RawMessage + BlocksByNumber map[rpc.BlockNumber]*json.RawMessage + + ReceiptsByHash map[common.Hash][]*types.Receipt + ReceiptsByNumber map[rpc.BlockNumber][]*types.Receipt + + Latest *json.RawMessage + Safe *json.RawMessage + Finalized *json.RawMessage +} + +func NewMockELReader(chainID eth.ChainID) *MockELReader { + return &MockELReader{ + ChainID: hexutil.Big(*chainID.ToBig()), + BlocksByHash: make(map[common.Hash]*json.RawMessage), + BlocksByNumber: make(map[rpc.BlockNumber]*json.RawMessage), + ReceiptsByHash: make(map[common.Hash][]*types.Receipt), + ReceiptsByNumber: make(map[rpc.BlockNumber][]*types.Receipt), + } +} + +func (m *MockELReader) ChainId(ctx context.Context) (hexutil.Big, error) { + return m.ChainID, nil +} + +func (m *MockELReader) GetBlockByNumberJSON(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) { + raw, ok := m.BlocksByNumber[number] + if !ok { + return nil, ethereum.NotFound + } + return *raw, nil +} + +func (m *MockELReader) GetBlockByHashJSON(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) { + raw, ok := m.BlocksByHash[hash] + if !ok { + return nil, ethereum.NotFound + } + return *raw, nil +} + +func (m *MockELReader) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + return nil, nil +} + +func (m *MockELReader) GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return nil, nil +} + +func (m *MockELReader) GetBlockReceipts(ctx context.Context, bnh rpc.BlockNumberOrHash) ([]*types.Receipt, error) { + hash, isHash := bnh.Hash() + if isHash { + receipts, ok := m.ReceiptsByHash[hash] + if !ok { + return nil, ethereum.NotFound + } + return receipts, nil + } + number, isNumber := bnh.Number() + if !isNumber { + // bnh is not a number and not a hash so return not found + return nil, ethereum.NotFound + } + receipts, ok := m.ReceiptsByNumber[number] + if !ok { + return nil, ethereum.NotFound + } + return receipts, nil +} + +func initTestSyncTester(t *testing.T, chainID eth.ChainID, elReader ReadOnlyELBackend) *SyncTester { + syncTester := NewSyncTester(testlog.Logger(t, log.LevelInfo), nil, sttypes.SyncTesterID("test"), chainID, elReader) + return syncTester +} + +func TestSyncTester_ChainId(t *testing.T) { + dummySession := &Session{SessionID: uuid.New().String()} + tests := []struct { + name string + cfgID eth.ChainID + elID eth.ChainID + session *Session + wantErrContains string + }{ + { + name: "no session", + cfgID: eth.ChainIDFromUInt64(1), + elID: eth.ChainIDFromUInt64(1), + wantErrContains: "no session", + }, + { + name: "happy path", + cfgID: eth.ChainIDFromUInt64(11155111), + elID: eth.ChainIDFromUInt64(11155111), + session: dummySession, + }, + { + name: "mismatch", + cfgID: eth.ChainIDFromUInt64(1), + elID: eth.ChainIDFromUInt64(11155111), + session: dummySession, + wantErrContains: "chainID mismatch", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mock := NewMockELReader(tc.elID) + st := initTestSyncTester(t, tc.cfgID, mock) + ctx := context.Background() + if tc.session != nil { + ctx = WithSession(ctx, tc.session) + } + got, err := st.ChainId(ctx) + if tc.wantErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrContains) + return + } + require.NoError(t, err) + require.Equal(t, hexutil.Big(*tc.cfgID.ToBig()), got) + }) + } +} + +func makeBlockRaw(num uint64) *json.RawMessage { + raw := json.RawMessage(fmt.Sprintf(`{"number":"0x%x"}`, num)) + return &raw +} + +func TestSyncTester_GetBlockByHash(t *testing.T) { + hash := common.HexToHash("0xdeadbeef") + tests := []struct { + name string + sessionLatest uint64 + rawNumber uint64 // block.number returned by EL + session *Session + wantErrContains string + }{ + { + name: "no session", + sessionLatest: 0, + rawNumber: 0, + session: nil, + wantErrContains: "no session", + }, + { + name: "block number greater than latest", + sessionLatest: 100, + rawNumber: 101, // greater than Latest + session: &Session{SessionID: uuid.New().String(), CurrentState: FCUState{Latest: 100}}, + wantErrContains: "not found", + }, + { + name: "happy path", + sessionLatest: 100, + rawNumber: 99, + session: &Session{SessionID: uuid.New().String(), CurrentState: FCUState{Latest: 100}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + el := NewMockELReader(eth.ChainIDFromUInt64(1)) + block := makeBlockRaw(tc.rawNumber) + el.BlocksByHash[hash] = block + st := initTestSyncTester(t, eth.ChainIDFromUInt64(1), el) + ctx := context.Background() + if tc.session != nil { + ctx = WithSession(ctx, tc.session) + } + raw, err := st.GetBlockByHash(ctx, hash, false) + if tc.wantErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrContains) + return + } + require.NoError(t, err) + require.NotNil(t, raw) + + var header HeaderNumberOnly + require.NoError(t, json.Unmarshal(raw, &header)) + require.EqualValues(t, tc.rawNumber, header.Number.ToInt().Uint64()) + }) + } +} + +func TestSyncTester_GetBlockByNumber(t *testing.T) { + type testCase struct { + name string + session *Session + inNumber rpc.BlockNumber + wantNum uint64 + wantErrContains string + } + + tests := []testCase{ + { + name: "no session", + session: nil, + wantErrContains: "no session", + }, + { + name: "happy path: numeric less than latest", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 95, + Finalized: 90, + }, + }, + inNumber: rpc.BlockNumber(99), + wantNum: 99, + }, + { + name: "happy path: label latest returns latest", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 95, + Finalized: 90, + }, + }, + inNumber: rpc.LatestBlockNumber, + wantNum: 100, + }, + { + name: "happy path: label safe returns safe", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 97, + Finalized: 90, + }, + }, + inNumber: rpc.SafeBlockNumber, + wantNum: 97, + }, + { + name: "happy path: label finalized returns finalized", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 97, + Finalized: 92, + }, + }, + inNumber: rpc.FinalizedBlockNumber, + wantNum: 92, + }, + { + name: "pending returns not found", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + inNumber: rpc.PendingBlockNumber, + wantErrContains: "not found", + }, + { + name: "earliest label returns not found", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + inNumber: rpc.EarliestBlockNumber, + wantErrContains: "not found", + }, + { + name: "numeric greater than latest returns not found", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + inNumber: rpc.BlockNumber(101), + wantErrContains: "not found", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + el := NewMockELReader(eth.ChainIDFromUInt64(1)) + if tc.session != nil { + el.BlocksByNumber[rpc.BlockNumber(tc.session.CurrentState.Latest)] = makeBlockRaw(tc.session.CurrentState.Latest) + el.BlocksByNumber[rpc.BlockNumber(tc.session.CurrentState.Safe)] = makeBlockRaw(tc.session.CurrentState.Safe) + el.BlocksByNumber[rpc.BlockNumber(tc.session.CurrentState.Finalized)] = makeBlockRaw(tc.session.CurrentState.Finalized) + } + el.BlocksByNumber[tc.inNumber] = makeBlockRaw(uint64(tc.inNumber.Int64())) + st := initTestSyncTester(t, eth.ChainIDFromUInt64(1), el) + ctx := context.Background() + if tc.session != nil { + ctx = WithSession(ctx, tc.session) + } + raw, err := st.GetBlockByNumber(ctx, tc.inNumber, false) + if tc.wantErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrContains) + return + } + require.NoError(t, err) + require.NotNil(t, raw) + var header HeaderNumberOnly + require.NoError(t, json.Unmarshal(raw, &header)) + require.EqualValues(t, tc.wantNum, header.Number.ToInt().Uint64()) + }) + } +} + +func TestSyncTester_GetBlockReceipts(t *testing.T) { + makeReceipts := func(n uint64) []*types.Receipt { + r := new(types.Receipt) + r.BlockNumber = new(big.Int).SetUint64(n) + return []*types.Receipt{r} + } + type testCase struct { + name string + session *Session + arg rpc.BlockNumberOrHash + seedFn func(el *MockELReader, s *Session) + wantFirstBN uint64 + wantErrContains string + } + hashGood := common.HexToHash("0xabc1") + hashTooNew := common.HexToHash("0xabc2") + tests := []testCase{ + { + name: "no session", + session: nil, + arg: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + wantErrContains: "no session", + }, + { + name: "happy: via hash, blockNumber less than latest", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 95, + Finalized: 90, + }, + }, + arg: rpc.BlockNumberOrHashWithHash(hashGood, false), + seedFn: func(el *MockELReader, s *Session) { + el.ReceiptsByHash[hashGood] = makeReceipts(s.CurrentState.Latest - 1) + }, + wantFirstBN: 99, + }, + { + name: "bad: via hash, blockNumber >= latest returns not found", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{ + Latest: 100, + Safe: 95, + Finalized: 90, + }, + }, + arg: rpc.BlockNumberOrHashWithHash(hashTooNew, false), + seedFn: func(el *MockELReader, s *Session) { + // strictly greater than Latest so the post-check triggers NotFound + el.ReceiptsByHash[hashTooNew] = makeReceipts(s.CurrentState.Latest + 1) + }, + wantErrContains: "not found", + }, + { + name: "happy: label latest returns latest", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 95, Finalized: 90}, + }, + arg: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + seedFn: func(el *MockELReader, s *Session) { + el.ReceiptsByNumber[rpc.BlockNumber(s.CurrentState.Latest)] = makeReceipts(s.CurrentState.Latest) + }, + wantFirstBN: 100, + }, + { + name: "happy: label safe returns safe", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 90}, + }, + arg: rpc.BlockNumberOrHashWithNumber(rpc.SafeBlockNumber), + seedFn: func(el *MockELReader, s *Session) { + el.ReceiptsByNumber[rpc.BlockNumber(s.CurrentState.Safe)] = makeReceipts(s.CurrentState.Safe) + }, + wantFirstBN: 97, + }, + { + name: "happy: label finalized returns finalized", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + arg: rpc.BlockNumberOrHashWithNumber(rpc.FinalizedBlockNumber), + seedFn: func(el *MockELReader, s *Session) { + el.ReceiptsByNumber[rpc.BlockNumber(s.CurrentState.Finalized)] = makeReceipts(s.CurrentState.Finalized) + }, + wantFirstBN: 92, + }, + { + name: "happy: numeric less than latest", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + arg: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(99)), + seedFn: func(el *MockELReader, _ *Session) { + el.ReceiptsByNumber[rpc.BlockNumber(99)] = makeReceipts(99) + }, + wantFirstBN: 99, + }, + { + name: "bad: numeric greater than latest returns not found", + session: &Session{ + SessionID: uuid.New().String(), + CurrentState: FCUState{Latest: 100, Safe: 97, Finalized: 92}, + }, + arg: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(101)), + wantErrContains: "not found", + // No seeding needed: checkBlockNumber should fail before EL call + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + el := NewMockELReader(eth.ChainIDFromUInt64(1)) + if tc.seedFn != nil && tc.session != nil { + tc.seedFn(el, tc.session) + } + st := initTestSyncTester(t, eth.ChainIDFromUInt64(1), el) + ctx := context.Background() + if tc.session != nil { + ctx = WithSession(ctx, tc.session) + } + recs, err := st.GetBlockReceipts(ctx, tc.arg) + if tc.wantErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrContains) + return + } + require.NoError(t, err) + require.NotNil(t, recs) + require.GreaterOrEqual(t, len(recs), 1) + require.EqualValues(t, tc.wantFirstBN, recs[0].BlockNumber.Uint64()) + }) + } +} diff --git a/op-sync-tester/synctester/frontend/eth.go b/op-sync-tester/synctester/frontend/eth.go index 5258bad9f0fb5..1a9aad490b062 100644 --- a/op-sync-tester/synctester/frontend/eth.go +++ b/op-sync-tester/synctester/frontend/eth.go @@ -2,19 +2,19 @@ package frontend import ( "context" - "math/big" + "encoding/json" - "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" ) type EthBackend interface { - GetBlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) + GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) - ChainId(ctx context.Context) (eth.ChainID, error) + ChainId(ctx context.Context) (hexutil.Big, error) } type EthFrontend struct { @@ -25,18 +25,18 @@ func NewEthFrontend(b EthBackend) *EthFrontend { return &EthFrontend{b: b} } -func (e *EthFrontend) GetBlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return e.b.GetBlockByNumber(ctx, number) +func (e *EthFrontend) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (json.RawMessage, error) { + return e.b.GetBlockByNumber(ctx, number, fullTx) } -func (e *EthFrontend) GetBlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return e.b.GetBlockByHash(ctx, hash) +func (e *EthFrontend) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (json.RawMessage, error) { + return e.b.GetBlockByHash(ctx, hash, fullTx) } func (e *EthFrontend) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) { return e.b.GetBlockReceipts(ctx, blockNrOrHash) } -func (e *EthFrontend) ChainId(ctx context.Context) (eth.ChainID, error) { +func (e *EthFrontend) ChainId(ctx context.Context) (hexutil.Big, error) { return e.b.ChainId(ctx) } diff --git a/op-sync-tester/synctester/middleware.go b/op-sync-tester/synctester/middleware.go index 314baf461a375..21a00124bac2b 100644 --- a/op-sync-tester/synctester/middleware.go +++ b/op-sync-tester/synctester/middleware.go @@ -64,9 +64,16 @@ func parseSession(r *http.Request, log log.Logger) (*http.Request, error) { } session := &backend.Session{ SessionID: sessionID, - Latest: latest, - Safe: safe, - Finalized: finalized, + CurrentState: backend.FCUState{ + Latest: latest, + Safe: safe, + Finalized: finalized, + }, + InitialState: backend.FCUState{ + Latest: latest, + Safe: safe, + Finalized: finalized, + }, } ctx := backend.WithSession(r.Context(), session) // remove uuid path for routing diff --git a/op-sync-tester/synctester/middleware_test.go b/op-sync-tester/synctester/middleware_test.go index f02330b676b6e..60f520896995d 100644 --- a/op-sync-tester/synctester/middleware_test.go +++ b/op-sync-tester/synctester/middleware_test.go @@ -37,9 +37,10 @@ func TestParseSession_Valid(t *testing.T) { require.True(t, ok) require.NotNil(t, session) require.Equal(t, id, session.SessionID) - require.Equal(t, uint64(100), session.Latest) - require.Equal(t, uint64(90), session.Safe) - require.Equal(t, uint64(80), session.Finalized) + require.Equal(t, uint64(100), session.InitialState.Latest) + require.Equal(t, uint64(90), session.InitialState.Safe) + require.Equal(t, uint64(80), session.InitialState.Finalized) + require.Equal(t, session.InitialState, session.CurrentState) require.Equal(t, "/chain/1/synctest", newReq.URL.Path) } @@ -55,9 +56,10 @@ func TestParseSession_DefaultsToZero(t *testing.T) { require.True(t, ok) require.NotNil(t, session) require.Equal(t, id, session.SessionID) - require.Equal(t, uint64(0), session.Latest) - require.Equal(t, uint64(0), session.Safe) - require.Equal(t, uint64(0), session.Finalized) + require.Equal(t, uint64(0), session.InitialState.Latest) + require.Equal(t, uint64(0), session.InitialState.Safe) + require.Equal(t, uint64(0), session.InitialState.Finalized) + require.Equal(t, session.InitialState, session.CurrentState) } func TestParseSession_NoSessionInitialized(t *testing.T) {