Skip to content

Commit

Permalink
Fetching PrivateKey from Environnement and Execute onchain transacti…
Browse files Browse the repository at this point in the history
…on. (#55)

* refactor: previous testing + add the privatekey testing.

* feat: add the max body size to make sure no DoS is possible with tests.
  • Loading branch information
Ethnical authored Sep 2, 2024
1 parent 6d99605 commit e7c02ec
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 42 deletions.
108 changes: 91 additions & 17 deletions op-defender/psp_executor/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package psp_executor
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/mux"
Expand All @@ -17,6 +19,22 @@ func (e *SimpleExecutor) FetchAndExecute(d *Defender) {
// Do nothing for now, for mocking purposes
}

// GeneratePrivatekey generates a private key of the given size useful for testing.
func GeneratePrivatekey(size int) string {
// Generate a random byte slice of the specified size
privateKeyBytes := make([]byte, size)
_, err := rand.Read(privateKeyBytes)
if err != nil {
return ""
}

// Convert the byte slice to a hexadecimal string
privateKeyHex := hex.EncodeToString(privateKeyBytes)

// Add the "0x" prefix to the hexadecimal string
return "0x" + privateKeyHex
}

// 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
Expand All @@ -25,8 +43,9 @@ func TestHTTPServerHasOnlyPSPExecutionRoute(t *testing.T) {
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",
NodeURL: mockNodeUrl,
PortAPI: "8080",
privatekeyflag: GeneratePrivatekey(32),
}
// Initialize the Defender with necessary mock or real components
defender, err := NewDefender(context.Background(), logger, metricsfactory, cfg, executor)
Expand All @@ -48,19 +67,19 @@ func TestHTTPServerHasOnlyPSPExecutionRoute(t *testing.T) {
})

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

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

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

if len(methods) != 1 || methods[0] != expectedMethod {
t.Errorf("Expected method %s, but found %v", expectedMethod, methods)
t.Errorf("Expected method %s, but got %v", expectedMethod, methods)
}
} else {
t.Error("No route found")
Expand All @@ -75,8 +94,9 @@ func TestDefenderInitialization(t *testing.T) {
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",
NodeURL: mockNodeUrl,
PortAPI: "8080",
privatekeyflag: GeneratePrivatekey(32),
}
// Initialize the Defender with necessary mock or real components
_, err := NewDefender(context.Background(), logger, metricsfactory, cfg, executor)
Expand All @@ -96,15 +116,21 @@ func TestHandlePostMockFetch(t *testing.T) {
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",
NodeURL: mockNodeUrl,
PortAPI: "8080",
privatekeyflag: GeneratePrivatekey(32),
}

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

// Create a large request body (> 1MB)
largeBody := make([]byte, 1048577) // 1MB + 1 byte
for i := range largeBody {
largeBody[i] = 'a'
}
// Define test cases
tests := []struct {
name string
Expand All @@ -115,25 +141,37 @@ func TestHandlePostMockFetch(t *testing.T) {
{
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"}`,
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":}`,
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}`,
body: `{"Timestamp":1596240000}`,
expectedStatus: http.StatusBadRequest,
},
{
path: "/api/psp_execution",
name: "Too Many Fields", // Check if there are extra fields present and return the 400 status code.
body: `{"Pause":true,"Timestamp":1596240000,"Operator":"0x123", "extra":"unnecessary_value"}`,
expectedStatus: http.StatusBadRequest,
},
{
path: "/api/psp_execution",
name: "Payload Size Greater Than Limit", // Check if the path is incorrect return the 404 status code.
body: `{"Pause":true,"Timestamp":1596240000,"Operator":"` + string(largeBody) + `"}`,
expectedStatus: http.StatusRequestEntityTooLarge,
},
{
path: "/api/",
name: "Incorrect Path Fields", // Check if the path is incorrect return the 404 status code.
body: `{"pause":true,"timestamp":1596240000,"operator":"0x123"}`,
body: `{"Pause":true,"Timestamp":1596240000,"Operator":"0x123"}`,
expectedStatus: http.StatusNotFound,
},
}
Expand All @@ -152,8 +190,8 @@ func TestHandlePostMockFetch(t *testing.T) {
muxrouter.ServeHTTP(recorder, req)

if status := recorder.Code; status != tc.expectedStatus {
t.Errorf("handler \"%s\" returned wrong status code: got %v want %v",
tc.name, status, tc.expectedStatus)
t.Errorf("handler \"%s\" returned wrong status code: Expected %v but got %v",
tc.name, tc.expectedStatus, status)
}
})
}
Expand All @@ -175,11 +213,47 @@ func TestCheckAndReturnRPC(t *testing.T) {
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)
t.Errorf("Test: \"%s\" Expected error = %v, but got %v", tt.name, tt.expectErr, err)

return
}
if !tt.expectErr && client == nil {
t.Errorf("CheckAndReturnRPC() returned nil client for valid URL")
t.Errorf("Test: \"%s\" Expected no error but got \"client=<nil>\"", tt.name)
}
})
}
}
func TestCheckAndReturnPrivateKey(t *testing.T) {
validPrivateKeyGenerated := GeneratePrivatekey(32)
tests := []struct {
name string
input string
expected string
expectError bool
}{
{"Valid private key", "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", false},
{"Valid private key without 0x", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", false},
{"Valid private key Generated", validPrivateKeyGenerated, validPrivateKeyGenerated[2:], false},
{"Empty string", "", "", true},
{"Invalid hex string", "0xInvalidHex", "", true},
{"Incorrect length", "0x1234", "", true},
{"Invalid private key", "0x0000000000000000000000000000000000000000000000000000000000000000", "", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := CheckAndReturnPrivateKey(tt.input)
if tt.expectError {
if err == nil {
t.Errorf("Test: \"%s\" Expected an error, but got no error", tt.name)
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("Test: \"%s\" Expected %s, but got %s", tt.name, tt.expected, result)
}
}
})
}
Expand Down
116 changes: 91 additions & 25 deletions op-defender/psp_executor/defender.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package psp_executor
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
Expand Down Expand Up @@ -55,6 +56,7 @@ type Defender struct {
l1Client *ethclient.Client
router *mux.Router
executor Executor
privatekey string
// metrics
latestPspNonce *prometheus.GaugeVec
unexpectedRpcErrors *prometheus.CounterVec
Expand All @@ -75,22 +77,58 @@ type RequestData struct {

// handlePost handles POST requests and processes the JSON body
func (d *Defender) handlePost(w http.ResponseWriter, r *http.Request) {
var data RequestData
// Decode the JSON body into a map
r.Body = http.MaxBytesReader(w, r.Body, 1048576)
var requestMap map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&requestMap); err != nil {
if err.Error() == "http: request body too large" {
http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge)
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
}

d.log.Info("Received HTTP request", "method", r.Method, "url", r.URL) // Log the requests for traceability.
// Decode the JSON body into the struct
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
// Check for exactly 3 fields
if len(requestMap) != 3 {
http.Error(w, "Request must contain exactly 3 fields: Pause, Timestamp, and Operator", http.StatusBadRequest)
return
}
//check that all the fields are set
if data.Pause == false || data.Timestamp == 0 || data.Operator == "" {
http.Error(w, "Missing fields in the request", http.StatusBadRequest)

// Check for the presence of required fields and their types
pause, ok := requestMap["Pause"].(bool)
if !ok {
http.Error(w, "Pause field must be a boolean", http.StatusBadRequest)
return
}

timestamp, ok := requestMap["Timestamp"].(float64)
if !ok {
http.Error(w, "Timestamp field must be a number", http.StatusBadRequest)
return
}

operator, ok := requestMap["Operator"].(string)
if !ok {
http.Error(w, "Operator field must be a string", http.StatusBadRequest)
return
}

// Create the RequestData struct with the validated fields
data := RequestData{
Pause: pause,
Timestamp: int64(timestamp),
Operator: operator,
}

// Check that all the fields are set with valid values
if !data.Pause || data.Timestamp == 0 || data.Operator == "" {
http.Error(w, "Missing or invalid fields in the request", http.StatusBadRequest)
return
}

// Execute the PSP on the chain.
d.executor.FetchAndExecute(d) //TODO: Make sure, a malformed HTTP request can't arrived here.
// Execute the PSP on the chain by calling the FetchAndExecute method of the executor.
d.executor.FetchAndExecute(d)
return
}

Expand All @@ -101,19 +139,28 @@ func NewDefender(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLI
if err != nil {
return nil, fmt.Errorf("failed to fetch l1 RPC: %w", err)
}
privatekey, err := CheckAndReturnPrivateKey(cfg.privatekeyflag)
if err != nil {
return nil, fmt.Errorf("failed to return the privatekey: %w", err)
}

if cfg.PortAPI == "" {
return nil, fmt.Errorf("port.api is not set.")
}

defender := &Defender{
log: log,
l1Client: l1client,
port: cfg.PortAPI,
executor: executor,
log: log,
l1Client: l1client,
port: cfg.PortAPI,
executor: executor,
privatekey: privatekey,
}

defender.router = mux.NewRouter()
defender.router.HandleFunc("/api/psp_execution", defender.handlePost).Methods("POST")
defender.router.HandleFunc("/api/psp_execution", func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // Limit payload to 1MB
defender.handlePost(w, r)
}).Methods("POST")
defender.log.Info("Starting HTTP JSON API PSP Execution server...", "port", defender.port)
return defender, nil
}
Expand All @@ -123,18 +170,12 @@ func NewDefender(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLI
// In the future, the function will fetch the PSPs from a secret file and execute it onchain through a EVM transaction.
func (e *DefenderExecutor) FetchAndExecute(d *Defender) {
ctx := context.Background()
privateKey, err := FetchPrivateKeyInGcp()
if err != nil {
d.log.Crit("Failed to fetch the private key from GCP", "error", err)
return
}

configAddress, safeAddress, data, err := FetchPSPInGCP()
if err != nil {
d.log.Crit("Failed to fetch PSP data from GCP", "error", err)
return
}
PspExecutionOnChain(ctx, d.l1Client, configAddress, privateKey, safeAddress, data)
PspExecutionOnChain(ctx, d.l1Client, configAddress, d.privatekey, safeAddress, data)
}

// CheckAndReturnRPC() will return the L1 client based on the RPC provided in the config and ensure that the RPC is not production one.
Expand All @@ -154,9 +195,34 @@ func CheckAndReturnRPC(rpc_url string) (*ethclient.Client, error) {
return client, nil
}

// FetchPrivateKey() will fetch the privatekey of the account that will execute the pause (from the GCP secret manager).
func FetchPrivateKeyInGcp() (string, error) {
return "2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6", nil // Mock with a well-known private key from "test test ... test junk" derivation (9).
// CheckAndReturnPrivateKey() will return the privatekey only if the privatekey is a valid one otherwise return an error.
func CheckAndReturnPrivateKey(privateKeyStr string) (string, error) {
// Remove "0x" prefix if present
privateKeyStr = strings.TrimPrefix(privateKeyStr, "0x")

// Check if the private key is a valid hex string
if !isValidHexString(privateKeyStr) {
return "", fmt.Errorf("invalid private key: not a valid hex string")
}

// Check if the private key has the correct length (32 bytes = 64 hex characters)
if len(privateKeyStr) != 64 {
return "", fmt.Errorf("invalid private key: incorrect length")
}

// Attempt to parse the private key
_, err := crypto.HexToECDSA(privateKeyStr)
if err != nil {
return "", fmt.Errorf("invalid private key: %v", err)
}

return privateKeyStr, nil
}

// isValidHexString checks if a string is a valid hexadecimal string
func isValidHexString(s string) bool {
_, err := hex.DecodeString(s)
return err == nil
}

// FetchPSPInGCP() will fetch the correct PSPs into GCP and return the Data.
Expand Down

0 comments on commit e7c02ec

Please sign in to comment.