Skip to content

Commit

Permalink
Add a onchain execution on forked network with Hardcoded values. (#54)
Browse files Browse the repository at this point in the history
* remove the authentication and execute a hardcoded transaction when cURL is received.

* refactor: functions to add them as module of the `defender` struct.

* refactor: clean code.

* feat: executor to mock object during testing

* refactor: Previous test are now working with the new executor mocking logic.

* gitignore: ignore the `run.sh` that is used with `.air.toml`.

* refactor: and the clean the code

* feat: add the unit tests `TestHTTPServerHasOnlyPSPExecutionRoute` and `TestDefenderInitialization` and `TestHandlePostMockFetch`

* docs: update the docs of the current functions.

* docs: update the docs with necessaries fields

* refactor: clean up the codes + remove the `calldata` fields that is now useless from the API

* docs: fix docs with typos

* remove the calldata from the tests

* docs: improve the readme with the tab and the field used for the each fields.

* docs: improve the docs

* chore: fix alignement

* Update op-defender/psp_executor/defender.go

Co-authored-by: Kevin Z Chen <kevin.zchn@gmail.com>

* Update op-defender/psp_executor/api_test.go

Co-authored-by: Kevin Z Chen <kevin.zchn@gmail.com>

* refactor: replace `expectedErr` to `expectErr`.

---------

Co-authored-by: Kevin Z Chen <kevin.zchn@gmail.com>
  • Loading branch information
Ethnical and zchn authored Aug 30, 2024
1 parent acb6609 commit 6d99605
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 119 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ crytic-export

bin
op-defender/.air.toml
op-defender/run.sh
4 changes: 3 additions & 1 deletion op-defender/cmd/defender/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"

executor "github.com/ethereum-optimism/monitorism/op-defender/psp_executor"
"github.com/ethereum/go-ethereum/params"

"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -57,7 +58,8 @@ func PSPExecutorMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp
}

metricsRegistry := opmetrics.NewRegistry()
defender_thread, err := psp_executor.NewDefender(ctx.Context, log, opmetrics.With(metricsRegistry), cfg)
executor := &executor.DefenderExecutor{}
defender_thread, err := psp_executor.NewDefender(ctx.Context, log, opmetrics.With(metricsRegistry), cfg, executor)
if err != nil {
return nil, fmt.Errorf("Failed to create psp_executor HTTP API service: %w", err)
}
Expand Down
71 changes: 47 additions & 24 deletions op-defender/psp_executor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,60 @@

The PSP Executor service is a service designed to execute PSP onchain faster to increase our readiness and speed in case of incident response.

The service is design to listen on a port and execute a PSP onchain when a request is received.
The service is designed to listen on a port and execute a PSP onchain when a request is received.

⚠️ The service as to use a authentification method before calling this API ⚠️
⚠️ The service has to use an authentication method before calling this API ⚠️

### Options and Configuration
## 1. Usage

### 1 .Run HTTP API service

To start the HTTP API service we can use the following oneliner command:
![f112841bad84c59ea3ed1ca380740f5694f553de8755b96b1a40ece4d1c26f81](https://github.com/user-attachments/assets/17235e99-bf25-40a5-af2c-a0d9990c6276)
Settings of the HTTP API service:

| Port | API Path | HTTP Method |
| ----------------------------- | -------------------- | ----------- |
| 8080 (Default can be changed) | `/api/psp_execution` | POST |

```shell
go run ../cmd/defender psp_executor --privatekey XXXXXX --receiver.address 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF --rpc.url http://localhost:8545 --port.api 8080
```

### 2. Request the HTTP API

To use the HTTP API you can use the following `curl` command with the following fields:

![image](https://github.com/user-attachments/assets/3edc2ee5-6dfd-4872-9bc6-e3ead7444a96)

```bash
curl -X POST http://${HTTP_API_PSP}:${PORT}/api/psp_execution \-H "Content-Type: application/json" \-d '{"pause": true, "timestamp": 1719432011, "operator": "Tom"}'
```

Explanation of the _mandatory_ fields:
| Field | Description |
| --------- | -------------------------------------------------------------------------------- |
| pause | A boolean value indicating whether to pause (true) or unpause (false) the SuperchainConfig.|
| timestamp | The Unix timestamp when the request is made. |
| operator | The name or identifier of the person initiating the PSP execution. |

### 3. Metrics Server

To monitor the _PSPexecutor service_ the metrics server can be enabled. The metrics server will expose the metrics on the specified address and port.
The metrics are using **Prometheus** and can be set with the following options:
| Option | Description | Default Value | Environment Variable |
| ------------------- | ------------------------- | ------------- | ----------------------------- |
| `--metrics.enabled` | Enable the metrics server | `false` | `$MONITORISM_METRICS_ENABLED` |
| `--metrics.addr` | Metrics listening address | `"0.0.0.0"` | `$MONITORISM_METRICS_ADDR` |
| `--metrics.port` | Metrics listening port | `7300` | `$MONITORISM_METRICS_PORT` |

### 4. Options and Configuration

The current options are the following:

```
OPTIONS:
--rpc-url value Node URL of a peer (default: "http://127.0.0.1:8545") [$PSPEXECUTOR_MON_NODE_URL]
--rpc.url value Node URL of a peer (default: "http://127.0.0.1:8545") [$PSPEXECUTOR_MON_NODE_URL]
--privatekey value Private key of the account that will issue the pause () [$PSPEXECUTOR_MON_PRIVATE_KEY]
--receiver.address value The receiver address of the pause request. [$PSPEXECUTOR_MON_RECEIVER_ADDRESS]
--port.api value Port of the API server you want to listen on (e.g. 8080). (default: "8080") [$PSPEXECUTOR_MON_PORT_API]
Expand All @@ -26,23 +69,3 @@ OPTIONS:
--loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
--help, -h show help
```

## Usage
### HTTP API service
To start the HTTP API service we can use the following oneliner command:
![f112841bad84c59ea3ed1ca380740f5694f553de8755b96b1a40ece4d1c26f81](https://github.com/user-attachments/assets/17235e99-bf25-40a5-af2c-a0d9990c6276)

```shell
go run ../cmd/defender psp_executor --privatekey XXXXXX --receiver.address 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF --rpc.url http://localhost:8545 --port.api 8080
```

### cURL HTTP API

To use the HTTP API you can use the following `curl` command:

![image](https://github.com/user-attachments/assets/3edc2ee5-6dfd-4872-9bc6-e3ead7444a96)

```bash
curl -X POST http://${HTTP_API_PSP}:${PORT}/api/psp_execution \-H "Content-Type: application/json" \-d '{"pause": true, "timestamp": 1719432011, "operator": "Tom"}'
```

191 changes: 159 additions & 32 deletions op-defender/psp_executor/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,184 @@ package psp_executor

import (
"bytes"
"encoding/json"
"context"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/mux"
"net/http"
"net/http/httptest"
"testing"
)

// TestHandlePost tests the handlePost function for various scenarios.
func TestHandlePost(t *testing.T) {
type SimpleExecutor struct{}

func (e *SimpleExecutor) FetchAndExecute(d *Defender) {
// Do nothing for now, for mocking purposes
}

// TestHTTPServerHasOnlyPSPExecutionRoute tests if the HTTP server has only one route with "/api/psp_execution" path and "POST" method.
func TestHTTPServerHasOnlyPSPExecutionRoute(t *testing.T) {
// Mock dependencies or create real ones depending on your test needs
logger := log.New() //@TODO: replace with testlog https://github.com/ethereum-optimism/optimism/blob/develop/op-service/testlog/testlog.go#L61
executor := &SimpleExecutor{}
metricsfactory := opmetrics.With(opmetrics.NewRegistry())
mockNodeUrl := "http://rpc.tenderly.co/fork/" // Need to have the "fork" in the URL to avoid mistake for now.
cfg := CLIConfig{
NodeURL: mockNodeUrl,
PortAPI: "8080",
}
// Initialize the Defender with necessary mock or real components
defender, err := NewDefender(context.Background(), logger, metricsfactory, cfg, executor)

if err != nil {
t.Fatalf("Failed to create Defender: %v", err)
}

// We Check if the router has only one route
routeCount := 0
expectedPath := "/api/psp_execution"
expectedMethod := "POST"
var foundRoute *mux.Route

defender.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
routeCount++
foundRoute = route
return nil
})

if routeCount != 1 {
t.Errorf("Expected 1 route, but found %d", routeCount)
}

if foundRoute != nil {
path, _ := foundRoute.GetPathTemplate()
methods, _ := foundRoute.GetMethods()

if path != expectedPath {
t.Errorf("Expected path %s, but found %s", expectedPath, path)
}

if len(methods) != 1 || methods[0] != expectedMethod {
t.Errorf("Expected method %s, but found %v", expectedMethod, methods)
}
} else {
t.Error("No route found")
}
}

// TestDefenderInitialization tests the initialization of the Defender struct with mock dependencies.
func TestDefenderInitialization(t *testing.T) {
// Mock dependencies or create real ones depending on your test needs
logger := log.New() //@TODO: replace with testlog https://github.com/ethereum-optimism/optimism/blob/develop/op-service/testlog/testlog.go#L61
executor := &SimpleExecutor{}
metricsfactory := opmetrics.With(opmetrics.NewRegistry())
mockNodeUrl := "http://rpc.tenderly.co/fork/" // Need to have the "fork" in the URL to avoid mistake for now.
cfg := CLIConfig{
NodeURL: mockNodeUrl,
PortAPI: "8080",
}
// Initialize the Defender with necessary mock or real components
_, err := NewDefender(context.Background(), logger, metricsfactory, cfg, executor)

if err != nil {
t.Fatalf("Failed to create Defender: %v", err)
}

}

// TestHandlePostMockFetch tests the handlePost function with HTTP status code to make sure HTTP code returned are expected in every possible cases.
func TestHandlePostMockFetch(t *testing.T) {
// Initialize the Defender with necessary mock or real components
logger := log.New() //@TODO: replace with testlog https://github.com/ethereum-optimism/optimism/blob/develop/op-service/testlog/testlog.go#L61
metricsRegistry := opmetrics.NewRegistry()
metricsfactory := opmetrics.With(metricsRegistry)
mockNodeUrl := "http://rpc.tenderly.co/fork/" // Need to have the "fork" in the URL to avoid mistake for now.
executor := &SimpleExecutor{}
cfg := CLIConfig{
NodeURL: mockNodeUrl,
PortAPI: "8080",
}

defender, err := NewDefender(context.Background(), logger, metricsfactory, cfg, executor)
if err != nil {
t.Fatalf("Failed to create Defender: %v", err)
}

// Define test cases
tests := []struct {
name string
body RequestData
body string
expectedStatus int
expectedBody string
path string
}{
{
name: "Network Authentication Required",
body: RequestData{
Pause: false,
Timestamp: 0,
Operator: "",
Calldata: "",
},
expectedStatus: http.StatusNetworkAuthenticationRequired,
expectedBody: "Network Authentication Required\n", //do not forget the newline character.
path: "/api/psp_execution",
name: "Valid Request", // Check if the request is valid as expected return the 200 status code.
body: `{"pause":true,"timestamp":1596240000,"operator":"0x123"}`,
expectedStatus: http.StatusOK,
},
{
path: "/api/psp_execution",
name: "Invalid JSON", // Check if the JSON is invalid return the 400 status code.
body: `{"pause":true, "timestamp":"invalid","operator":}`,
expectedStatus: http.StatusBadRequest,
},
{
path: "/api/psp_execution",
name: "Missing Fields", // Check if the required fields are missing return the 400 status code.
body: `{"timestamp":1596240000}`,
expectedStatus: http.StatusBadRequest,
},
{
path: "/api/",
name: "Incorrect Path Fields", // Check if the path is incorrect return the 404 status code.
body: `{"pause":true,"timestamp":1596240000,"operator":"0x123"}`,
expectedStatus: http.StatusNotFound,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert the body to JSON
jsonBody, _ := json.Marshal(tt.body)
req, err := http.NewRequest("POST", "/api/psp_execution", bytes.NewBuffer(jsonBody))
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest("POST", tc.path, bytes.NewBufferString(tc.body))
if err != nil {
t.Fatal(err)
t.Fatalf("Could not create request: %v", err)
}
recorder := httptest.NewRecorder()

// Create a ResponseRecorder to record the response.
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlePost) // Call the `handlePost` that is the entrypoint of the API.
// Get the servermux of the defender.router to check the routes
muxrouter := defender.router
// Use the mux to serve the request
muxrouter.ServeHTTP(recorder, req)

// Call the handler function
handler.ServeHTTP(rr, req)

// Check the status code
if status := rr.Code; status != tt.expectedStatus {
t.Errorf("handler returned wrong status code: got `%v` want `%v`", status, tt.expectedStatus)
if status := recorder.Code; status != tc.expectedStatus {
t.Errorf("handler \"%s\" returned wrong status code: got %v want %v",
tc.name, status, tc.expectedStatus)
}
})
}
}

// Check the response body
if rr.Body.String() != tt.expectedBody {
t.Errorf("handler returned unexpected body: got `%v` want `%v`", rr.Body.String(), tt.expectedBody)
// TestCheckAndReturnRPC tests that the CheckAndReturnRPC function returns the correct client or error for an incorrect URL provided.
func TestCheckAndReturnRPC(t *testing.T) {
tests := []struct {
name string
rpcURL string
expectErr bool
}{
{"Empty URL", "", true},
{"Production URL", "https://mainnet.infura.io", true},
{"Valid Tenderly Fork URL", "https://rpc.tenderly.co/fork/some-id", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := CheckAndReturnRPC(tt.rpcURL)
if (err != nil) != tt.expectErr {
t.Errorf("CheckAndReturnRPC() error = %v, expectedErr %v", err, tt.expectErr)
return
}
if !tt.expectErr && client == nil {
t.Errorf("CheckAndReturnRPC() returned nil client for valid URL")
}
})
}
Expand Down
17 changes: 8 additions & 9 deletions op-defender/psp_executor/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,31 @@ const (
)

type CLIConfig struct {
NodeUrl string
NodeURL string
privatekeyflag string
portapi string
receiverAddress string
hexString string
PortAPI string
ReceiverAddress string
HexString string
}

func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
cfg := CLIConfig{NodeUrl: ctx.String(NodeURLFlagName)}
cfg := CLIConfig{NodeURL: ctx.String(NodeURLFlagName)}
if len(PrivateKeyFlagName) == 0 {
return cfg, fmt.Errorf("must have a PrivateKeyFlagName set to execute the pause on mainnet")
}
cfg.privatekeyflag = ctx.String(PrivateKeyFlagName)
if len(PortAPIFlagName) == 0 {
return cfg, fmt.Errorf("must have a PortAPIFlagName set to execute the pause on mainnet")
}
cfg.receiverAddress = ctx.String(ReceiverAddressFlagName)
cfg.PortAPI = ctx.String(PortAPIFlagName)
if len(ReceiverAddressFlagName) == 0 {
return cfg, fmt.Errorf("must have a ReceiverAddressFlagName set to receive the pause on mainnet.")
}

cfg.hexString = ctx.String(DataFlagName)
cfg.ReceiverAddress = ctx.String(ReceiverAddressFlagName)
if len(DataFlagName) == 0 {
return cfg, fmt.Errorf("must have a `data` set to execute the calldata on mainnet.")
}
cfg.portapi = ctx.String(PortAPIFlagName)
cfg.HexString = ctx.String(DataFlagName)
return cfg, nil
}

Expand Down
Loading

0 comments on commit 6d99605

Please sign in to comment.