Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetching PrivateKey from Environnement and Execute onchain transaction. #55

Merged
merged 2 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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