diff --git a/README.md b/README.md index 2faecdb..91d6f02 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ GNOSIS_SEND_INTERVAL="600" NODE_URL="https://erpc.chiado.staging.shutter.network" WAIT_TX_TIMEOUT=10 TEST_DURATION=1 + +#DECRYPTION MONITOR +SHUTTER_API="http://shutter-api.shutter.network/api" +API_REQUEST_INTERVAL="60" +SHUTTER_REGISTRY_CALLER_ADDRESS="0x228DefCF37Da29475F0EE2B9E4dfAeDc3b0746bc" +DEC_KEY_WAIT_INTERVAL="20" ``` - `MODE=chiado`: Sends transactions at intervals defined by `CHIADO_SEND_INTERVAL` to the Chiado URL. @@ -41,6 +47,7 @@ TEST_DURATION=1 - waits for a timeout defined by `WAIT_TX_TIMEOUT` - then sends the next one at nonce `n+ 1` - test is run for the duration defined in `TEST_DURATION` +- `MODE=decryption-monitor`: Sends an HTTP request to register an identity at intervals defined by `API_REQUEST_INTERVAL` on the Shutter API, and monitors the release of relevant decryption keys by requesting them from the Shutter API at intervals defined by `DEC_KEY_WAIT_INTERVAL`. - Multiple tests can be run at the same time by separating the different modes with a comma, i.e. `MODE="chiado,gnosis"`. diff --git a/config/config.go b/config/config.go index b331f3a..06c2b36 100644 --- a/config/config.go +++ b/config/config.go @@ -5,40 +5,49 @@ import ( "os" "strconv" "time" - - "github.com/joho/godotenv" ) type Config struct { - Mode string - PrivateKey string - ChiadoURL string - ChiadoSendInterval time.Duration - GnosisURL string - GnosisSendInterval time.Duration - Interval time.Duration - Timeout time.Duration - TestDuration time.Duration - NodeURL string + Mode string + PrivateKey string + ChiadoURL string + ChiadoSendInterval time.Duration + GnosisURL string + GnosisSendInterval time.Duration + Interval time.Duration + Timeout time.Duration + TestDuration time.Duration + NodeURL string + ShutterAPI string + ShutterRegistryCaller string + ShutterRegistryCallerNonce string + ApiRequestInterval time.Duration + DecryptionKeyWaitInterval time.Duration + BlameFolder string } func LoadConfig() Config { // Load the .env file - err := godotenv.Load() - if err != nil { - log.Fatalf("Error loading .env file") - } + // err := godotenv.Load() + // if err != nil { + // log.Fatalf("Error loading .env file") + // } config := Config{ - Mode: os.Getenv("MODE"), - PrivateKey: os.Getenv("PRIVATE_KEY"), - ChiadoURL: os.Getenv("CHIADO_URL"), - ChiadoSendInterval: time.Duration(GetEnvAsInt("CHIADO_SEND_INTERVAL")) * time.Second, - GnosisURL: os.Getenv("GNOSIS_URL"), - GnosisSendInterval: time.Duration(GetEnvAsInt("GNOSIS_SEND_INTERVAL")) * time.Second, - Timeout: time.Duration(GetEnvAsInt("WAIT_TX_TIMEOUT")) * time.Second, - TestDuration: time.Duration(GetEnvAsInt("TEST_DURATION")) * time.Second, - NodeURL: os.Getenv("NODE_URL"), + Mode: os.Getenv("MODE"), + PrivateKey: os.Getenv("PRIVATE_KEY"), + ChiadoURL: os.Getenv("CHIADO_URL"), + ChiadoSendInterval: time.Duration(GetEnvAsInt("CHIADO_SEND_INTERVAL")) * time.Second, + GnosisURL: os.Getenv("GNOSIS_URL"), + GnosisSendInterval: time.Duration(GetEnvAsInt("GNOSIS_SEND_INTERVAL")) * time.Second, + Timeout: time.Duration(GetEnvAsInt("WAIT_TX_TIMEOUT")) * time.Second, + TestDuration: time.Duration(GetEnvAsInt("TEST_DURATION")) * time.Second, + NodeURL: os.Getenv("NODE_URL"), + ShutterAPI: os.Getenv("SHUTTER_API"), + ApiRequestInterval: time.Duration(GetEnvAsInt("API_REQUEST_INTERVAL")) * time.Second, + ShutterRegistryCaller: os.Getenv("SHUTTER_REGISTRY_CALLER_ADDRESS"), + DecryptionKeyWaitInterval: time.Duration(GetEnvAsInt("DEC_KEY_WAIT_INTERVAL")) * time.Second, + BlameFolder: os.Getenv("DECRYPTION_BLAME_FOLDER"), } return config diff --git a/docker-compose.yaml b/docker-compose.yaml index 4339342..a2b2326 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,14 +8,18 @@ services: volumes: - ./logs:/app/logs environment: - - PRIVATE_KEY=${PRIVATE_KEY} - - MODE=${MODE} - - CHIADO_URL=${CHIADO_URL} - - CHIADO_SEND_INTERVAL=${CHIADO_SEND_INTERVAL} - - GNOSIS_URL=${GNOSIS_URL} - - GNOSIS_SEND_INTERVAL=${GNOSIS_SEND_INTERVAL} - - NODE_URL=${NODE_URL} - - WAIT_TX_TIMEOUT=${WAIT_TX_TIMEOUT} - - TEST_DURATION=${TEST_DURATION} - - command: ./main + PRIVATE_KEY: + MODE: + CHIADO_URL: + CHIADO_SEND_INTERVAL: + GNOSIS_URL: + GNOSIS_SEND_INTERVAL: + NODE_URL: + WAIT_TX_TIMEOUT: + TEST_DURATION: + SHUTTER_API: + SHUTTER_REGISTRY_CALLER_ADDRESS: + API_REQUEST_INTERVAL: + DEC_KEY_WAIT_INTERVAL: + DECRYPTION_BLAME_FOLDER: + # command: ./main diff --git a/go.mod b/go.mod index 90cf5f8..88d4083 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/crypto v0.23.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/sync v0.7.0 golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/main.go b/main.go index e52d0dd..319955c 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ func main() { var modes []string var cfg config.Config if len(os.Args[1:]) == 0 { - cfg := config.LoadConfig() + cfg = config.LoadConfig() log.Println(cfg.Mode) mode := cfg.Mode @@ -65,6 +65,12 @@ func main() { runCollector() wg.Done() }() + case "decryption-monitor": + wg.Add(1) + go func() { + tests.RunDecryptionMonitor(cfg) + wg.Done() + }() default: log.Printf("Unknown mode: %s", m) } diff --git a/template.env b/template.env index 9aeec46..cd9dae0 100644 --- a/template.env +++ b/template.env @@ -15,3 +15,9 @@ NODE_URL="https://erpc.chiado.staging.shutter.network" WAIT_TX_TIMEOUT=10 TEST_DURATION=1 +#DECRYPTION MONITOR +SHUTTER_API="http://shutter-api.shutter.network/api" +API_REQUEST_INTERVAL="60" +SHUTTER_REGISTRY_CALLER_ADDRESS="0x228DefCF37Da29475F0EE2B9E4dfAeDc3b0746bc" +DEC_KEY_WAIT_INTERVAL="20" +DECRYPTION_BLAME_FOLDER="decryptionBlameDir" \ No newline at end of file diff --git a/tests/decryptionmonitor.go b/tests/decryptionmonitor.go new file mode 100644 index 0000000..c9ad99d --- /dev/null +++ b/tests/decryptionmonitor.go @@ -0,0 +1,283 @@ +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/shutter-network/nethermind-tests/config" +) + +type RegisterIdentityRequest struct { + DecryptionTimestamp int64 `json:"decryptionTimestamp"` + IdentityPrefix string `json:"identityPrefix"` +} + +type RegisterIdentityResponse struct { + Message RegisterIdentityMessage `json:"message"` +} + +type RegisterIdentityMessage struct { + Eon int64 `json:"eon"` + Identity string `json:"identity"` + IdentityPrefix string `json:"identity_prefix"` + EonKey string `json:"eon_key"` + TxHash string `json:"tx_hash"` +} + +type DecryptionStats struct { + totalDecryptions atomic.Int64 + validDecryptions atomic.Int64 + invalidDecryptions atomic.Int64 + errorMessages []string +} + +var ( + stats DecryptionStats + stopChan = make(chan os.Signal, 1) +) + +type GetDataForEncryptionResponse struct { + Eon uint64 `json:"eon"` + Identity string `json:"identity"` + IdentityPrefix string `json:"identity_prefix"` + EonKey string `json:"eon_key"` + EpochID string `json:"epoch_id"` +} + +type ErrorResponse struct { + Description string `json:"description,omitempty"` + Metadata string `json:"metadata,omitempty"` + StatusCode int `json:"statusCode"` +} + +func RunDecryptionMonitor(cfg config.Config) { + baseURL := cfg.ShutterAPI + interval := cfg.ApiRequestInterval + address := cfg.ShutterRegistryCaller + + log.Printf("Starting performance monitoring\n") + log.Printf("Base URL: %s\n", baseURL) + log.Printf("Interval: %v\n\n", interval) + + // Setup graceful shutdown + signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + + var wg sync.WaitGroup + + // Start monitoring in separate goroutine + go func() { + runFlow(baseURL, address, &wg, cfg) + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + runFlow(baseURL, address, &wg, cfg) + case <-stopChan: + return + } + } + }() + + // Wait for interrupt signal + <-stopChan + log.Printf("Shutting down...\n") + + // Wait for all decryption requests to complete + wg.Wait() + + // Print final statistics + printStatistics(cfg) +} + +func printStatistics(cfg config.Config) { + log.Printf("\n=== Final Decryption Statistics ===\n") + log.Printf("Total decryption attempts: %d\n", stats.totalDecryptions.Load()) + log.Printf("Successful decryptions: %d\n", stats.validDecryptions.Load()) + log.Printf("Failed decryptions: %d\n", stats.invalidDecryptions.Load()) + + total := stats.totalDecryptions.Load() + if total > 0 { + successRate := float64(stats.validDecryptions.Load()) / float64(total) * 100 + log.Printf("Success rate: %.2f%%\n", successRate) + } + + // Write to blame file if BlameFolder is set + if cfg.BlameFolder != "" { + blameFile := fmt.Sprintf("%s/%d.blame", cfg.BlameFolder, time.Now().Unix()) + f, err := os.Create(blameFile) + if err != nil { + log.Printf("Failed to create blame file: %v", err) + return + } + defer f.Close() + fmt.Fprintf(f, "=== Final Decryption Statistics ===\n") + fmt.Fprintf(f, "Total decryption attempts: %d\n", stats.totalDecryptions.Load()) + fmt.Fprintf(f, "Successful decryptions: %d\n", stats.validDecryptions.Load()) + fmt.Fprintf(f, "Failed decryptions: %d\n", stats.invalidDecryptions.Load()) + if total > 0 { + successRate := float64(stats.validDecryptions.Load()) / float64(total) * 100 + fmt.Fprintf(f, "Success rate: %.2f%%\n", successRate) + } + if len(stats.errorMessages) > 0 { + fmt.Fprintf(f, "\nError Messages:\n") + for _, msg := range stats.errorMessages { + fmt.Fprintf(f, "%s\n", msg) + } + } + log.Printf("Wrote statistics to blame file: %s", blameFile) + } +} + +func runFlow(baseURL, address string, wg *sync.WaitGroup, cfg config.Config) { + timestamp := time.Now().Format("2006-01-02 15:04:05") + log.Printf("\n=== Performance Check at %s ===\n", timestamp) + + encryptionData, err := getDataForEncryption(baseURL, address, "") + if err != nil { + log.Printf("error in get data for encryption endpoint %s", err) + stats.invalidDecryptions.Add(1) + stats.errorMessages = append(stats.errorMessages, fmt.Sprintf("Error in get data for encryption endpoint: %s", err)) + return + } + decryptionTimestamp := time.Now().Unix() + 10 + registerReq := RegisterIdentityRequest{ + DecryptionTimestamp: decryptionTimestamp, + IdentityPrefix: encryptionData["message"].IdentityPrefix, + } + + identity, err := registerIdentity(baseURL, registerReq) + if err != nil { + log.Printf("error encountered while registering identity %s", err) + stats.invalidDecryptions.Add(1) + stats.errorMessages = append(stats.errorMessages, fmt.Sprintf("Error encountered while registering identity: %s", err)) + return + } + + // Launch decryption key request in separate goroutine + wg.Add(1) + go func(identity string) { + defer wg.Done() + time.Sleep(cfg.DecryptionKeyWaitInterval) + fmt.Printf("Requesting decryption key for identity: %s, at time: %s\n", identity, time.Now().Format("2006-01-02 15:04:05")) + stats.totalDecryptions.Add(1) + err = getDecryptionKey(baseURL, identity) + if err != nil { + log.Printf("error encountered while getting decryption key %s", err) + stats.invalidDecryptions.Add(1) + stats.errorMessages = append(stats.errorMessages, fmt.Sprintf("Error encountered while getting decryption key: %s", err)) + } else { + fmt.Printf("Decryption key retrieved successfully for identity: %s, at time: %s\n", identity, time.Now().Format("2006-01-02 15:04:05")) + stats.validDecryptions.Add(1) + } + }(identity) +} + +func getDataForEncryption(baseURL, address, identityPrefix string) (map[string]GetDataForEncryptionResponse, error) { + params := url.Values{} + params.Add("address", address) + if identityPrefix != "" { + params.Add("identityPrefix", identityPrefix) + } + + url := fmt.Sprintf("%s/get_data_for_encryption?%s", baseURL, params.Encode()) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + var errorResp ErrorResponse + if err := json.Unmarshal(body, &errorResp); err != nil { + return nil, fmt.Errorf("failed to parse error response: %v", err) + } + return nil, err + + } + + var response map[string]GetDataForEncryptionResponse + + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %v", err) + } + + return response, nil +} + +func registerIdentity(baseURL string, req RegisterIdentityRequest) (string, error) { + jsonData, err := json.Marshal(req) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %v", err) + } + + resp, err := http.Post( + baseURL+"/register_identity", + "application/json", + bytes.NewBuffer(jsonData), + ) + if err != nil { + return "", fmt.Errorf("request failed: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %v", err) + } + + if resp.StatusCode != http.StatusOK { + var errorResp ErrorResponse + if err := json.Unmarshal(body, &errorResp); err != nil { + return "", fmt.Errorf("failed to parse error response: %v", err) + } + return "", fmt.Errorf("response error %v", errorResp.Description) + } + var response RegisterIdentityResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("failed to parse response: %v", err) + } + fmt.Printf("Identity registered successfully: %s | tx: %s\n", response.Message.Identity, response.Message.TxHash) + return response.Message.Identity, nil +} + +func getDecryptionKey(baseURL, identity string) error { + params := url.Values{} + params.Add("identity", identity) + + url := fmt.Sprintf("%s/get_decryption_key?%s", baseURL, params.Encode()) + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %v", err) + } + if resp.StatusCode != http.StatusOK { + var errorResp ErrorResponse + if err := json.Unmarshal(body, &errorResp); err != nil { + return fmt.Errorf("failed to parse error response, %v", err) + } + return fmt.Errorf("failed to parse error response, %s", errorResp.Description) + } + return nil +}