Skip to content

Commit

Permalink
CCQ: Query Server
Browse files Browse the repository at this point in the history
  • Loading branch information
bruce-riley committed Oct 4, 2023
1 parent 251e6c4 commit 16b9007
Show file tree
Hide file tree
Showing 10 changed files with 1,033 additions and 0 deletions.
60 changes: 60 additions & 0 deletions devnet/query-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
apiVersion: v1
kind: Service
metadata:
name: query-server
labels:
app: query-server
spec:
ports:
- name: rest
port: 6069
protocol: TCP
selector:
app: query-server
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: query-server
spec:
selector:
matchLabels:
app: query-server
serviceName: query-server
replicas: 1
template:
metadata:
labels:
app: query-server
spec:
containers:
- name: query-server
image: guardiand-image
command:
- /guardiand
- query-server
- --env
- dev
- --nodeKey
- node/cmd/ccq/ccq.p2p.key
- --signerKey
- node/cmd/ccq/ccq.signing.key
- --listenAddr
- "[::]:6069"
- --permFile
- "node/cmd/ccq/devnet.config.json"
- --ethRPC
- http://eth-devnet:8545
- --ethContract
- "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
# Hardcoded devnet bootstrap (generated from deterministic key in guardiand)
- --bootstrap
- /dns4/guardian-0.guardian/udp/8996/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw
- --logLevel=info
ports:
- containerPort: 6069
name: rest
protocol: TCP
readinessProbe:
tcpSocket:
port: rest
1 change: 1 addition & 0 deletions node/cmd/ccq/ccq.p2p.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@~D&�o%�[�_j��szf=5����f�C�d �ӥ���91)���U�=���@JxӼl ]a��f?
6 changes: 6 additions & 0 deletions node/cmd/ccq/ccq.signing.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN CCQ SERVER SIGNING KEY-----
PublicKey: 0x25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54

CiCWNLSaicmcA2T563fLSM0r2uFviwPdA1VV9i76DlJh3Q==
=NV1/
-----END CCQ SERVER SIGNING KEY-----
65 changes: 65 additions & 0 deletions node/cmd/ccq/devnet.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"permissions": [
{
"userName": "Test User",
"apiKey": "my_secret_key",
"allowedCalls": [
{
"ethCall": {
"note:": "Name of WETH on Goerli",
"chain": 2,
"contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x06fdde03"
}
},
{
"ethCall": {
"note:": "Total supply of WETH on Goerli",
"chain": 2,
"contractAddress": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x18160ddd"
}
},
{
"ethCall": {
"note:": "Name of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x06fdde03"
}
},
{
"ethCall": {
"note:": "Total supply of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x18160ddd"
}
}
]
},
{
"userName": "Test User Two",
"apiKey": "my_secret_key_2",
"allowUnsigned": true,
"allowedCalls": [
{
"ethCall": {
"note:": "Name of WETH on Goerli",
"chain": 2,
"contractAddress": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x06fdde03"
}
},
{
"ethCall": {
"note:": "Name of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x06fdde03"
}
}
]
}
]
}
175 changes: 175 additions & 0 deletions node/cmd/ccq/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package ccq

import (
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"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/gorilla/mux"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

type queryRequest struct {
Bytes string `json:"bytes"`
Signature string `json:"signature"`
}

type queryResponse struct {
Bytes string `json:"bytes"`
Signatures []string `json:"signatures"`
}

type httpServer struct {
topic *pubsub.Topic
logger *zap.Logger
env common.Environment
permissions Permissions
signerKey *ecdsa.PrivateKey
pendingResponses *PendingResponses
}

func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) {
// Set CORS headers for all requests.
w.Header().Set("Access-Control-Allow-Origin", "*")

// Set CORS headers for the preflight request
if r.Method == http.MethodOptions {

w.Header().Set("Access-Control-Allow-Methods", "PUT")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Api-Key")
w.Header().Set("Access-Control-Max-Age", "3600")
w.WriteHeader(http.StatusNoContent)
return
}
var q queryRequest
err := json.NewDecoder(r.Body).Decode(&q)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// There should be one and only one API key in the header.
apiKey, exists := r.Header["X-Api-Key"]
if !exists || len(apiKey) != 1 {
s.logger.Debug("received a request without an api key", zap.Stringer("url", r.URL), zap.Error(err))
http.Error(w, "api key is missing", http.StatusBadRequest)
return
}

queryRequestBytes, err := hex.DecodeString(q.Bytes)
if err != nil {
s.logger.Debug("failed to decode request bytes", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

signature, err := hex.DecodeString(q.Signature)
if err != nil {
s.logger.Debug("failed to decode signature bytes", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

signedQueryRequest := &gossipv1.SignedQueryRequest{
QueryRequest: queryRequestBytes,
Signature: signature,
}

if err := validateRequest(s.logger, s.env, s.permissions, s.signerKey, apiKey[0], signedQueryRequest); err != nil {
s.logger.Debug("invalid request", zap.String("api_key", apiKey[0]), zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

m := gossipv1.GossipMessage{
Message: &gossipv1.GossipMessage_SignedQueryRequest{
SignedQueryRequest: signedQueryRequest,
},
}

b, err := proto.Marshal(&m)
if err != nil {
s.logger.Debug("failed to marshal gossip message", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

pendingResponse := NewPendingResponse(signedQueryRequest)
added := s.pendingResponses.Add(pendingResponse)
if !added {
http.Error(w, "Duplicate request", http.StatusInternalServerError)
return
}

err = s.topic.Publish(r.Context(), b)
if err != nil {
s.logger.Debug("failed to publish gossip message", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
s.pendingResponses.Remove(pendingResponse)
return
}

// Wait for the response or timeout
select {
case <-time.After(query.RequestTimeout + 5*time.Second):
http.Error(w, "Timed out waiting for response", http.StatusGatewayTimeout)
case res := <-pendingResponse.ch:
resBytes, err := res.Response.Marshal()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
break
}
// Signature indices must be ascending for on-chain verification
sort.Slice(res.Signatures, func(i, j int) bool {
return res.Signatures[i].Index < res.Signatures[j].Index
})
signatures := make([]string, 0, len(res.Signatures))
for _, s := range res.Signatures {
// ECDSA signature + a byte for the index of the guardian in the guardian set
signature := fmt.Sprintf("%s%02x", s.Signature, uint8(s.Index))
signatures = append(signatures, signature)
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&queryResponse{
Signatures: signatures,
Bytes: hex.EncodeToString(resBytes),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

s.pendingResponses.Remove(pendingResponse)
}

func (s *httpServer) handleHealth(w http.ResponseWriter, r *http.Request) {
s.logger.Debug("health check")
}

func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, signerKey *ecdsa.PrivateKey, p *PendingResponses, logger *zap.Logger, env common.Environment) *http.Server {
s := &httpServer{
topic: t,
permissions: permissions,
signerKey: signerKey,
pendingResponses: p,
logger: logger,
env: env,
}
r := mux.NewRouter()
r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT", "OPTIONS")
r.HandleFunc("/v1/health", s.handleHealth).Methods("GET")
return &http.Server{
Addr: addr,
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
}
}
Loading

0 comments on commit 16b9007

Please sign in to comment.