From 07508b533e291cbd8fb1669355722c3b1283cd2c Mon Sep 17 00:00:00 2001 From: Marc Font <36164126+Marketen@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:24:49 +0200 Subject: [PATCH] add signature array processing (#9) --- listener/src/api/handlers.go | 119 +++++++++++++++++++---------------- listener/src/api/utils.go | 21 ++++++- 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/listener/src/api/handlers.go b/listener/src/api/handlers.go index 59c8fde..1796e02 100644 --- a/listener/src/api/handlers.go +++ b/listener/src/api/handlers.go @@ -17,71 +17,82 @@ func (s *httpApi) handleRoot(w http.ResponseWriter, r *http.Request) { func (s *httpApi) handleSignature(w http.ResponseWriter, r *http.Request) { logger.Debug("Received new POST '/signature' request") - var req types.SignatureRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - logger.Error("Failed to decode request body" + err.Error()) - s.respondError(w, http.StatusBadRequest, "Invalid request") - return - } - - // validate that the request has the required fields and that the signature is valid - // we expect network and label to be strings, signature to be a 0x prefixed hex string of 194 characters - // and payload to be a 0x prefixed hex string. - if req.Network == "" || req.Label == "" || req.Signature == "" || req.Payload == "" { - logger.Debug("Invalid request, missing fields. Request had the following fields: " + req.Network + " " + req.Label + " " + req.Signature + " " + req.Payload) - s.respondError(w, http.StatusBadRequest, "Invalid request, missing fields") - return - } - - // validate the signature - if !validateSignature(req.Signature) { - logger.Debug("Invalid signature. It should be a 0x prefixed hex string of 194 characters. Request had the following signature: " + req.Signature) - s.respondError(w, http.StatusBadRequest, "Invalid signature") - return - } - // Decode BASE64 payload - decodedBytes, err := base64.StdEncoding.DecodeString(req.Payload) + var sigs []types.SignatureRequest + err := json.NewDecoder(r.Body).Decode(&sigs) if err != nil { - logger.Error("Failed to decode BASE64 payload" + err.Error()) - s.respondError(w, http.StatusBadRequest, "Invalid BASE64 payload") + logger.Error("Failed to decode request body: " + err.Error()) + s.respondError(w, http.StatusBadRequest, "Invalid request format") return } - var decodedPayload types.DecodedPayload - err = json.Unmarshal(decodedBytes, &decodedPayload) - if err != nil { - logger.Error("Failed to decode JSON payload" + err.Error()) - s.respondError(w, http.StatusBadRequest, "Invalid payload format") - return + + var validPubkeys []string // Needed to store valid pubkeys for bulk validation later + + // For each element of the request slice, we validate the element format and decode its payload + for _, req := range sigs { + if req.Network == "" || req.Label == "" || req.Signature == "" || req.Payload == "" { + logger.Debug("Skipping invalid signature from request, missing fields") + continue // Skipping invalid elements + } + if !validateSignature(req.Signature) { + logger.Debug("Skipping invalid signature from request, invalid signature format: " + req.Signature) + continue // Skipping invalid signatures + } + decodedBytes, err := base64.StdEncoding.DecodeString(req.Payload) + if err != nil { + logger.Error("Failed to decode BASE64 payload from request: " + err.Error()) + continue // Skipping payloads that can't be decoded from BASE64 + } + var decodedPayload types.DecodedPayload + if err := json.Unmarshal(decodedBytes, &decodedPayload); err != nil { + logger.Error("Failed to decode JSON payload from request: " + err.Error()) + continue // Skipping payloads that can't be decoded from JSON + } + // If the payload is valid, we append the pubkey to the validPubkeys slice. Else, we skip it + if decodedPayload.Platform == "dappnode" && decodedPayload.Timestamp != "" && decodedPayload.Pubkey != "" { + validPubkeys = append(validPubkeys, decodedPayload.Pubkey) // Collecting valid pubkeys + } else { + logger.Debug("Skipping invalid signature from request, invalid payload format") + } } - // Validate the decoded payload (maybe we should be more strict here) - if decodedPayload.Platform != "dappnode" || decodedPayload.Timestamp == "" || decodedPayload.Pubkey == "" { - logger.Debug("Invalid payload content. Request had the following payload: " + decodedPayload.Platform + " " + decodedPayload.Timestamp + " " + decodedPayload.Pubkey) - s.respondError(w, http.StatusBadRequest, "Payload content is invalid") + // Make a single API call to validate pubkeys in bulk + validatedPubkeys := validatePubkeysWithConsensusClient(validPubkeys) + if len(validatedPubkeys) == 0 { + s.respondError(w, http.StatusInternalServerError, "Failed to validate pubkeys with consensus client") return } - logger.Debug("Request's payload decoded successfully. Inserting decoded message into MongoDB") - - // Insert into MongoDB - _, err = s.dbCollection.InsertOne(r.Context(), bson.M{ - "platform": decodedPayload.Platform, - "timestamp": decodedPayload.Timestamp, - "pubkey": decodedPayload.Pubkey, - "signature": req.Signature, - "network": req.Network, - "label": req.Label, - }) - if err != nil { - logger.Error("Failed to insert message into MongoDB" + err.Error()) - s.respondError(w, http.StatusInternalServerError, "Failed to insert payload into MongoDB") - return + // Now, iterate over the originally valid requests, check if the pubkey was validated, then verify signature and insert into DB + // This means going over the requests again! TODO: find a better way? + for _, req := range sigs { + decodedBytes, _ := base64.StdEncoding.DecodeString(req.Payload) + var decodedPayload types.DecodedPayload + json.Unmarshal(decodedBytes, &decodedPayload) + + // Only try to validate message signatures if the pubkey is validated + if _, exists := validatedPubkeys[decodedPayload.Pubkey]; exists { + // If the pubkey is validated, we can proceed to validate the signature + if validateSignatureAgainstPayload(req.Signature, decodedPayload) { + // Insert into MongoDB + _, err := s.dbCollection.InsertOne(r.Context(), bson.M{ + "platform": decodedPayload.Platform, + "timestamp": decodedPayload.Timestamp, + "pubkey": decodedPayload.Pubkey, + "signature": req.Signature, + "network": req.Network, + "label": req.Label, + }) + if err != nil { + continue // TODO: Log error or handle as needed + } else { + logger.Info("New Signature " + req.Signature + " inserted into MongoDB") + } + } + } } - logger.Info("A new message with pubkey " + decodedPayload.Pubkey + " was decoded and inserted into MongoDB successfully") - s.respondOK(w, "Message validated and inserted into MongoDB") + s.respondOK(w, "Finished processing signatures") } func (s *httpApi) handleGetSignatures(w http.ResponseWriter, r *http.Request) { diff --git a/listener/src/api/utils.go b/listener/src/api/utils.go index 5c2667a..aa1b7d7 100644 --- a/listener/src/api/utils.go +++ b/listener/src/api/utils.go @@ -1,11 +1,30 @@ package api +import "github.com/dappnode/validator-monitoring/listener/src/types" + // A valid signature is a 0x prefixed hex string of 194 characters (including the prefix) func validateSignature(signature string) bool { - // validate the signature if len(signature) != 194 || signature[:2] != "0x" { return false } return true } + +// validatePubkeysWithConsensusClient simulates making a bulk request to a consensus client for validating pubkeys. +// It can return a map of validated pubkeys that exist as validators. +func validatePubkeysWithConsensusClient(pubkeys []string) map[string]bool { + validatedPubkeys := make(map[string]bool) + // make api call: GET /eth/v1/beacon/states/{state_id}/validators?id=validator_pubkey1,validator_pubkey2,validator_pubkey3 + + for _, pubkey := range pubkeys { + validatedPubkeys[pubkey] = true // Assuming all given pubkeys are valid + } + return validatedPubkeys +} + +// Dummy implementation of validateSignatureAgainstPayload +func validateSignatureAgainstPayload(signature string, payload types.DecodedPayload) bool { + // signature validation logic here + return true +}