154 lines
6.6 KiB
Go
154 lines
6.6 KiB
Go
package ccq
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/certusone/wormhole/node/pkg/common"
|
|
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
|
|
"github.com/certusone/wormhole/node/pkg/query"
|
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
"go.uber.org/zap"
|
|
|
|
ethAbi "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi"
|
|
ethBind "github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
eth_common "github.com/ethereum/go-ethereum/common"
|
|
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
|
ethClient "github.com/ethereum/go-ethereum/ethclient"
|
|
ethRpc "github.com/ethereum/go-ethereum/rpc"
|
|
)
|
|
|
|
func FetchCurrentGuardianSet(rpcUrl, coreAddr string) (*common.GuardianSet, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
defer cancel()
|
|
ethContract := eth_common.HexToAddress(coreAddr)
|
|
rawClient, err := ethRpc.DialContext(ctx, rpcUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to ethereum")
|
|
}
|
|
client := ethClient.NewClient(rawClient)
|
|
caller, err := ethAbi.NewAbiCaller(ethContract, client)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create caller")
|
|
}
|
|
currentIndex, err := caller.GetCurrentGuardianSetIndex(ðBind.CallOpts{Context: ctx})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error requesting current guardian set index: %w", err)
|
|
}
|
|
gs, err := caller.GetGuardianSet(ðBind.CallOpts{Context: ctx}, currentIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error requesting current guardian set value: %w", err)
|
|
}
|
|
return &common.GuardianSet{
|
|
Keys: gs.Keys,
|
|
Index: currentIndex,
|
|
}, nil
|
|
}
|
|
|
|
// validateRequest verifies that this API key is allowed to do all of the calls in this request. In the case of an error, it returns the HTTP status.
|
|
func validateRequest(logger *zap.Logger, env common.Environment, perms *Permissions, signerKey *ecdsa.PrivateKey, apiKey string, qr *gossipv1.SignedQueryRequest) (int, error) {
|
|
permsForUser, exists := perms.GetUserEntry(apiKey)
|
|
if !exists {
|
|
logger.Debug("invalid api key", zap.String("apiKey", apiKey))
|
|
invalidQueryRequestReceived.WithLabelValues("invalid_api_key").Inc()
|
|
return http.StatusForbidden, fmt.Errorf("invalid api key")
|
|
}
|
|
|
|
// TODO: Should we verify the signatures?
|
|
|
|
if len(qr.Signature) == 0 {
|
|
if !permsForUser.allowUnsigned || signerKey == nil {
|
|
logger.Debug("request not signed and unsigned requests not supported for this user",
|
|
zap.String("userName", permsForUser.userName),
|
|
zap.Bool("allowUnsigned", permsForUser.allowUnsigned),
|
|
zap.Bool("signerKeyConfigured", signerKey != nil),
|
|
)
|
|
invalidQueryRequestReceived.WithLabelValues("request_not_signed").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf("request not signed")
|
|
}
|
|
|
|
// Sign the request using our key.
|
|
var err error
|
|
digest := query.QueryRequestDigest(env, qr.QueryRequest)
|
|
qr.Signature, err = ethCrypto.Sign(digest.Bytes(), signerKey)
|
|
if err != nil {
|
|
logger.Debug("failed to sign request", zap.String("userName", permsForUser.userName), zap.Error(err))
|
|
invalidQueryRequestReceived.WithLabelValues("failed_to_sign_request").Inc()
|
|
return http.StatusInternalServerError, fmt.Errorf("failed to sign request: %w", err)
|
|
}
|
|
}
|
|
|
|
var queryRequest query.QueryRequest
|
|
err := queryRequest.Unmarshal(qr.QueryRequest)
|
|
if err != nil {
|
|
logger.Debug("failed to unmarshal request", zap.String("userName", permsForUser.userName), zap.Error(err))
|
|
invalidQueryRequestReceived.WithLabelValues("failed_to_unmarshal_request").Inc()
|
|
return http.StatusInternalServerError, fmt.Errorf("failed to unmarshal request: %w", err)
|
|
}
|
|
|
|
// Make sure the overall query request is sane.
|
|
if err := queryRequest.Validate(); err != nil {
|
|
logger.Debug("failed to validate request", zap.String("userName", permsForUser.userName), zap.Error(err))
|
|
invalidQueryRequestReceived.WithLabelValues("failed_to_validate_request").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf("failed to validate request: %w", err)
|
|
}
|
|
|
|
// Make sure they are allowed to make all of the calls that they are asking for.
|
|
for _, pcq := range queryRequest.PerChainQueries {
|
|
var status int
|
|
var err error
|
|
switch q := pcq.Query.(type) {
|
|
case *query.EthCallQueryRequest:
|
|
status, err = validateCallData(logger, permsForUser, "ethCall", pcq.ChainId, q.CallData)
|
|
case *query.EthCallByTimestampQueryRequest:
|
|
status, err = validateCallData(logger, permsForUser, "ethCallByTimestamp", pcq.ChainId, q.CallData)
|
|
case *query.EthCallWithFinalityQueryRequest:
|
|
status, err = validateCallData(logger, permsForUser, "ethCallWithFinality", pcq.ChainId, q.CallData)
|
|
default:
|
|
logger.Debug("unsupported query type", zap.String("userName", permsForUser.userName), zap.Any("type", pcq.Query))
|
|
invalidQueryRequestReceived.WithLabelValues("unsupported_query_type").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf("unsupported query type")
|
|
}
|
|
|
|
if err != nil {
|
|
// Metric is pegged below.
|
|
return status, err
|
|
}
|
|
}
|
|
|
|
logger.Debug("submitting query request", zap.String("userName", permsForUser.userName))
|
|
return http.StatusOK, nil
|
|
}
|
|
|
|
// validateCallData performs verification on all of the call data objects in a query.
|
|
func validateCallData(logger *zap.Logger, permsForUser *permissionEntry, callTag string, chainId vaa.ChainID, callData []*query.EthCallData) (int, error) {
|
|
for _, cd := range callData {
|
|
contractAddress, err := vaa.BytesToAddress(cd.To)
|
|
if err != nil {
|
|
logger.Debug("failed to parse contract address", zap.String("userName", permsForUser.userName), zap.String("contract", hex.EncodeToString(cd.To)), zap.Error(err))
|
|
invalidQueryRequestReceived.WithLabelValues("invalid_contract_address").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf("failed to parse contract address: %w", err)
|
|
}
|
|
if len(cd.Data) < ETH_CALL_SIG_LENGTH {
|
|
logger.Debug("eth call data must be at least four bytes", zap.String("userName", permsForUser.userName), zap.String("data", hex.EncodeToString(cd.Data)))
|
|
invalidQueryRequestReceived.WithLabelValues("bad_call_data").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf("eth call data must be at least four bytes")
|
|
}
|
|
call := hex.EncodeToString(cd.Data[0:ETH_CALL_SIG_LENGTH])
|
|
callKey := fmt.Sprintf("%s:%d:%s:%s", callTag, chainId, contractAddress, call)
|
|
if _, exists := permsForUser.allowedCalls[callKey]; !exists {
|
|
logger.Debug("requested call not authorized", zap.String("userName", permsForUser.userName), zap.String("callKey", callKey))
|
|
invalidQueryRequestReceived.WithLabelValues("call_not_authorized").Inc()
|
|
return http.StatusBadRequest, fmt.Errorf(`call "%s" not authorized`, callKey)
|
|
}
|
|
|
|
totalRequestedCallsByChain.WithLabelValues(chainId.String()).Inc()
|
|
}
|
|
|
|
return http.StatusOK, nil
|
|
}
|