diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 6c70295759..e91ee5ad0c 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -4,6 +4,7 @@ package main import ( + "bytes" "context" "crypto/ecdsa" "encoding/hex" @@ -17,7 +18,9 @@ import ( "github.com/certusone/wormhole/node/pkg/p2p" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1" + "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/libp2p/go-libp2p" dht "github.com/libp2p/go-libp2p-kad-dht" @@ -176,8 +179,19 @@ func main() { // END SETUP // + wethAbi, err := abi.JSON(strings.NewReader("[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]")) + if err != nil { + panic(err) + } + + // methodName := "totalSupply" + methodName := "name" + data, err := wethAbi.Pack(methodName) + if err != nil { + panic(err) + } + to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") - data, _ := hex.DecodeString("18160ddd") // block := "0x28d9630" block := "latest" // block := "0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2" @@ -227,6 +241,7 @@ func main() { logger.Info("Waiting for message...") // TODO: max wait time + // TODO: accumulate signatures to reach quorum for { envelope, err := sub.Next(ctx) if err != nil { @@ -243,9 +258,25 @@ func main() { var isMatchingResponse bool switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: - // TODO: check if it's matching - logger.Info("response received", zap.Any("response", m.SignedQueryResponse)) - isMatchingResponse = true + logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) + response, err := common.UnmarshalQueryResponsePublication(m.SignedQueryResponse.QueryResponse) + if err != nil { + logger.Warn("failed to unmarshal response", zap.Error(err)) + break + } + if bytes.Equal(response.Request.QueryRequest, queryRequestBytes) && bytes.Equal(response.Request.Signature, sig) { + // TODO: verify response signature + isMatchingResponse = true + + result, err := wethAbi.Methods[methodName].Outputs.Unpack(response.Response.Result) + if err != nil { + logger.Warn("failed to unpack result", zap.Error(err)) + break + } + + resultStr := hexutil.Encode(response.Response.Result) + logger.Info("found matching response", zap.String("number", response.Response.Number.String()), zap.String("hash", response.Response.Hash.String()), zap.String("time", response.Response.Time.String()), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) + } default: continue } diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index e5b9fe5a92..2fbc340aa6 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -5,11 +5,11 @@ import ( "encoding/binary" "fmt" "math" + "math/big" "time" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/ethereum/go-ethereum/common" - eth_hexutil "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/wormhole-foundation/wormhole/sdk/vaa" "google.golang.org/protobuf/proto" @@ -18,9 +18,9 @@ import ( var queryResponsePrefix = []byte("query_response_0000000000000000000|") type EthCallQueryResponse struct { - Number *eth_hexutil.Big + Number *big.Int Hash common.Hash - Time eth_hexutil.Uint64 + Time time.Time Result []byte } @@ -29,6 +29,7 @@ type QueryResponsePublication struct { Response EthCallQueryResponse } +// Marshal serializes the binary representation of a query response func (msg *QueryResponsePublication) Marshal() ([]byte, error) { // TODO: copy request write checks to query module request handling // TODO: only receive the unmarshalled query request (see note in query.go) @@ -51,8 +52,11 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { switch req := queryRequest.Message.(type) { case *gossipv1.QueryRequest_EthCallQueryRequest: vaa.MustWrite(buf, binary.BigEndian, uint8(1)) - vaa.MustWrite(buf, binary.BigEndian, queryRequest.ChainId) // uint32 - vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 + if queryRequest.ChainId > math.MaxUint16 { + return nil, fmt.Errorf("invalid chain id: %d is out of bounds", queryRequest.ChainId) + } + vaa.MustWrite(buf, binary.BigEndian, uint16(queryRequest.ChainId)) + vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 if len(req.EthCallQueryRequest.To) != 20 { return nil, fmt.Errorf("invalid length for To contract") } @@ -71,12 +75,13 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { // Response // TODO: probably some kind of request/response pair validation - vaa.MustWrite(buf, binary.BigEndian, msg.Response.Number.ToInt().Uint64()) + // TODO: is uint64 safe? + vaa.MustWrite(buf, binary.BigEndian, msg.Response.Number.Uint64()) if len(msg.Response.Hash) != 32 { return nil, fmt.Errorf("invalid length for block hash") } buf.Write(msg.Response.Hash[:]) - vaa.MustWrite(buf, binary.BigEndian, uint32(time.Unix(int64(msg.Response.Time), 0).Unix())) + vaa.MustWrite(buf, binary.BigEndian, uint32(msg.Response.Time.Unix())) if len(msg.Response.Result) > math.MaxUint32 { return nil, fmt.Errorf("response data too long") } @@ -88,60 +93,130 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { } } -// TODO -// Unmarshal deserializes the binary representation of a VAA -// func UnmarshalMessagePublication(data []byte) (*MessagePublication, error) { -// if len(data) < minMsgLength { -// return nil, fmt.Errorf("message is too short") -// } - -// msg := &MessagePublication{} - -// reader := bytes.NewReader(data[:]) - -// txHash := common.Hash{} -// if n, err := reader.Read(txHash[:]); err != nil || n != 32 { -// return nil, fmt.Errorf("failed to read TxHash [%d]: %w", n, err) -// } -// msg.TxHash = txHash - -// unixSeconds := uint32(0) -// if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil { -// return nil, fmt.Errorf("failed to read timestamp: %w", err) -// } -// msg.Timestamp = time.Unix(int64(unixSeconds), 0) - -// if err := binary.Read(reader, binary.BigEndian, &msg.Nonce); err != nil { -// return nil, fmt.Errorf("failed to read nonce: %w", err) -// } - -// if err := binary.Read(reader, binary.BigEndian, &msg.Sequence); err != nil { -// return nil, fmt.Errorf("failed to read sequence: %w", err) -// } - -// if err := binary.Read(reader, binary.BigEndian, &msg.ConsistencyLevel); err != nil { -// return nil, fmt.Errorf("failed to read consistency level: %w", err) -// } - -// if err := binary.Read(reader, binary.BigEndian, &msg.EmitterChain); err != nil { -// return nil, fmt.Errorf("failed to read emitter chain: %w", err) -// } - -// emitterAddress := vaa.Address{} -// if n, err := reader.Read(emitterAddress[:]); err != nil || n != 32 { -// return nil, fmt.Errorf("failed to read emitter address [%d]: %w", n, err) -// } -// msg.EmitterAddress = emitterAddress - -// payload := make([]byte, reader.Len()) -// n, err := reader.Read(payload) -// if err != nil || n == 0 { -// return nil, fmt.Errorf("failed to read payload [%d]: %w", n, err) -// } -// msg.Payload = payload[:n] - -// return msg, nil -// } +// Unmarshal deserializes the binary representation of a query response +func UnmarshalQueryResponsePublication(data []byte) (*QueryResponsePublication, error) { + // if len(data) < minMsgLength { + // return nil, fmt.Errorf("message is too short") + // } + + msg := &QueryResponsePublication{} + + reader := bytes.NewReader(data[:]) + + // Request + requestChain := vaa.ChainID(0) + if err := binary.Read(reader, binary.BigEndian, &requestChain); err != nil { + return nil, fmt.Errorf("failed to read request chain: %w", err) + } + if requestChain != vaa.ChainIDUnset { + // TODO: support reading off-chain and on-chain requests + return nil, fmt.Errorf("unsupported request chain: %d", requestChain) + } + + signedQueryRequest := &gossipv1.SignedQueryRequest{} + signature := [65]byte{} + if n, err := reader.Read(signature[:]); err != nil || n != 65 { + return nil, fmt.Errorf("failed to read signature [%d]: %w", n, err) + } + signedQueryRequest.Signature = signature[:] + + requestType := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &requestType); err != nil { + return nil, fmt.Errorf("failed to read request chain: %w", err) + } + if requestType != 1 { + // TODO: support reading different types of request/response pairs + return nil, fmt.Errorf("unsupported request type: %d", requestType) + } + + queryRequest := &gossipv1.QueryRequest{} + queryChain := vaa.ChainID(0) + if err := binary.Read(reader, binary.BigEndian, &queryChain); err != nil { + return nil, fmt.Errorf("failed to read request chain: %w", err) + } + queryRequest.ChainId = uint32(queryChain) + + queryNonce := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &queryNonce); err != nil { + return nil, fmt.Errorf("failed to read request nonce: %w", err) + } + queryRequest.Nonce = queryNonce + + ethCallQueryRequest := &gossipv1.EthCallQueryRequest{} + + queryEthCallTo := [20]byte{} + if n, err := reader.Read(queryEthCallTo[:]); err != nil || n != 20 { + return nil, fmt.Errorf("failed to read call To [%d]: %w", n, err) + } + ethCallQueryRequest.To = queryEthCallTo[:] + + queryEthCallDataLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &queryEthCallDataLen); err != nil { + return nil, fmt.Errorf("failed to read call Data len: %w", err) + } + queryEthCallData := make([]byte, queryEthCallDataLen) + if n, err := reader.Read(queryEthCallData[:]); err != nil || n != int(queryEthCallDataLen) { + return nil, fmt.Errorf("failed to read call To [%d]: %w", n, err) + } + ethCallQueryRequest.Data = queryEthCallData[:] + + queryEthCallBlockLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &queryEthCallBlockLen); err != nil { + return nil, fmt.Errorf("failed to read call Data len: %w", err) + } + queryEthCallBlockBytes := make([]byte, queryEthCallBlockLen) + if n, err := reader.Read(queryEthCallBlockBytes[:]); err != nil || n != int(queryEthCallBlockLen) { + return nil, fmt.Errorf("failed to read call To [%d]: %w", n, err) + } + ethCallQueryRequest.Block = string(queryEthCallBlockBytes[:]) + + queryRequest.Message = &gossipv1.QueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: ethCallQueryRequest, + } + queryRequestBytes, err := proto.Marshal(queryRequest) + if err != nil { + return nil, err + } + signedQueryRequest.QueryRequest = queryRequestBytes + + msg.Request = signedQueryRequest + + // Response + queryResponse := EthCallQueryResponse{} + + responseNumber := uint64(0) + if err := binary.Read(reader, binary.BigEndian, &responseNumber); err != nil { + return nil, fmt.Errorf("failed to read response number: %w", err) + } + responseNumberBig := big.NewInt(0).SetUint64(responseNumber) + queryResponse.Number = responseNumberBig + + responseHash := common.Hash{} + if n, err := reader.Read(responseHash[:]); err != nil || n != 32 { + return nil, fmt.Errorf("failed to read response hash [%d]: %w", n, err) + } + queryResponse.Hash = responseHash + + unixSeconds := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &unixSeconds); err != nil { + return nil, fmt.Errorf("failed to read response timestamp: %w", err) + } + queryResponse.Time = time.Unix(int64(unixSeconds), 0) + + responseResultLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &responseResultLen); err != nil { + return nil, fmt.Errorf("failed to read response len: %w", err) + } + responseResult := make([]byte, responseResultLen) + if n, err := reader.Read(responseResult[:]); err != nil || n != int(responseResultLen) { + return nil, fmt.Errorf("failed to read result [%d]: %w", n, err) + } + queryResponse.Result = responseResult[:] + + msg.Response = queryResponse + + return msg, nil +} // Similar to sdk/vaa/structs.go, // In order to save space in the solana signature verification instruction, we hash twice so we only need to pass in diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 7776f5aa7c..dae3ee4211 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -677,9 +677,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { queryResponse := common.QueryResponsePublication{ Request: signedQueryRequest, Response: common.EthCallQueryResponse{ - Number: blockResult.Number, + Number: blockResult.Number.ToInt(), Hash: blockResult.Hash, - Time: blockResult.Time, + Time: time.Unix(int64(blockResult.Time), 0), Result: callResult, }, }