Files
prysm/validator/keymanager/remote-web3signer/internal/client.go
james-prysm 539b981ac3 Web3signer: persistent public keys (#13682)
* WIP

* broken and still wip

* more wip improving saving

* wip

* removing cyclic dependency

* gaz

* fixes

* fixing more tests and how files load

* fixing wallet tests

* fixing test

* updating keymanager tests

* improving how the web3signer keymanager works

* WIP

* updated keymanager to read from file

* gaz

* reuse readkeyfile function and add in duplicate keys check

* adding in locks to increase safety

* refactored how saving keys work, more tests needed:

* fix test

* fix tests

* adding unit tests and cleaning up locks

* fixing tests

* tests were not fixed properly

* removing unneeded files

* Update cmd/validator/accounts/wallet_utils.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* Update validator/accounts/wallet/wallet.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* review feedback

* updating flags and e2e

* deepsource fix

* resolving feedback

* removing fatal test for now

* addressing manu's feedback

* gofmt

* fixing tests

* fixing unit tests

* more idomatic feedback

* updating log files

* updating based on preston's suggestion

* improving logs and event triggers

* addressing comments from manu

* truncating was not triggering key file reload

* fixing unit test

* removing wrong dependency

* fix another broken unit test

* fixing bad pathing on file

* handle errors in test

* fixing testdata dependency

* resolving deepsource and comment around logs

* removing unneeded buffer

* reworking ux of web3signer file, unit tests to come

* adding unit tests for file change retries

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* updating based on review feedback

* missed err check

* adding some aliases to make running easier

* Update validator/keymanager/remote-web3signer/log.go

Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* radek's review

* Update validator/keymanager/remote-web3signer/internal/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/keymanager/remote-web3signer/keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* addressing more review feedback and linting

* fixing tests

* adding log

* adding 1 more test

* improving logs

---------

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2024-06-26 15:36:32 +00:00

222 lines
7.3 KiB
Go

package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
"github.com/prysmaticlabs/prysm/v5/monitoring/tracing"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
)
const (
ethApiNamespace = "/api/v1/eth2/sign/"
)
type SignRequestJson []byte
// SignatureResponse is the struct representing the signing request response in json format
type SignatureResponse struct {
Signature hexutil.Bytes `json:"signature"`
}
// HttpSignerClient defines the interface for interacting with a remote web3signer.
type HttpSignerClient interface {
Sign(ctx context.Context, pubKey string, request SignRequestJson) (bls.Signature, error)
GetPublicKeys(ctx context.Context, url string) ([]string, error)
}
// ApiClient a wrapper object around web3signer APIs. Please refer to the docs from Consensys' web3signer project.
type ApiClient struct {
BaseURL *url.URL
RestClient *http.Client
}
// NewApiClient method instantiates a new ApiClient object.
func NewApiClient(baseEndpoint string) (*ApiClient, error) {
u, err := url.ParseRequestURI(baseEndpoint)
if err != nil {
return nil, errors.Wrap(err, "invalid format, unable to parse url")
}
if u.Scheme == "" || u.Host == "" {
return nil, fmt.Errorf("web3signer url must be in the format of http(s)://host:port url used: %v", baseEndpoint)
}
return &ApiClient{
BaseURL: u,
RestClient: &http.Client{},
}, nil
}
// Sign is a wrapper method around the web3signer sign api.
func (client *ApiClient) Sign(ctx context.Context, pubKey string, request SignRequestJson) (bls.Signature, error) {
requestPath := ethApiNamespace + pubKey
resp, err := client.doRequest(ctx, http.MethodPost, client.BaseURL.String()+requestPath, bytes.NewBuffer(request))
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("public key not found")
}
if resp.StatusCode == http.StatusPreconditionFailed {
return nil, fmt.Errorf("signing operation failed due to slashing protection rules, Signing Request URL: %v, Status: %v", client.BaseURL.String()+requestPath, resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") {
var sigResp SignatureResponse
if err := unmarshalResponse(resp.Body, &sigResp); err != nil {
return nil, err
}
return bls.SignatureFromBytes(sigResp.Signature)
} else {
return unmarshalSignatureResponse(resp.Body)
}
}
// GetPublicKeys is a wrapper method around the web3signer publickeys api (this may be removed in the future or moved to another location due to its usage).
func (client *ApiClient) GetPublicKeys(ctx context.Context, url string) ([]string, error) {
resp, err := client.doRequest(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
var publicKeys []string
if err := unmarshalResponse(resp.Body, &publicKeys); err != nil {
return nil, err
}
if len(publicKeys) == 0 {
return publicKeys, nil
}
// early check if it's a hex and a public key
// note: a full loop will be conducted in keymanager.go if the quick check passes
b, err := hexutil.Decode(publicKeys[0])
if err != nil {
return nil, errors.Wrap(err, "unable to decode public key")
}
if len(b) != fieldparams.BLSPubkeyLength {
return nil, fmt.Errorf("invalid public key length of %v bytes", len(b))
}
return publicKeys, nil
}
// ReloadSignerKeys is a wrapper method around the web3signer reload api.
func (client *ApiClient) ReloadSignerKeys(ctx context.Context) error {
const requestPath = "/reload"
if _, err := client.doRequest(ctx, http.MethodPost, client.BaseURL.String()+requestPath, nil); err != nil {
return err
}
return nil
}
// GetServerStatus is a wrapper method around the web3signer upcheck api
func (client *ApiClient) GetServerStatus(ctx context.Context) (string, error) {
const requestPath = "/upcheck"
resp, err := client.doRequest(ctx, http.MethodGet, client.BaseURL.String()+requestPath, nil /* no body needed on get request */)
if err != nil {
return "", err
}
var status string
if err := unmarshalResponse(resp.Body, &status); err != nil {
return "", err
}
return status, nil
}
// doRequest is a utility method for requests.
func (client *ApiClient) doRequest(ctx context.Context, httpMethod, fullPath string, body io.Reader) (*http.Response, error) {
var requestDump []byte
ctx, span := trace.StartSpan(ctx, "remote_web3signer.Client.doRequest")
defer span.End()
span.AddAttributes(
trace.StringAttribute("httpMethod", httpMethod),
trace.StringAttribute("fullPath", fullPath),
trace.BoolAttribute("hasBody", body != nil),
)
req, err := http.NewRequestWithContext(ctx, httpMethod, fullPath, body)
if err != nil {
return nil, errors.Wrap(err, "invalid format, failed to create new Post Request Object")
}
req.Header.Set("Content-Type", "application/json")
start := time.Now()
resp, err := client.RestClient.Do(req)
duration := time.Since(start)
if err != nil {
signRequestDurationSeconds.WithLabelValues(req.Method, "error").Observe(duration.Seconds())
err = errors.Wrap(err, "failed to execute json request")
tracing.AnnotateError(span, err)
return resp, err
} else {
signRequestDurationSeconds.WithLabelValues(req.Method, strconv.Itoa(resp.StatusCode)).Observe(duration.Seconds())
}
if resp.StatusCode != http.StatusOK {
requestDump, err = httputil.DumpRequestOut(req, true)
if err != nil {
return nil, err
}
responseDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}
log.WithFields(logrus.Fields{
"status": resp.StatusCode,
"request": string(requestDump),
"response": string(responseDump),
}).Error("web3signer request failed")
}
if resp.StatusCode == http.StatusInternalServerError {
err = fmt.Errorf("internal Web3Signer server error, Signing Request URL: %v Status: %v", fullPath, resp.StatusCode)
tracing.AnnotateError(span, err)
return nil, err
} else if resp.StatusCode == http.StatusBadRequest {
err = fmt.Errorf("bad request format, Signing Request URL: %v Status: %v", fullPath, resp.StatusCode)
tracing.AnnotateError(span, err)
return nil, err
}
return resp, nil
}
// unmarshalResponse is a utility method for unmarshalling responses.
func unmarshalResponse(responseBody io.ReadCloser, unmarshalledResponseObject interface{}) error {
defer closeBody(responseBody)
if err := json.NewDecoder(responseBody).Decode(&unmarshalledResponseObject); err != nil {
body, err := io.ReadAll(responseBody)
if err != nil {
return errors.Wrap(err, "failed to read response body")
}
return errors.Wrap(err, fmt.Sprintf("invalid format, unable to read response body: %v", string(body)))
}
return nil
}
func unmarshalSignatureResponse(responseBody io.ReadCloser) (bls.Signature, error) {
defer closeBody(responseBody)
body, err := io.ReadAll(responseBody)
if err != nil {
return nil, err
}
sigBytes, err := hexutil.Decode(string(body))
if err != nil {
return nil, err
}
return bls.SignatureFromBytes(sigBytes)
}
// closeBody a utility method to wrap an error for closing
func closeBody(body io.Closer) {
if err := body.Close(); err != nil {
log.WithError(err).Error("could not close response body")
}
}