diff --git a/op-conductor/conductor/execution_miner_proxy_test.go b/op-conductor/conductor/execution_miner_proxy_test.go new file mode 100644 index 00000000000..f580d797a49 --- /dev/null +++ b/op-conductor/conductor/execution_miner_proxy_test.go @@ -0,0 +1,109 @@ +package conductor + +import ( + "context" + "fmt" + "log/slog" + "testing" + + clientmocks "github.com/ethereum-optimism/optimism/op-conductor/client/mocks" + consensusmocks "github.com/ethereum-optimism/optimism/op-conductor/consensus/mocks" + healthmocks "github.com/ethereum-optimism/optimism/op-conductor/health/mocks" + "github.com/ethereum-optimism/optimism/op-conductor/metrics" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils/mockrpc" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" +) + +// TestSetMaxDASize tests the SetMaxDASize method of the ExecutionMinerProxyBackend +// It ensures that the proxy is transparently proxying the call to the execution engine +func TestSetMaxDASize(t *testing.T) { + t.Run("compliant sequencer", func(t *testing.T) { + testSetMaxDASize(t, true, false) + }) + t.Run("non-compliant sequencer", func(t *testing.T) { + testSetMaxDASize(t, false, false) + }) + t.Run("sequencer down", func(t *testing.T) { + testSetMaxDASize(t, true, true) + }) +} + +func testSetMaxDASize(t *testing.T, compliantSequencer bool, sequencerDown bool) { + ctx := context.Background() + var expectationsFile string + if compliantSequencer { + expectationsFile = "testdata/compliant-sequencer.json" + } else { + expectationsFile = "testdata/non-compliant-sequencer.json" + } + + sequencer := mockrpc.NewMockRPC(t, testlog.Logger(t, slog.LevelDebug), mockrpc.WithExpectationsFile(t, expectationsFile)) + endpoint := sequencer.Endpoint() + + config := mockConfig(t) + config.ExecutionRPC = endpoint + config.NodeRPC = endpoint // this won't be used but needs to be set to get the conductor to init properly + config.RPCEnableProxy = true + config.RPC.ListenAddr = "localhost" + config.RPC.ListenPort = 0 // Let the system pick a random port, which we will inspect later + + logger, logs := testlog.CaptureLogger(t, slog.LevelDebug) + + conductor, err := NewOpConductor( + ctx, + &config, + logger, + &metrics.NoopMetricsImpl{}, + "test-version", + &clientmocks.SequencerControl{}, // not used in this test + &consensusmocks.Consensus{}, // not used in this test + &healthmocks.HealthMonitor{}, // not used in this test + ) + require.NoError(t, err) + + // Start the RPC server part of the conductor + err = conductor.rpcServer.Start() + require.NoError(t, err) + defer func() { _ = conductor.rpcServer.Stop() }() + + port, err := conductor.rpcServer.Port() + require.NoError(t, err) + t.Log("RPC server listening on port:", port) + + url := fmt.Sprintf("http://localhost:%d", port) + + rpcClient, err := rpc.Dial(url) + require.NoError(t, err) + defer rpcClient.Close() + + if sequencerDown { + require.NoError(t, sequencer.Close()) + } + + var result bool + err = rpcClient.CallContext(ctx, &result, "miner_setMaxDASize", "0x1", "0x2") + + if sequencerDown { + require.Error(t, err) + expectedLog := "proxy miner_setMaxDASize call failed" + r := logs.FindLog(testlog.NewMessageContainsFilter(expectedLog)) + require.NotNil(t, r, "could not find log message containing '%s'", expectedLog) + return + } + + if compliantSequencer { + require.NoError(t, err) + require.True(t, result) + expectedLog := "successfully proxied miner_setMaxDASize call" + r := logs.FindLog(testlog.NewMessageContainsFilter(expectedLog)) + require.NotNil(t, r, "could not find log message containing '%s'", expectedLog) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "Method not found") + expectedLog := "proxy miner_setMaxDASize call returned an RPC error" + r := logs.FindLog(testlog.NewMessageContainsFilter(expectedLog)) + require.NotNil(t, r, "could not find log message containing '%s'", expectedLog) + } +} diff --git a/op-conductor/conductor/testdata/compliant-sequencer.json b/op-conductor/conductor/testdata/compliant-sequencer.json new file mode 100644 index 00000000000..702f0a91a16 --- /dev/null +++ b/op-conductor/conductor/testdata/compliant-sequencer.json @@ -0,0 +1,10 @@ +[ + { + "method": "miner_setMaxDASize", + "params": [ + "0x1", + "0x2" + ], + "result": true + } +] diff --git a/op-conductor/conductor/testdata/non-compliant-sequencer.json b/op-conductor/conductor/testdata/non-compliant-sequencer.json new file mode 100644 index 00000000000..2087b9864f5 --- /dev/null +++ b/op-conductor/conductor/testdata/non-compliant-sequencer.json @@ -0,0 +1,11 @@ +[ + { + "method": "miner_setMaxDASize", + "params": [ + "0x1", + "0x2" + ], + "err": "Method not found", + "errCode": -32601 + } +] diff --git a/op-conductor/rpc/api.go b/op-conductor/rpc/api.go index a8b62e3b74f..13ce455b933 100644 --- a/op-conductor/rpc/api.go +++ b/op-conductor/rpc/api.go @@ -71,7 +71,7 @@ type ExecutionProxyAPI interface { // ExecutionMinerProxyAPI defines the methods proxied to the execution 'miner_' rpc backend // This should include all methods that are called by op-batcher or op-proposer type ExecutionMinerProxyAPI interface { - SetMaxDASize(ctx context.Context, maxTxSize hexutil.Big, maxBlockSize hexutil.Big) bool + SetMaxDASize(ctx context.Context, maxTxSize hexutil.Big, maxBlockSize hexutil.Big) (bool, error) } // NodeProxyAPI defines the methods proxied to the node 'optimism_' rpc backend diff --git a/op-conductor/rpc/excecution_miner_proxy.go b/op-conductor/rpc/excecution_miner_proxy.go index 7413310b281..201480a2272 100644 --- a/op-conductor/rpc/excecution_miner_proxy.go +++ b/op-conductor/rpc/excecution_miner_proxy.go @@ -2,10 +2,12 @@ package rpc import ( "context" + "errors" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" ) var ExecutionMinerRPCNamespace = "miner" @@ -27,12 +29,30 @@ func NewExecutionMinerProxyBackend(log log.Logger, con conductor, client *ethcli } } -func (api *ExecutionMinerProxyBackend) SetMaxDASize(ctx context.Context, maxTxSize hexutil.Big, maxBlockSize hexutil.Big) bool { +func (api *ExecutionMinerProxyBackend) SetMaxDASize(ctx context.Context, maxTxSize hexutil.Big, maxBlockSize hexutil.Big) (bool, error) { var result bool err := api.client.Client().Call(&result, "miner_setMaxDASize", maxTxSize, maxBlockSize) if err != nil { - api.log.Warn("proxy miner_setMaxDASize call failed", "err", err) - return false + var rpcErr rpc.Error + switch { + case errors.As(err, &rpcErr): + api.log.Debug("proxy miner_setMaxDASize call returned an RPC error", + "err", err, + "maxTxSize", maxTxSize, + "maxBlockSize", maxBlockSize, + "method", "miner_setMaxDASize") + default: + api.log.Warn("proxy miner_setMaxDASize call failed", + "err", err, + "maxTxSize", maxTxSize, + "maxBlockSize", maxBlockSize, + "method", "miner_setMaxDASize") + } + return false, err } - return result + api.log.Debug("successfully proxied miner_setMaxDASize call", + "maxTxSize", maxTxSize, + "maxBlockSize", maxBlockSize, + "result", result) + return result, nil } diff --git a/op-service/testutils/mockrpc/mockrpc.go b/op-service/testutils/mockrpc/mockrpc.go index 3304de8c1d6..a1921386009 100644 --- a/op-service/testutils/mockrpc/mockrpc.go +++ b/op-service/testutils/mockrpc/mockrpc.go @@ -73,6 +73,8 @@ type MockRPC struct { lis net.Listener err error + + srv *http.Server } type Option func(*MockRPC) @@ -114,6 +116,8 @@ func NewMockRPC(t *testing.T, lgr log.Logger, opts ...Option) *MockRPC { Handler: m, } + m.srv = srv + errCh := make(chan error, 1) go func() { err := srv.Serve(m.lis) @@ -228,6 +232,10 @@ func (m *MockRPC) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (m *MockRPC) Close() error { + return m.srv.Shutdown(context.Background()) +} + func (m *MockRPC) Endpoint() string { return fmt.Sprintf("http://%s", m.lis.Addr().String()) }