| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- package ccq
- import (
- "context"
- "crypto/ecdsa"
- "encoding/hex"
- "errors"
- "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"
- "github.com/gagliardetto/solana-go"
- )
- func FetchCurrentGuardianSet(ctx context.Context, rpcUrl, coreAddr string) (*common.GuardianSet, error) {
- ctx, cancel := context.WithTimeout(ctx, time.Second*5)
- defer cancel()
- ethContract := eth_common.HexToAddress(coreAddr)
- rawClient, err := ethRpc.DialContext(ctx, rpcUrl)
- if err != nil {
- return nil, errors.New("failed to connect to ethereum")
- }
- client := ethClient.NewClient(rawClient)
- caller, err := ethAbi.NewAbiCaller(ethContract, client)
- if err != nil {
- return nil, errors.New("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, *query.QueryRequest, 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, nil, errors.New("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, nil, errors.New("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, nil, 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.StatusBadRequest, nil, 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, nil, 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)
- case *query.SolanaAccountQueryRequest:
- status, err = validateSolanaAccountQuery(logger, permsForUser, "solAccount", pcq.ChainId, q)
- case *query.SolanaPdaQueryRequest:
- status, err = validateSolanaPdaQuery(logger, permsForUser, "solPDA", pcq.ChainId, q)
- 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, nil, errors.New("unsupported query type")
- }
- if err != nil {
- // Metric is pegged below.
- return status, nil, err
- }
- }
- logger.Debug("submitting query request", zap.String("userName", permsForUser.userName))
- return http.StatusOK, &queryRequest, 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, errors.New("eth call data must be at least four bytes")
- }
- if !permsForUser.allowAnything {
- 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 {
- // The call data doesn't exist including the contract address. See if it's covered by a wildcard.
- wildCardCallKey := fmt.Sprintf("%s:%d:*:%s", callTag, chainId, call)
- if _, exists := permsForUser.allowedCalls[wildCardCallKey]; !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
- }
- // validateSolanaAccountQuery performs verification on a Solana sol_account query.
- func validateSolanaAccountQuery(logger *zap.Logger, permsForUser *permissionEntry, callTag string, chainId vaa.ChainID, q *query.SolanaAccountQueryRequest) (int, error) {
- if !permsForUser.allowAnything {
- for _, acct := range q.Accounts {
- callKey := fmt.Sprintf("%s:%d:%s", callTag, chainId, solana.PublicKey(acct).String())
- 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.StatusForbidden, fmt.Errorf(`call "%s" not authorized`, callKey)
- }
- totalRequestedCallsByChain.WithLabelValues(chainId.String()).Inc()
- }
- }
- return http.StatusOK, nil
- }
- // validateSolanaPdaQuery performs verification on a Solana sol_account query.
- func validateSolanaPdaQuery(logger *zap.Logger, permsForUser *permissionEntry, callTag string, chainId vaa.ChainID, q *query.SolanaPdaQueryRequest) (int, error) {
- if !permsForUser.allowAnything {
- for _, acct := range q.PDAs {
- callKey := fmt.Sprintf("%s:%d:%s", callTag, chainId, solana.PublicKey(acct.ProgramAddress).String())
- 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.StatusForbidden, fmt.Errorf(`call "%s" not authorized`, callKey)
- }
- totalRequestedCallsByChain.WithLabelValues(chainId.String()).Inc()
- }
- }
- return http.StatusOK, nil
- }
|