From 7e3276dc10fd49e0e8ed0b7d2e3d29d0468b57a8 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Wed, 17 May 2023 20:09:47 -0400 Subject: [PATCH 01/37] WIP: CCQ --- Dockerfile.const | 11 +- devnet/node.yaml | 7 +- node/cmd/guardiand/node.go | 2 +- node/cmd/guardiand/query.go | 84 +++ node/cmd/spy/spy.go | 16 + node/hack/query/dev.guardian.key | 8 + node/hack/query/send_req.go | 284 +++++++++++ node/pkg/common/queryReqSendC.go | 16 + node/pkg/p2p/p2p.go | 5 + node/pkg/proto/gossip/v1/gossip.pb.go | 708 ++++++++++++++++++-------- node/pkg/watchers/evm/watcher.go | 84 +++ proto/gossip/v1/gossip.proto | 26 + 12 files changed, 1041 insertions(+), 210 deletions(-) create mode 100644 node/cmd/guardiand/query.go create mode 100644 node/hack/query/dev.guardian.key create mode 100644 node/hack/query/send_req.go create mode 100644 node/pkg/common/queryReqSendC.go diff --git a/Dockerfile.const b/Dockerfile.const index d6a9616aa9..47b0926d68 100644 --- a/Dockerfile.const +++ b/Dockerfile.const @@ -2,13 +2,18 @@ FROM cli-gen as cli-export FROM node:18-alpine@sha256:44aaf1ccc80eaed6572a0f2ef7d6b5a2982d54481e4255480041ac92221e2f11 as const-build -# fetch scripts/guardian-set-init.sh deps -RUN apk update && apk add bash g++ make python3 curl jq findutils - # Support additional root CAs COPY README.md cert.pem* /certs/ # Alpine RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/cert.pem; fi +# Node +ENV NODE_EXTRA_CA_CERTS=/certs/cert.pem +ENV NODE_OPTIONS=--use-openssl-ca +# npm +RUN if [ -e /certs/cert.pem ]; then npm config set cafile /certs/cert.pem; fi + +# fetch scripts/guardian-set-init.sh deps +RUN apk update && apk add bash g++ make python3 curl jq findutils # Copy and link CLI COPY --from=cli-export clients/js /cli diff --git a/devnet/node.yaml b/devnet/node.yaml index 475f043dab..77bc9f6b5b 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -73,7 +73,9 @@ spec: # - --bscRPC # - ws://eth-devnet2:8545 - --polygonRPC - - ws://eth-devnet:8545 + - wss://ws-matic-mainnet.chainstacklabs.com + - --polygonContract + - "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7" - --avalancheRPC - ws://eth-devnet:8545 - --auroraRPC @@ -160,7 +162,8 @@ spec: - /tmp/data - --publicRpcLogDetail - "full" - # - --chainGovernorEnabled=true + # - --chainGovernorEnabled=true + # - --logLevel=debug securityContext: capabilities: add: diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index e82b42ae6b..12ba4a8473 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -473,7 +473,7 @@ func runNode(cmd *cobra.Command, args []string) { // Deterministic ganache ETH devnet address. *ethContract = unsafeDevModeEvmContractAddress(*ethContract) *bscContract = unsafeDevModeEvmContractAddress(*bscContract) - *polygonContract = unsafeDevModeEvmContractAddress(*polygonContract) + // *polygonContract = unsafeDevModeEvmContractAddress(*polygonContract) *avalancheContract = unsafeDevModeEvmContractAddress(*avalancheContract) *oasisContract = unsafeDevModeEvmContractAddress(*oasisContract) *auroraContract = unsafeDevModeEvmContractAddress(*auroraContract) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go new file mode 100644 index 0000000000..25b851ae64 --- /dev/null +++ b/node/cmd/guardiand/query.go @@ -0,0 +1,84 @@ +package guardiand + +import ( + "context" + + "github.com/benbjohnson/clock" + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +// TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 +var queryRequestPrefix = []byte("query_request_00000000000000000000|") + +func queryRequestDigest(b []byte) common.Hash { + return ethcrypto.Keccak256Hash(append(queryRequestPrefix, b...)) +} + +var allowedRequestor = common.BytesToAddress(common.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")) + +// Multiplex observation requests to the appropriate chain +func handleQueryRequests( + ctx context.Context, + clock clock.Clock, + logger *zap.Logger, + signedQueryReqC <-chan *gossipv1.SignedQueryRequest, + chainQueryReqC map[vaa.ChainID]chan *gossipv1.QueryRequest, +) { + qLogger := logger.With(zap.String("component", "queryHandler")) + for { + select { + case <-ctx.Done(): + return + case signedQueryRequest := <-signedQueryReqC: + // requestor validation happens here + // request type validation is currently handled by the watcher + // in the future, it may be worthwhile to catch certain types of + // invalid requests here for tracking purposes + requestorAddr := common.BytesToAddress(signedQueryRequest.RequestorAddr) + if requestorAddr != allowedRequestor { + qLogger.Error("invalid requestor", zap.String("requestor", requestorAddr.Hex())) + continue + } + + digest := queryRequestDigest(signedQueryRequest.QueryRequest) + + signerBytes, err := ethcrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) + if err != nil { + qLogger.Error("failed to recover public key", zap.String("requestor", requestorAddr.Hex())) + continue + } + + signerAddress := common.BytesToAddress(ethcrypto.Keccak256(signerBytes[1:])[12:]) + if signerAddress != requestorAddr { + qLogger.Error("requestor signer mismatch", zap.String("requestor", requestorAddr.Hex()), zap.String("signer", signerAddress.Hex())) + continue + } + + var queryRequest gossipv1.QueryRequest + err = proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) + if err != nil { + qLogger.Error("received invalid message", + zap.String("requestor", requestorAddr.Hex())) + continue + } + + if channel, ok := chainQueryReqC[vaa.ChainID(queryRequest.ChainId)]; ok { + select { + // TODO: is pointer fine here? + case channel <- &queryRequest: + default: + qLogger.Warn("failed to send query request to watcher", + zap.Uint16("chain_id", uint16(queryRequest.ChainId))) + } + } else { + qLogger.Error("unknown chain ID for query request", + zap.Uint16("chain_id", uint16(queryRequest.ChainId))) + } + } + } +} diff --git a/node/cmd/spy/spy.go b/node/cmd/spy/spy.go index a38141f233..6da7216ac8 100644 --- a/node/cmd/spy/spy.go +++ b/node/cmd/spy/spy.go @@ -271,6 +271,9 @@ func runSpy(cmd *cobra.Command, args []string) { // Inbound observation requests obsvReqC := make(chan *gossipv1.ObservationRequest, 1024) + // Inbound observation requests + queryReqC := make(chan *gossipv1.SignedQueryRequest, 50) + // Inbound signed VAAs signedInC := make(chan *gossipv1.SignedVAAWithQuorum, 1024) @@ -307,6 +310,18 @@ func runSpy(cmd *cobra.Command, args []string) { } }() + // Ignore query requests + // Note: without this, the whole program hangs on query requests + go func() { + for { + select { + case <-rootCtx.Done(): + return + case <-queryReqC: + } + } + }() + // Log signed VAAs go func() { for { @@ -356,6 +371,7 @@ func runSpy(cmd *cobra.Command, args []string) { components, nil, // ibc feature string false, // gateway relayer enabled + queryReqC, )); err != nil { return err } diff --git a/node/hack/query/dev.guardian.key b/node/hack/query/dev.guardian.key new file mode 100644 index 0000000000..0be9e035bd --- /dev/null +++ b/node/hack/query/dev.guardian.key @@ -0,0 +1,8 @@ +-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY----- +PublicKey: 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe +Description: auto-generated deterministic devnet key + +CiDPsSMDoZzeWAu03XcWObDSa8aDU2RVcajP9RarLuEToBAB +=VN/A +-----END WORMHOLE GUARDIAN PRIVATE KEY----- + diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go new file mode 100644 index 0000000000..0bcb0e977f --- /dev/null +++ b/node/hack/query/send_req.go @@ -0,0 +1,284 @@ +// This tool can be used to send various queries to the p2p gossip network. +// It is meant for testing purposes only. + +package main + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "io" + "os" + "strings" + + "github.com/certusone/wormhole/node/pkg/common" + "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" + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/libp2p/go-libp2p" + dht "github.com/libp2p/go-libp2p-kad-dht" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/libp2p/go-libp2p/core/routing" + libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" + libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "github.com/multiformats/go-multiaddr" + "go.uber.org/zap" + "golang.org/x/crypto/openpgp/armor" //nolint + "google.golang.org/protobuf/proto" +) + +var queryRequestPrefix = []byte("query_request_00000000000000000000|") + +func queryRequestDigest(b []byte) ethCommon.Hash { + return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) +} + +// this script has to be run inside kubernetes since it relies on UDP +// https://github.com/kubernetes/kubernetes/issues/47862 +// kubectl --namespace=wormhole exec -it spy-0 -- sh -c "cd node/hack/query/ && go run send_req.go" + +func main() { + + p2pNetworkID := "/wormhole/dev" + var p2pPort uint = 8998 // don't collide with spy so we can run from the same container in tilt + p2pBootstrap := "/dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" + nodeKeyPath := "/tmp/querier.key" // don't use node key so we get a new address + + ctx := context.Background() + logger, _ := zap.NewDevelopment() + + signingKeyPath := string("./dev.guardian.key") + + logger.Info("Loading signing key", zap.String("signingKeyPath", signingKeyPath)) + sk, err := loadGuardianKey(signingKeyPath) + if err != nil { + logger.Fatal("failed to load guardian key", zap.Error(err)) + } + logger.Info("Signing key loaded", zap.String("publicKey", ethCrypto.PubkeyToAddress(sk.PublicKey).Hex())) + + // Load p2p private key + var priv crypto.PrivKey + priv, err = common.GetOrCreateNodeKey(logger, nodeKeyPath) + if err != nil { + logger.Fatal("Failed to load node key", zap.Error(err)) + } + + // Manual p2p setup + components := p2p.DefaultComponents() + components.Port = p2pPort + bootstrapPeers := p2pBootstrap + networkID := p2pNetworkID + h, err := libp2p.New( + // Use the keypair we generated + libp2p.Identity(priv), + + // Multiple listen addresses + libp2p.ListenAddrStrings( + components.ListeningAddresses()..., + ), + + // Enable TLS security as the only security protocol. + libp2p.Security(libp2ptls.ID, libp2ptls.New), + + // Enable QUIC transport as the only transport. + libp2p.Transport(libp2pquic.NewTransport), + + // Let's prevent our peer from having too many + // connections by attaching a connection manager. + libp2p.ConnectionManager(components.ConnMgr), + + // Let this host use the DHT to find other hosts + libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { + logger.Info("Connecting to bootstrap peers", zap.String("bootstrap_peers", bootstrapPeers)) + bootstrappers := make([]peer.AddrInfo, 0) + for _, addr := range strings.Split(bootstrapPeers, ",") { + if addr == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + if pi.ID == h.ID() { + logger.Info("We're a bootstrap node") + continue + } + bootstrappers = append(bootstrappers, *pi) + } + // TODO(leo): Persistent data store (i.e. address book) + idht, err := dht.New(ctx, h, dht.Mode(dht.ModeServer), + // This intentionally makes us incompatible with the global IPFS DHT + dht.ProtocolPrefix(protocol.ID("/"+networkID)), + dht.BootstrapPeers(bootstrappers...), + ) + return idht, err + }), + ) + + if err != nil { + panic(err) + } + + topic := fmt.Sprintf("%s/%s", networkID, "broadcast") + + logger.Info("Subscribing pubsub topic", zap.String("topic", topic)) + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + panic(err) + } + + th, err := ps.Join(topic) + if err != nil { + logger.Panic("failed to join topic", zap.Error(err)) + } + + sub, err := th.Subscribe() + if err != nil { + logger.Panic("failed to subscribe topic", zap.Error(err)) + } + + logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), + zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) + + to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") + data, _ := hex.DecodeString("18160ddd") + // block := "0x28d9630" + // block := "latest" + block := "0x2e0d2bc116d77308db4e76eb906f6c168767ed00ad62cd2e2a31c61744c506e6" + callRequest := &gossipv1.EthCallQueryRequest{ + To: to, + Data: data, + Block: block, + } + queryRequest := &gossipv1.QueryRequest{ + ChainId: 5, + Nonce: 0, + Message: &gossipv1.QueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest}} + + queryRequestBytes, err := proto.Marshal(queryRequest) + if err != nil { + panic(err) + } + + // Sign the query request using our private key. + digest := queryRequestDigest(queryRequestBytes) + sig, err := ethCrypto.Sign(digest.Bytes(), sk) + if err != nil { + panic(err) + } + + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig, + RequestorAddr: ethCrypto.PubkeyToAddress(sk.PublicKey).Bytes(), + } + + msg := gossipv1.GossipMessage{ + Message: &gossipv1.GossipMessage_SignedQueryRequest{ + SignedQueryRequest: signedQueryRequest, + }, + } + + b, err := proto.Marshal(&msg) + if err != nil { + panic(err) + } + + // do something to wait for peers, this waits to receive a message + logger.Info("Waiting for a message...") + for { + envelope, err := sub.Next(ctx) + if err != nil { + logger.Panic("failed to receive pubsub message", zap.Error(err)) + } + var msg gossipv1.GossipMessage + err = proto.Unmarshal(envelope.Data, &msg) + if err != nil { + logger.Info("received invalid message", + zap.Binary("data", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + continue + } + logger.Info("Received a message!") + break + } + + err = th.Publish(ctx, b) + if err != nil { + panic(err) + } + + logger.Info("Waiting for message...") + for { + envelope, err := sub.Next(ctx) + if err != nil { + logger.Panic("failed to receive pubsub message", zap.Error(err)) + } + var msg gossipv1.GossipMessage + err = proto.Unmarshal(envelope.Data, &msg) + if err != nil { + logger.Info("received invalid message", + zap.Binary("data", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + continue + } + // TODO: actually wait for the corresponding response + logger.Info("received message") + break + } + + logger.Info("Success! All tests passed!") +} + +const ( + GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" +) + +// loadGuardianKey loads a serialized guardian key from disk. +func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { + f, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + p, err := armor.Decode(f) + if err != nil { + return nil, fmt.Errorf("failed to read armored file: %w", err) + } + + if p.Type != GuardianKeyArmoredBlock { + return nil, fmt.Errorf("invalid block type: %s", p.Type) + } + + b, err := io.ReadAll(p.Body) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var m nodev1.GuardianKey + err = proto.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) + } + + gk, err := ethCrypto.ToECDSA(m.Data) + if err != nil { + return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) + } + + return gk, nil +} diff --git a/node/pkg/common/queryReqSendC.go b/node/pkg/common/queryReqSendC.go new file mode 100644 index 0000000000..9acfedeb5c --- /dev/null +++ b/node/pkg/common/queryReqSendC.go @@ -0,0 +1,16 @@ +package common + +import ( + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" +) + +const QueryReqChannelSize = 50 + +func PostQueryRequest(obsvReqSendC chan<- *gossipv1.QueryRequest, req *gossipv1.QueryRequest) error { + select { + case obsvReqSendC <- req: + return nil + default: + return ErrChanFull + } +} diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index e281a3f6d1..25141912ba 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -210,6 +210,7 @@ func Run( components *Components, ibcFeaturesFunc func() string, gatewayRelayerEnabled bool, + signedQueryReqC chan<- *gossipv1.SignedQueryRequest, ) func(ctx context.Context) error { if components == nil { components = DefaultComponents() @@ -654,6 +655,10 @@ func Run( if signedGovSt != nil { signedGovSt <- m.SignedChainGovernorStatus } + case *gossipv1.GossipMessage_SignedQueryRequest: + if signedQueryReqC != nil { + signedQueryReqC <- m.SignedQueryRequest + } default: p2pMessagesReceived.WithLabelValues("unknown").Inc() logger.Warn("received unknown message type (running outdated software?)", diff --git a/node/pkg/proto/gossip/v1/gossip.pb.go b/node/pkg/proto/gossip/v1/gossip.pb.go index 291a25c3ac..4159b13a7f 100644 --- a/node/pkg/proto/gossip/v1/gossip.pb.go +++ b/node/pkg/proto/gossip/v1/gossip.pb.go @@ -35,6 +35,7 @@ type GossipMessage struct { // *GossipMessage_SignedBatchVaaWithQuorum // *GossipMessage_SignedChainGovernorConfig // *GossipMessage_SignedChainGovernorStatus + // *GossipMessage_SignedQueryRequest Message isGossipMessage_Message `protobuf_oneof:"message"` } @@ -133,6 +134,13 @@ func (x *GossipMessage) GetSignedChainGovernorStatus() *SignedChainGovernorStatu return nil } +func (x *GossipMessage) GetSignedQueryRequest() *SignedQueryRequest { + if x, ok := x.GetMessage().(*GossipMessage_SignedQueryRequest); ok { + return x.SignedQueryRequest + } + return nil +} + type isGossipMessage_Message interface { isGossipMessage_Message() } @@ -169,6 +177,10 @@ type GossipMessage_SignedChainGovernorStatus struct { SignedChainGovernorStatus *SignedChainGovernorStatus `protobuf:"bytes,9,opt,name=signed_chain_governor_status,json=signedChainGovernorStatus,proto3,oneof"` } +type GossipMessage_SignedQueryRequest struct { + SignedQueryRequest *SignedQueryRequest `protobuf:"bytes,10,opt,name=signed_query_request,json=signedQueryRequest,proto3,oneof"` +} + func (*GossipMessage_SignedObservation) isGossipMessage_Message() {} func (*GossipMessage_SignedHeartbeat) isGossipMessage_Message() {} @@ -185,6 +197,8 @@ func (*GossipMessage_SignedChainGovernorConfig) isGossipMessage_Message() {} func (*GossipMessage_SignedChainGovernorStatus) isGossipMessage_Message() {} +func (*GossipMessage_SignedQueryRequest) isGossipMessage_Message() {} + type SignedHeartbeat struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1127,6 +1141,218 @@ func (x *ChainGovernorStatus) GetChains() []*ChainGovernorStatus_Chain { return nil } +type SignedQueryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Serialized QueryRequest message. + QueryRequest []byte `protobuf:"bytes,1,opt,name=query_request,json=queryRequest,proto3" json:"query_request,omitempty"` + // ECDSA signature using the requestor's public key. + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + // Requestor address that signed this payload (truncated Eth address). + RequestorAddr []byte `protobuf:"bytes,3,opt,name=requestor_addr,json=requestorAddr,proto3" json:"requestor_addr,omitempty"` +} + +func (x *SignedQueryRequest) Reset() { + *x = SignedQueryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignedQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignedQueryRequest) ProtoMessage() {} + +func (x *SignedQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignedQueryRequest.ProtoReflect.Descriptor instead. +func (*SignedQueryRequest) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{13} +} + +func (x *SignedQueryRequest) GetQueryRequest() []byte { + if x != nil { + return x.QueryRequest + } + return nil +} + +func (x *SignedQueryRequest) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *SignedQueryRequest) GetRequestorAddr() []byte { + if x != nil { + return x.RequestorAddr + } + return nil +} + +type QueryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChainId uint32 `protobuf:"varint,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + Nonce uint32 `protobuf:"varint,2,opt,name=nonce,proto3" json:"nonce,omitempty"` + // Types that are assignable to Message: + // + // *QueryRequest_EthCallQueryRequest + Message isQueryRequest_Message `protobuf_oneof:"message"` +} + +func (x *QueryRequest) Reset() { + *x = QueryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRequest) ProtoMessage() {} + +func (x *QueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. +func (*QueryRequest) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{14} +} + +func (x *QueryRequest) GetChainId() uint32 { + if x != nil { + return x.ChainId + } + return 0 +} + +func (x *QueryRequest) GetNonce() uint32 { + if x != nil { + return x.Nonce + } + return 0 +} + +func (m *QueryRequest) GetMessage() isQueryRequest_Message { + if m != nil { + return m.Message + } + return nil +} + +func (x *QueryRequest) GetEthCallQueryRequest() *EthCallQueryRequest { + if x, ok := x.GetMessage().(*QueryRequest_EthCallQueryRequest); ok { + return x.EthCallQueryRequest + } + return nil +} + +type isQueryRequest_Message interface { + isQueryRequest_Message() +} + +type QueryRequest_EthCallQueryRequest struct { + EthCallQueryRequest *EthCallQueryRequest `protobuf:"bytes,3,opt,name=eth_call_query_request,json=ethCallQueryRequest,proto3,oneof"` +} + +func (*QueryRequest_EthCallQueryRequest) isQueryRequest_Message() {} + +type EthCallQueryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + To []byte `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Block string `protobuf:"bytes,3,opt,name=block,proto3" json:"block,omitempty"` +} + +func (x *EthCallQueryRequest) Reset() { + *x = EthCallQueryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EthCallQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EthCallQueryRequest) ProtoMessage() {} + +func (x *EthCallQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EthCallQueryRequest.ProtoReflect.Descriptor instead. +func (*EthCallQueryRequest) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{15} +} + +func (x *EthCallQueryRequest) GetTo() []byte { + if x != nil { + return x.To + } + return nil +} + +func (x *EthCallQueryRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *EthCallQueryRequest) GetBlock() string { + if x != nil { + return x.Block + } + return "" +} + type Heartbeat_Network struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1145,7 +1371,7 @@ type Heartbeat_Network struct { func (x *Heartbeat_Network) Reset() { *x = Heartbeat_Network{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[13] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1158,7 +1384,7 @@ func (x *Heartbeat_Network) String() string { func (*Heartbeat_Network) ProtoMessage() {} func (x *Heartbeat_Network) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[13] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1215,7 +1441,7 @@ type ChainGovernorConfig_Chain struct { func (x *ChainGovernorConfig_Chain) Reset() { *x = ChainGovernorConfig_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[14] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1228,7 +1454,7 @@ func (x *ChainGovernorConfig_Chain) String() string { func (*ChainGovernorConfig_Chain) ProtoMessage() {} func (x *ChainGovernorConfig_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[14] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1278,7 +1504,7 @@ type ChainGovernorConfig_Token struct { func (x *ChainGovernorConfig_Token) Reset() { *x = ChainGovernorConfig_Token{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1291,7 +1517,7 @@ func (x *ChainGovernorConfig_Token) String() string { func (*ChainGovernorConfig_Token) ProtoMessage() {} func (x *ChainGovernorConfig_Token) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1342,7 +1568,7 @@ type ChainGovernorStatus_EnqueuedVAA struct { func (x *ChainGovernorStatus_EnqueuedVAA) Reset() { *x = ChainGovernorStatus_EnqueuedVAA{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1355,7 +1581,7 @@ func (x *ChainGovernorStatus_EnqueuedVAA) String() string { func (*ChainGovernorStatus_EnqueuedVAA) ProtoMessage() {} func (x *ChainGovernorStatus_EnqueuedVAA) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1412,7 +1638,7 @@ type ChainGovernorStatus_Emitter struct { func (x *ChainGovernorStatus_Emitter) Reset() { *x = ChainGovernorStatus_Emitter{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1425,7 +1651,7 @@ func (x *ChainGovernorStatus_Emitter) String() string { func (*ChainGovernorStatus_Emitter) ProtoMessage() {} func (x *ChainGovernorStatus_Emitter) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1475,7 +1701,7 @@ type ChainGovernorStatus_Chain struct { func (x *ChainGovernorStatus_Chain) Reset() { *x = ChainGovernorStatus_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1488,7 +1714,7 @@ func (x *ChainGovernorStatus_Chain) String() string { func (*ChainGovernorStatus_Chain) ProtoMessage() {} func (x *ChainGovernorStatus_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1530,7 +1756,7 @@ var File_gossip_v1_gossip_proto protoreflect.FileDescriptor var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x0a, 0x16, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, - 0x2e, 0x76, 0x31, 0x22, 0x86, 0x06, 0x0a, 0x0d, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x4d, 0x65, + 0x2e, 0x76, 0x31, 0x22, 0xd9, 0x06, 0x0a, 0x0d, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, @@ -1578,180 +1804,209 @@ var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x19, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x72, 0x0a, 0x0f, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, - 0x22, 0xbb, 0x03, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x12, 0x38, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, - 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x0e, - 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, - 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, - 0x1e, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x32, 0x70, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x1a, - 0x7d, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, - 0x68, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1f, 0x0a, - 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x91, - 0x01, 0x0a, 0x11, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, + 0x73, 0x12, 0x51, 0x0a, 0x14, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x72, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, + 0x61, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, + 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, + 0x64, 0x64, 0x72, 0x22, 0xbb, 0x03, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x38, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, + 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x2e, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, + 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, + 0x25, 0x0a, 0x0e, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x32, 0x70, 0x4e, 0x6f, 0x64, 0x65, + 0x49, 0x64, 0x1a, 0x7d, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, + 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, + 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, + 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, + 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x49, 0x64, 0x22, 0x27, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x56, + 0x41, 0x41, 0x57, 0x69, 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x10, 0x0a, 0x03, + 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x61, 0x22, 0x8e, + 0x01, 0x0a, 0x18, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x13, 0x6f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, + 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, + 0x48, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, + 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x22, 0xbf, 0x01, 0x0a, 0x16, 0x53, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x49, 0x64, 0x22, 0x27, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x56, 0x41, 0x41, 0x57, - 0x69, 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x61, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x61, 0x22, 0x8e, 0x01, 0x0a, 0x18, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x13, 0x6f, 0x62, 0x73, 0x65, - 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, - 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, - 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0x48, 0x0a, 0x12, - 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, - 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, - 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x22, 0xbf, 0x01, 0x0a, 0x16, 0x53, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x0a, - 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x18, 0x53, 0x69, 0x67, - 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x56, 0x41, 0x41, 0x57, 0x69, 0x74, 0x68, 0x51, - 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x76, - 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x56, - 0x61, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x13, 0x0a, - 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, - 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x74, 0x63, - 0x68, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, - 0x68, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, - 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, - 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, - 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0xd1, 0x03, 0x0a, 0x13, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, + 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, + 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x18, + 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x56, 0x41, 0x41, 0x57, 0x69, + 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, + 0x68, 0x5f, 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, + 0x63, 0x68, 0x56, 0x61, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, + 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, + 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, + 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, - 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x06, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x1a, 0x7b, 0x0a, 0x05, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, - 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x69, - 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x30, 0x0a, 0x14, 0x62, 0x69, 0x67, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x62, - 0x69, 0x67, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x69, 0x7a, - 0x65, 0x1a, 0x6c, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x6f, 0x72, - 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x22, - 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, - 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, - 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, - 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0x98, 0x05, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, + 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, + 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0xd1, + 0x03, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, + 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, + 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x68, 0x61, 0x69, + 0x6e, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, + 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, + 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x1a, 0x7b, 0x0a, 0x05, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6e, + 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x62, 0x69, 0x67, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x12, 0x62, 0x69, 0x67, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x69, 0x7a, 0x65, 0x1a, 0x6c, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x26, 0x0a, + 0x0f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x43, 0x68, + 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, + 0x63, 0x65, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x73, 0x1a, 0x8c, 0x01, 0x0a, 0x0b, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, - 0x41, 0x41, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x21, - 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, - 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, - 0x68, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x12, 0x27, 0x0a, - 0x0f, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, - 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x11, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, - 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x12, 0x4f, 0x0a, 0x0d, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, - 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, - 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6e, - 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x41, 0x41, 0x52, 0x0c, 0x65, 0x6e, 0x71, 0x75, 0x65, - 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x1a, 0xa8, 0x01, 0x0a, 0x05, 0x43, 0x68, 0x61, 0x69, - 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x1c, - 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x42, - 0x0a, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, + 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, + 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0x98, 0x05, 0x0a, 0x13, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x8c, 0x01, 0x0a, 0x0b, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, + 0x65, 0x64, 0x56, 0x41, 0x41, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, + 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, + 0x48, 0x61, 0x73, 0x68, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, + 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x6d, 0x69, 0x74, 0x74, + 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x45, 0x6e, 0x71, + 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x12, 0x4f, 0x0a, 0x0d, 0x65, 0x6e, 0x71, + 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x52, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, - 0x72, 0x73, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, 0x68, - 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, 0x73, - 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x41, 0x41, 0x52, 0x0c, 0x65, 0x6e, + 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x1a, 0xa8, 0x01, 0x0a, 0x05, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, + 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x76, 0x61, + 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, + 0x6c, 0x12, 0x42, 0x0a, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x52, 0x08, 0x65, 0x6d, 0x69, + 0x74, 0x74, 0x65, 0x72, 0x73, 0x22, 0x7e, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x6f, + 0x72, 0x41, 0x64, 0x64, 0x72, 0x22, 0xa1, 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, + 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, + 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, + 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, + 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, + 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, + 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1766,7 +2021,7 @@ func file_gossip_v1_gossip_proto_rawDescGZIP() []byte { return file_gossip_v1_gossip_proto_rawDescData } -var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*GossipMessage)(nil), // 0: gossip.v1.GossipMessage (*SignedHeartbeat)(nil), // 1: gossip.v1.SignedHeartbeat @@ -1781,12 +2036,15 @@ var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*ChainGovernorConfig)(nil), // 10: gossip.v1.ChainGovernorConfig (*SignedChainGovernorStatus)(nil), // 11: gossip.v1.SignedChainGovernorStatus (*ChainGovernorStatus)(nil), // 12: gossip.v1.ChainGovernorStatus - (*Heartbeat_Network)(nil), // 13: gossip.v1.Heartbeat.Network - (*ChainGovernorConfig_Chain)(nil), // 14: gossip.v1.ChainGovernorConfig.Chain - (*ChainGovernorConfig_Token)(nil), // 15: gossip.v1.ChainGovernorConfig.Token - (*ChainGovernorStatus_EnqueuedVAA)(nil), // 16: gossip.v1.ChainGovernorStatus.EnqueuedVAA - (*ChainGovernorStatus_Emitter)(nil), // 17: gossip.v1.ChainGovernorStatus.Emitter - (*ChainGovernorStatus_Chain)(nil), // 18: gossip.v1.ChainGovernorStatus.Chain + (*SignedQueryRequest)(nil), // 13: gossip.v1.SignedQueryRequest + (*QueryRequest)(nil), // 14: gossip.v1.QueryRequest + (*EthCallQueryRequest)(nil), // 15: gossip.v1.EthCallQueryRequest + (*Heartbeat_Network)(nil), // 16: gossip.v1.Heartbeat.Network + (*ChainGovernorConfig_Chain)(nil), // 17: gossip.v1.ChainGovernorConfig.Chain + (*ChainGovernorConfig_Token)(nil), // 18: gossip.v1.ChainGovernorConfig.Token + (*ChainGovernorStatus_EnqueuedVAA)(nil), // 19: gossip.v1.ChainGovernorStatus.EnqueuedVAA + (*ChainGovernorStatus_Emitter)(nil), // 20: gossip.v1.ChainGovernorStatus.Emitter + (*ChainGovernorStatus_Chain)(nil), // 21: gossip.v1.ChainGovernorStatus.Chain } var file_gossip_v1_gossip_proto_depIdxs = []int32{ 3, // 0: gossip.v1.GossipMessage.signed_observation:type_name -> gossip.v1.SignedObservation @@ -1797,17 +2055,19 @@ var file_gossip_v1_gossip_proto_depIdxs = []int32{ 8, // 5: gossip.v1.GossipMessage.signed_batch_vaa_with_quorum:type_name -> gossip.v1.SignedBatchVAAWithQuorum 9, // 6: gossip.v1.GossipMessage.signed_chain_governor_config:type_name -> gossip.v1.SignedChainGovernorConfig 11, // 7: gossip.v1.GossipMessage.signed_chain_governor_status:type_name -> gossip.v1.SignedChainGovernorStatus - 13, // 8: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network - 14, // 9: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain - 15, // 10: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token - 18, // 11: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain - 16, // 12: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA - 17, // 13: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter - 14, // [14:14] is the sub-list for method output_type - 14, // [14:14] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 13, // 8: gossip.v1.GossipMessage.signed_query_request:type_name -> gossip.v1.SignedQueryRequest + 16, // 9: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network + 17, // 10: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain + 18, // 11: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token + 21, // 12: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain + 15, // 13: gossip.v1.QueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest + 19, // 14: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA + 20, // 15: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_gossip_v1_gossip_proto_init() } @@ -1973,7 +2233,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Heartbeat_Network); i { + switch v := v.(*SignedQueryRequest); i { case 0: return &v.state case 1: @@ -1985,7 +2245,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Chain); i { + switch v := v.(*QueryRequest); i { case 0: return &v.state case 1: @@ -1997,7 +2257,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Token); i { + switch v := v.(*EthCallQueryRequest); i { case 0: return &v.state case 1: @@ -2009,7 +2269,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { + switch v := v.(*Heartbeat_Network); i { case 0: return &v.state case 1: @@ -2021,7 +2281,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_Emitter); i { + switch v := v.(*ChainGovernorConfig_Chain); i { case 0: return &v.state case 1: @@ -2033,6 +2293,42 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChainGovernorConfig_Token); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gossip_v1_gossip_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gossip_v1_gossip_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChainGovernorStatus_Emitter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gossip_v1_gossip_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_Chain); i { case 0: return &v.state @@ -2054,6 +2350,10 @@ func file_gossip_v1_gossip_proto_init() { (*GossipMessage_SignedBatchVaaWithQuorum)(nil), (*GossipMessage_SignedChainGovernorConfig)(nil), (*GossipMessage_SignedChainGovernorStatus)(nil), + (*GossipMessage_SignedQueryRequest)(nil), + } + file_gossip_v1_gossip_proto_msgTypes[14].OneofWrappers = []interface{}{ + (*QueryRequest_EthCallQueryRequest)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2061,7 +2361,7 @@ func file_gossip_v1_gossip_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gossip_v1_gossip_proto_rawDesc, NumEnums: 0, - NumMessages: 19, + NumMessages: 22, NumExtensions: 0, NumServices: 0, }, diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 0cbc15996f..39864e415e 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -94,6 +94,10 @@ type ( // include requests for our chainID. obsvReqC <-chan *gossipv1.ObservationRequest + // Incoming query requests from the network. Pre-filtered to only + // include requests for our chainID. + queryReqC <-chan *gossipv1.QueryRequest + pending map[pendingKey]*pendingMessage pendingMu sync.Mutex @@ -143,6 +147,7 @@ func NewEthWatcher( msgC chan<- *common.MessagePublication, setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, + queryReqC <-chan *gossipv1.QueryRequest, unsafeDevMode bool, ) *Watcher { @@ -157,6 +162,7 @@ func NewEthWatcher( msgC: msgC, setC: setC, obsvReqC: obsvReqC, + queryReqC: queryReqC, pending: map[pendingKey]*pendingMessage{}, unsafeDevMode: unsafeDevMode, } @@ -515,6 +521,84 @@ func (w *Watcher) Run(parentCtx context.Context) error { } }) + common.RunWithScissors(ctx, errC, "evm_fetch_query_req", func(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case queryRequest := <-w.queryReqC: + // This can't happen unless there is a programming error - the caller + // is expected to send us only requests for our chainID. + if vaa.ChainID(queryRequest.ChainId) != w.chainID { + panic("invalid chain ID") + } + + switch req := queryRequest.Message.(type) { + case *gossipv1.QueryRequest_EthCallQueryRequest: + to := eth_common.BytesToAddress(req.EthCallQueryRequest.To) + data := eth_hexutil.Encode(req.EthCallQueryRequest.Data) + block := req.EthCallQueryRequest.Block + logger.Info("received query request", + zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + + timeout, cancel := context.WithTimeout(ctx, 5*time.Second) + // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 + arg := map[string]interface{}{ + "to": to, + "data": data, + } + var blockArg interface{} + // TODO: try making these error and see what happens + // 1. 66 chars but not 0x hex + // 2. 64 chars but not hex + // 3. bad blocks + // 4. bad 0x lengths + // 5. strings that aren't "latest", "safe", "finalized" + // 6. "safe" on a chain that doesn't support safe + // etc? + // I would expect this to trip within this scissor (if at all) but maybe this should get more defensive + if (len(block) == 66 || len(block) == 64) { + // looks like a hash which requires the object parameter + // https://docs.alchemy.com/reference/eth-call + hash := eth_common.HexToHash(block) + blockArg = rpc.BlockNumberOrHash{ + BlockHash: &hash, + } + } else { + blockArg = block + } + var result string + err := w.ethConn.RawCallContext(timeout, &result, "eth_call", arg, blockArg) + cancel() + + if err != nil { + logger.Error("failed to process query request", + zap.Error(err), zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + continue + } + + // TODO: error on "0x" response + + logger.Info("query result", + zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block), + zap.String("result", result)) + default: + logger.Warn("received unsupported request type", + zap.Any("payload", queryRequest.Message)) + } + } + } + }) + common.RunWithScissors(ctx, errC, "evm_fetch_messages", func(ctx context.Context) error { for { select { diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index 19d8a4ac83..0e9841721f 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -14,6 +14,7 @@ message GossipMessage { SignedBatchVAAWithQuorum signed_batch_vaa_with_quorum = 7; SignedChainGovernorConfig signed_chain_governor_config = 8; SignedChainGovernorStatus signed_chain_governor_status = 9; + SignedQueryRequest signed_query_request = 10; } } @@ -231,3 +232,28 @@ message ChainGovernorStatus { int64 timestamp = 3; repeated Chain chains = 4; } + +message SignedQueryRequest { + // Serialized QueryRequest message. + bytes query_request = 1; + + // ECDSA signature using the requestor's public key. + bytes signature = 2; + + // Requestor address that signed this payload (truncated Eth address). + bytes requestor_addr = 3; +} + +message QueryRequest { + uint32 chain_id = 1; + uint32 nonce = 2; + oneof message { + EthCallQueryRequest eth_call_query_request = 3; + } +} + +message EthCallQueryRequest { + bytes to = 1; + bytes data = 2; + string block = 3; +} From d486af7de1441d36ac7dce00460e0c3cd2ea55e3 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Sun, 21 May 2023 21:42:33 +0000 Subject: [PATCH 02/37] WIP: CCQ add block to call --- node/cmd/guardiand/query.go | 18 ++-- node/hack/query/send_req.go | 79 +++++++++----- node/pkg/adminrpc/adminserver_test.go | 5 + node/pkg/p2p/watermark_test.go | 1 + node/pkg/proto/gossip/v1/gossip.pb.go | 54 ++++------ node/pkg/watchers/evm/connectors/celo.go | 14 +++ node/pkg/watchers/evm/connectors/common.go | 13 +++ node/pkg/watchers/evm/connectors/ethereum.go | 3 + node/pkg/watchers/evm/connectors/poller.go | 12 +-- .../watchers/evm/connectors/poller_test.go | 5 + .../watchers/evm/finalizers/moonbeam_test.go | 5 + node/pkg/watchers/evm/watcher.go | 102 +++++++++++++++--- proto/gossip/v1/gossip.proto | 3 - 13 files changed, 209 insertions(+), 105 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 25b851ae64..f409e28e88 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -3,7 +3,6 @@ package guardiand import ( "context" - "github.com/benbjohnson/clock" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" @@ -24,7 +23,6 @@ var allowedRequestor = common.BytesToAddress(common.Hex2Bytes("beFA429d57cD18b7F // Multiplex observation requests to the appropriate chain func handleQueryRequests( ctx context.Context, - clock clock.Clock, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, chainQueryReqC map[vaa.ChainID]chan *gossipv1.QueryRequest, @@ -37,25 +35,21 @@ func handleQueryRequests( case signedQueryRequest := <-signedQueryReqC: // requestor validation happens here // request type validation is currently handled by the watcher - // in the future, it may be worthwhile to catch certain types of + // in the future, it may be worthwhile to catch certain types of // invalid requests here for tracking purposes - requestorAddr := common.BytesToAddress(signedQueryRequest.RequestorAddr) - if requestorAddr != allowedRequestor { - qLogger.Error("invalid requestor", zap.String("requestor", requestorAddr.Hex())) - continue - } digest := queryRequestDigest(signedQueryRequest.QueryRequest) signerBytes, err := ethcrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) if err != nil { - qLogger.Error("failed to recover public key", zap.String("requestor", requestorAddr.Hex())) + qLogger.Error("failed to recover public key") continue } signerAddress := common.BytesToAddress(ethcrypto.Keccak256(signerBytes[1:])[12:]) - if signerAddress != requestorAddr { - qLogger.Error("requestor signer mismatch", zap.String("requestor", requestorAddr.Hex()), zap.String("signer", signerAddress.Hex())) + + if signerAddress != allowedRequestor { + qLogger.Error("invalid requestor", zap.String("requestor", signerAddress.Hex())) continue } @@ -63,7 +57,7 @@ func handleQueryRequests( err = proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) if err != nil { qLogger.Error("received invalid message", - zap.String("requestor", requestorAddr.Hex())) + zap.String("requestor", signerAddress.Hex())) continue } diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 0bcb0e977f..d08cd638f6 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -11,6 +11,7 @@ import ( "io" "os" "strings" + "time" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/p2p" @@ -43,9 +44,22 @@ func queryRequestDigest(b []byte) ethCommon.Hash { // this script has to be run inside kubernetes since it relies on UDP // https://github.com/kubernetes/kubernetes/issues/47862 // kubectl --namespace=wormhole exec -it spy-0 -- sh -c "cd node/hack/query/ && go run send_req.go" +// one way to iterate inside the container +// kubectl --namespace=wormhole exec -it spy-0 -- bash +// apt update +// apt install nano +// cd node/hack/query +// echo "" > send_req.go +// nano send_req.go +// [paste, ^x, y, enter] +// go run send_req.go func main() { + // + // BEGIN SETUP + // + p2pNetworkID := "/wormhole/dev" var p2pPort uint = 8998 // don't collide with spy so we can run from the same container in tilt p2pBootstrap := "/dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" @@ -144,28 +158,37 @@ func main() { if err != nil { logger.Panic("failed to join topic", zap.Error(err)) } - + sub, err := th.Subscribe() if err != nil { logger.Panic("failed to subscribe topic", zap.Error(err)) } logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), - zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) + zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) + + // Wait for peers + for len(th.ListPeers()) < 1 { + time.Sleep(time.Millisecond * 100) + } + + // + // END SETUP + // to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") data, _ := hex.DecodeString("18160ddd") // block := "0x28d9630" - // block := "latest" - block := "0x2e0d2bc116d77308db4e76eb906f6c168767ed00ad62cd2e2a31c61744c506e6" + block := "latest" + // block := "0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2" callRequest := &gossipv1.EthCallQueryRequest{ - To: to, - Data: data, - Block: block, + To: to, + Data: data, + Block: block, } queryRequest := &gossipv1.QueryRequest{ ChainId: 5, - Nonce: 0, + Nonce: 0, Message: &gossipv1.QueryRequest_EthCallQueryRequest{ EthCallQueryRequest: callRequest}} @@ -183,8 +206,7 @@ func main() { signedQueryRequest := &gossipv1.SignedQueryRequest{ QueryRequest: queryRequestBytes, - Signature: sig, - RequestorAddr: ethCrypto.PubkeyToAddress(sk.PublicKey).Bytes(), + Signature: sig, } msg := gossipv1.GossipMessage{ @@ -198,25 +220,6 @@ func main() { panic(err) } - // do something to wait for peers, this waits to receive a message - logger.Info("Waiting for a message...") - for { - envelope, err := sub.Next(ctx) - if err != nil { - logger.Panic("failed to receive pubsub message", zap.Error(err)) - } - var msg gossipv1.GossipMessage - err = proto.Unmarshal(envelope.Data, &msg) - if err != nil { - logger.Info("received invalid message", - zap.Binary("data", envelope.Data), - zap.String("from", envelope.GetFrom().String())) - continue - } - logger.Info("Received a message!") - break - } - err = th.Publish(ctx, b) if err != nil { panic(err) @@ -241,6 +244,24 @@ func main() { break } + // + // BEGIN SHUTDOWN + // + + // Cleanly shutdown + // Without this the same host won't properly discover peers until some timeout + sub.Cancel() + if err := th.Close(); err != nil { + logger.Fatal("Error closing the topic", zap.Error(err)) + } + if err := h.Close(); err != nil { + logger.Fatal("Error closing the host", zap.Error(err)) + } + + // + // END SHUTDOWN + // + logger.Info("Success! All tests passed!") } diff --git a/node/pkg/adminrpc/adminserver_test.go b/node/pkg/adminrpc/adminserver_test.go index fa6dcd5973..7426f842d3 100644 --- a/node/pkg/adminrpc/adminserver_test.go +++ b/node/pkg/adminrpc/adminserver_test.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" + ethRpc "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/require" "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" @@ -68,6 +69,10 @@ func (m mockEVMConnector) RawCallContext(ctx context.Context, result interface{} panic("unimplemented") } +func (m mockEVMConnector) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error { + panic("unimplemented") +} + func generateGS(num int) (keys []*ecdsa.PrivateKey, addrs []common.Address) { for i := 0; i < num; i++ { key, err := ethcrypto.GenerateKey() diff --git a/node/pkg/p2p/watermark_test.go b/node/pkg/p2p/watermark_test.go index a1f7b925e3..59eab05ba6 100644 --- a/node/pkg/p2p/watermark_test.go +++ b/node/pkg/p2p/watermark_test.go @@ -185,5 +185,6 @@ func startGuardian(t *testing.T, ctx context.Context, g *G) { g.components, nil, // ibc feature string false, // gateway relayer enabled + nil, // signed query request channel )) } diff --git a/node/pkg/proto/gossip/v1/gossip.pb.go b/node/pkg/proto/gossip/v1/gossip.pb.go index 4159b13a7f..b8733cfb25 100644 --- a/node/pkg/proto/gossip/v1/gossip.pb.go +++ b/node/pkg/proto/gossip/v1/gossip.pb.go @@ -1150,8 +1150,6 @@ type SignedQueryRequest struct { QueryRequest []byte `protobuf:"bytes,1,opt,name=query_request,json=queryRequest,proto3" json:"query_request,omitempty"` // ECDSA signature using the requestor's public key. Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` - // Requestor address that signed this payload (truncated Eth address). - RequestorAddr []byte `protobuf:"bytes,3,opt,name=requestor_addr,json=requestorAddr,proto3" json:"requestor_addr,omitempty"` } func (x *SignedQueryRequest) Reset() { @@ -1200,13 +1198,6 @@ func (x *SignedQueryRequest) GetSignature() []byte { return nil } -func (x *SignedQueryRequest) GetRequestorAddr() []byte { - if x != nil { - return x.RequestorAddr - } - return nil -} - type QueryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1978,35 +1969,32 @@ var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x52, 0x08, 0x65, 0x6d, 0x69, - 0x74, 0x74, 0x65, 0x72, 0x73, 0x22, 0x7e, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, + 0x74, 0x74, 0x65, 0x72, 0x73, 0x22, 0x57, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x25, - 0x0a, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x41, 0x64, 0x64, 0x72, 0x22, 0xa1, 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, - 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, - 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, - 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, - 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xa1, + 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, + 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, + 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, + 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, - 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, - 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, - 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, - 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, + 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, + 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, + 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/node/pkg/watchers/evm/connectors/celo.go b/node/pkg/watchers/evm/connectors/celo.go index 9e46881c15..d1b5d355df 100644 --- a/node/pkg/watchers/evm/connectors/celo.go +++ b/node/pkg/watchers/evm/connectors/celo.go @@ -17,6 +17,7 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" ethTypes "github.com/ethereum/go-ethereum/core/types" ethEvent "github.com/ethereum/go-ethereum/event" + ethRpc "github.com/ethereum/go-ethereum/rpc" "github.com/certusone/wormhole/node/pkg/common" "go.uber.org/zap" @@ -181,6 +182,19 @@ func (c *CeloConnector) RawCallContext(ctx context.Context, result interface{}, return c.rawClient.CallContext(ctx, result, method, args...) } +func (c *CeloConnector) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error { + celoB := make([]celoRpc.BatchElem, len(b)) + for i, v := range b { + celoB[i] = celoRpc.BatchElem{ + Method: v.Method, + Args: v.Args, + Result: v.Result, + Error: v.Error, + } + } + return c.rawClient.BatchCallContext(ctx, celoB) +} + func convertCeloEventToEth(ev *celoAbi.AbiLogMessagePublished) *ethAbi.AbiLogMessagePublished { return ðAbi.AbiLogMessagePublished{ Sender: ethCommon.BytesToAddress(ev.Sender.Bytes()), diff --git a/node/pkg/watchers/evm/connectors/common.go b/node/pkg/watchers/evm/connectors/common.go index 187026ae0c..f177c8a539 100644 --- a/node/pkg/watchers/evm/connectors/common.go +++ b/node/pkg/watchers/evm/connectors/common.go @@ -10,10 +10,22 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" ) +type BlockMarshaller struct { + Number *hexutil.Big + Hash common.Hash `json:"hash"` + Time hexutil.Uint64 `json:"timestamp"` + + // L1BlockNumber is the L1 block number in which an Arbitrum batch containing this block was submitted. + // This field is only populated when connecting to Arbitrum. + L1BlockNumber *hexutil.Big +} + type NewBlock struct { Number *big.Int Hash common.Hash @@ -33,6 +45,7 @@ type Connector interface { ParseLogMessagePublished(log types.Log) (*ethabi.AbiLogMessagePublished, error) SubscribeForBlocks(ctx context.Context, errC chan error, sink chan<- *NewBlock) (ethereum.Subscription, error) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error + RawBatchCallContext(ctx context.Context, b []rpc.BatchElem) error } type PollSubscription struct { diff --git a/node/pkg/watchers/evm/connectors/ethereum.go b/node/pkg/watchers/evm/connectors/ethereum.go index 1c5c3bcf9d..e28d53ef10 100644 --- a/node/pkg/watchers/evm/connectors/ethereum.go +++ b/node/pkg/watchers/evm/connectors/ethereum.go @@ -133,7 +133,10 @@ func (e *EthereumConnector) SubscribeForBlocks(ctx context.Context, errC chan er func (e *EthereumConnector) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { return e.rawClient.CallContext(ctx, result, method, args...) +} +func (e *EthereumConnector) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error { + return e.rawClient.BatchCallContext(ctx, b) } func (e *EthereumConnector) Client() *ethClient.Client { diff --git a/node/pkg/watchers/evm/connectors/poller.go b/node/pkg/watchers/evm/connectors/poller.go index b26c81441d..4e5506b7e7 100644 --- a/node/pkg/watchers/evm/connectors/poller.go +++ b/node/pkg/watchers/evm/connectors/poller.go @@ -11,7 +11,6 @@ import ( ethEvent "github.com/ethereum/go-ethereum/event" ethereum "github.com/ethereum/go-ethereum" - ethCommon "github.com/ethereum/go-ethereum/common" ethHexUtils "github.com/ethereum/go-ethereum/common/hexutil" "go.uber.org/zap" @@ -224,16 +223,7 @@ func getBlock(ctx context.Context, logger *zap.Logger, conn Connector, number *b numStr = "latest" } - type Marshaller struct { - Number *ethHexUtils.Big - Hash ethCommon.Hash `json:"hash"` - - // L1BlockNumber is the L1 block number in which an Arbitrum batch containing this block was submitted. - // This field is only populated when connecting to Arbitrum. - L1BlockNumber *ethHexUtils.Big - } - - var m Marshaller + var m BlockMarshaller err := conn.RawCallContext(ctx, &m, "eth_getBlockByNumber", numStr, false) if err != nil { logger.Error("failed to get block", diff --git a/node/pkg/watchers/evm/connectors/poller_test.go b/node/pkg/watchers/evm/connectors/poller_test.go index d3b520da68..ef2e1cc103 100644 --- a/node/pkg/watchers/evm/connectors/poller_test.go +++ b/node/pkg/watchers/evm/connectors/poller_test.go @@ -20,6 +20,7 @@ import ( ethTypes "github.com/ethereum/go-ethereum/core/types" ethClient "github.com/ethereum/go-ethereum/ethclient" ethEvent "github.com/ethereum/go-ethereum/event" + ethRpc "github.com/ethereum/go-ethereum/rpc" ) // mockConnectorForPoller implements the connector interface for testing purposes. @@ -107,6 +108,10 @@ func (e *mockConnectorForPoller) RawCallContext(ctx context.Context, result inte return } +func (e *mockConnectorForPoller) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error { + panic("method not implemented by mockConnectorForPoller") +} + func (e *mockConnectorForPoller) setBlockNumber(blockNumber uint64) { e.mutex.Lock() e.blockNumber = blockNumber diff --git a/node/pkg/watchers/evm/finalizers/moonbeam_test.go b/node/pkg/watchers/evm/finalizers/moonbeam_test.go index 2f68b3f50b..554a71f28d 100644 --- a/node/pkg/watchers/evm/finalizers/moonbeam_test.go +++ b/node/pkg/watchers/evm/finalizers/moonbeam_test.go @@ -15,6 +15,7 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" ethTypes "github.com/ethereum/go-ethereum/core/types" ethEvent "github.com/ethereum/go-ethereum/event" + ethRpc "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,6 +37,10 @@ func (e *moonbeamMockConnector) RawCallContext(ctx context.Context, result inter return } +func (e *moonbeamMockConnector) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error { + panic("method not implemented by moonbeamMockConnector") +} + func (e *moonbeamMockConnector) NetworkName() string { return "moonbeamMockConnector" } diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 39864e415e..71b77c9340 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -539,18 +539,19 @@ func (w *Watcher) Run(parentCtx context.Context) error { data := eth_hexutil.Encode(req.EthCallQueryRequest.Data) block := req.EthCallQueryRequest.Block logger.Info("received query request", - zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), - zap.String("block", block)) - + zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + timeout, cancel := context.WithTimeout(ctx, 5*time.Second) // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 - arg := map[string]interface{}{ - "to": to, + callTransactionArg := map[string]interface{}{ + "to": to, "data": data, } - var blockArg interface{} + var blockMethod string + var callBlockArg interface{} // TODO: try making these error and see what happens // 1. 66 chars but not 0x hex // 2. 64 chars but not hex @@ -560,18 +561,44 @@ func (w *Watcher) Run(parentCtx context.Context) error { // 6. "safe" on a chain that doesn't support safe // etc? // I would expect this to trip within this scissor (if at all) but maybe this should get more defensive - if (len(block) == 66 || len(block) == 64) { + if len(block) == 66 || len(block) == 64 { + blockMethod = "eth_getBlockByHash" // looks like a hash which requires the object parameter + // https://eips.ethereum.org/EIPS/eip-1898 // https://docs.alchemy.com/reference/eth-call hash := eth_common.HexToHash(block) - blockArg = rpc.BlockNumberOrHash{ - BlockHash: &hash, - } + callBlockArg = rpc.BlockNumberOrHash{ + BlockHash: &hash, + RequireCanonical: true, + } } else { - blockArg = block + blockMethod = "eth_getBlockByNumber" + callBlockArg = block } - var result string - err := w.ethConn.RawCallContext(timeout, &result, "eth_call", arg, blockArg) + var blockResult connectors.BlockMarshaller + var blockError error + var callResult string + var callErr error + err := w.ethConn.RawBatchCallContext(timeout, []rpc.BatchElem{ + { + Method: blockMethod, + Args: []interface{}{ + block, + false, // no full transaction details + }, + Result: &blockResult, + Error: blockError, + }, + { + Method: "eth_call", + Args: []interface{}{ + callTransactionArg, + callBlockArg, + }, + Result: &callResult, + Error: callErr, + }, + }) cancel() if err != nil { @@ -583,14 +610,55 @@ func (w *Watcher) Run(parentCtx context.Context) error { continue } - // TODO: error on "0x" response + if blockError != nil { + logger.Error("failed to process query block request", + zap.Error(blockError), zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + continue + } + + if blockResult.Number == nil { + logger.Error("invalid query block result", + zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + continue + } + + if callErr != nil { + logger.Error("failed to process query call request", + zap.Error(callErr), zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block)) + continue + } + + // Empty results are not valid + // Yes, I mean "0x", we saw this as empty when testing endpoints in JS + // Empty results can occur when the queried block state is no longer available + if callResult == "" || callResult == "0x" { + logger.Error("invalid call result", + zap.String("eth_network", w.networkName), + zap.String("to", to.Hex()), + zap.Any("data", data), + zap.String("block", block), + zap.String("result", callResult)) + continue + } logger.Info("query result", zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), zap.String("block", block), - zap.String("result", result)) + zap.String("blockNumber", blockResult.Number.String()), + zap.String("blockHash", blockResult.Hash.Hex()), + zap.String("blockTime", blockResult.Time.String()), + zap.String("result", callResult)) default: logger.Warn("received unsupported request type", zap.Any("payload", queryRequest.Message)) diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index 0e9841721f..84884af110 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -239,9 +239,6 @@ message SignedQueryRequest { // ECDSA signature using the requestor's public key. bytes signature = 2; - - // Requestor address that signed this payload (truncated Eth address). - bytes requestor_addr = 3; } message QueryRequest { From dbd20aa53e507fd64964631e9db7b55363490329 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Tue, 23 May 2023 22:34:16 +0000 Subject: [PATCH 03/37] WIP: CCQ serialize and sign response --- node/cmd/guardiand/query.go | 24 +- node/cmd/spy/spy.go | 19 +- node/hack/query/send_req.go | 16 +- node/pkg/common/queryReqSendC.go | 16 - node/pkg/common/queryRequest.go | 16 + node/pkg/common/queryResponse.go | 160 ++++++++ node/pkg/p2p/p2p.go | 35 +- node/pkg/p2p/watermark_test.go | 1 + node/pkg/proto/gossip/v1/gossip.pb.go | 557 +++++++++++++++----------- node/pkg/watchers/evm/watcher.go | 46 ++- proto/gossip/v1/gossip.proto | 9 + 11 files changed, 612 insertions(+), 287 deletions(-) delete mode 100644 node/pkg/common/queryReqSendC.go create mode 100644 node/pkg/common/queryRequest.go create mode 100644 node/pkg/common/queryResponse.go diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index f409e28e88..68acbda63a 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -4,8 +4,8 @@ import ( "context" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" "google.golang.org/protobuf/proto" @@ -14,18 +14,18 @@ import ( // TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 var queryRequestPrefix = []byte("query_request_00000000000000000000|") -func queryRequestDigest(b []byte) common.Hash { - return ethcrypto.Keccak256Hash(append(queryRequestPrefix, b...)) +func queryRequestDigest(b []byte) ethCommon.Hash { + return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) } -var allowedRequestor = common.BytesToAddress(common.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")) +var allowedRequestor = ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")) // Multiplex observation requests to the appropriate chain func handleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *gossipv1.QueryRequest, + chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, ) { qLogger := logger.With(zap.String("component", "queryHandler")) for { @@ -37,16 +37,20 @@ func handleQueryRequests( // request type validation is currently handled by the watcher // in the future, it may be worthwhile to catch certain types of // invalid requests here for tracking purposes + // e.g. + // - length check on "signature" 65 bytes + // - length check on "to" address 20 bytes + // - valid "block" strings digest := queryRequestDigest(signedQueryRequest.QueryRequest) - signerBytes, err := ethcrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) + signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) if err != nil { qLogger.Error("failed to recover public key") continue } - signerAddress := common.BytesToAddress(ethcrypto.Keccak256(signerBytes[1:])[12:]) + signerAddress := ethCommon.BytesToAddress(ethCrypto.Keccak256(signerBytes[1:])[12:]) if signerAddress != allowedRequestor { qLogger.Error("invalid requestor", zap.String("requestor", signerAddress.Hex())) @@ -63,8 +67,8 @@ func handleQueryRequests( if channel, ok := chainQueryReqC[vaa.ChainID(queryRequest.ChainId)]; ok { select { - // TODO: is pointer fine here? - case channel <- &queryRequest: + // TODO: only send the query request itself and reassemble in this module + case channel <- signedQueryRequest: default: qLogger.Warn("failed to send query request to watcher", zap.Uint16("chain_id", uint16(queryRequest.ChainId))) diff --git a/node/cmd/spy/spy.go b/node/cmd/spy/spy.go index 6da7216ac8..ca2d2757c3 100644 --- a/node/cmd/spy/spy.go +++ b/node/cmd/spy/spy.go @@ -271,9 +271,6 @@ func runSpy(cmd *cobra.Command, args []string) { // Inbound observation requests obsvReqC := make(chan *gossipv1.ObservationRequest, 1024) - // Inbound observation requests - queryReqC := make(chan *gossipv1.SignedQueryRequest, 50) - // Inbound signed VAAs signedInC := make(chan *gossipv1.SignedVAAWithQuorum, 1024) @@ -310,18 +307,6 @@ func runSpy(cmd *cobra.Command, args []string) { } }() - // Ignore query requests - // Note: without this, the whole program hangs on query requests - go func() { - for { - select { - case <-rootCtx.Done(): - return - case <-queryReqC: - } - } - }() - // Log signed VAAs go func() { for { @@ -371,7 +356,9 @@ func runSpy(cmd *cobra.Command, args []string) { components, nil, // ibc feature string false, // gateway relayer enabled - queryReqC, + nil, // query requests + nil, // query responses + )); err != nil { return err } diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index d08cd638f6..6c70295759 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -226,6 +226,7 @@ func main() { } logger.Info("Waiting for message...") + // TODO: max wait time for { envelope, err := sub.Next(ctx) if err != nil { @@ -239,9 +240,18 @@ func main() { zap.String("from", envelope.GetFrom().String())) continue } - // TODO: actually wait for the corresponding response - logger.Info("received message") - break + 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 + default: + continue + } + if isMatchingResponse { + break + } } // diff --git a/node/pkg/common/queryReqSendC.go b/node/pkg/common/queryReqSendC.go deleted file mode 100644 index 9acfedeb5c..0000000000 --- a/node/pkg/common/queryReqSendC.go +++ /dev/null @@ -1,16 +0,0 @@ -package common - -import ( - gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" -) - -const QueryReqChannelSize = 50 - -func PostQueryRequest(obsvReqSendC chan<- *gossipv1.QueryRequest, req *gossipv1.QueryRequest) error { - select { - case obsvReqSendC <- req: - return nil - default: - return ErrChanFull - } -} diff --git a/node/pkg/common/queryRequest.go b/node/pkg/common/queryRequest.go new file mode 100644 index 0000000000..c29a788b05 --- /dev/null +++ b/node/pkg/common/queryRequest.go @@ -0,0 +1,16 @@ +package common + +import ( + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" +) + +const SignedQueryRequestChannelSize = 50 + +func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequest, req *gossipv1.SignedQueryRequest) error { + select { + case signedQueryReqSendC <- req: + return nil + default: + return ErrChanFull + } +} diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go new file mode 100644 index 0000000000..e5b9fe5a92 --- /dev/null +++ b/node/pkg/common/queryResponse.go @@ -0,0 +1,160 @@ +package common + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "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" +) + +var queryResponsePrefix = []byte("query_response_0000000000000000000|") + +type EthCallQueryResponse struct { + Number *eth_hexutil.Big + Hash common.Hash + Time eth_hexutil.Uint64 + Result []byte +} + +type QueryResponsePublication struct { + Request *gossipv1.SignedQueryRequest + Response EthCallQueryResponse +} + +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) + var queryRequest gossipv1.QueryRequest + err := proto.Unmarshal(msg.Request.QueryRequest, &queryRequest) + if err != nil { + return nil, fmt.Errorf("received invalid message from query module") + } + + buf := new(bytes.Buffer) + + // Source + // TODO: support writing off-chain and on-chain requests + // Here, unset represents an off-chain request + vaa.MustWrite(buf, binary.BigEndian, vaa.ChainIDUnset) + buf.Write(msg.Request.Signature[:]) + + // Request + // TODO: support writing different types of request/response pairs + 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 len(req.EthCallQueryRequest.To) != 20 { + return nil, fmt.Errorf("invalid length for To contract") + } + buf.Write(req.EthCallQueryRequest.To) + if len(req.EthCallQueryRequest.Data) > math.MaxUint32 { + return nil, fmt.Errorf("request data too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Data))) + buf.Write(req.EthCallQueryRequest.Data) + if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { + return nil, fmt.Errorf("request block too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Block))) + // TODO: should this be an enum or the literal string? + buf.Write([]byte(req.EthCallQueryRequest.Block)) + + // Response + // TODO: probably some kind of request/response pair validation + vaa.MustWrite(buf, binary.BigEndian, msg.Response.Number.ToInt().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())) + if len(msg.Response.Result) > math.MaxUint32 { + return nil, fmt.Errorf("response data too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(msg.Response.Result))) + buf.Write(msg.Response.Result) + return buf.Bytes(), nil + default: + return nil, fmt.Errorf("received invalid message from query module") + } +} + +// 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 +// } + +// 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 +// the first hash (32 bytes) vs the full body data. +// TODO: confirm if this works / is worthwhile. +func (msg *QueryResponsePublication) SigningDigest() (common.Hash, error) { + msgBytes, err := msg.Marshal() + if err != nil { + return common.Hash{}, err + } + return GetQueryResponseDigestFromBytes(msgBytes), nil +} + +func GetQueryResponseDigestFromBytes(b []byte) common.Hash { + return crypto.Keccak256Hash(append(queryResponsePrefix, crypto.Keccak256Hash(b).Bytes()...)) +} diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 25141912ba..935be65623 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -211,6 +211,7 @@ func Run( ibcFeaturesFunc func() string, gatewayRelayerEnabled bool, signedQueryReqC chan<- *gossipv1.SignedQueryRequest, + queryResponseReadC <-chan *node_common.QueryResponsePublication, ) func(ctx context.Context) error { if components == nil { components = DefaultComponents() @@ -497,6 +498,36 @@ func Run( } else { logger.Info("published signed observation request", zap.Any("signed_observation_request", sReq)) } + case msg := <-queryResponseReadC: + msgBytes, err := msg.Marshal() + if err != nil { + logger.Error("failed to marshal query response", zap.Error(err)) + continue + } + digest := node_common.GetQueryResponseDigestFromBytes(msgBytes) + sig, err := ethcrypto.Sign(digest.Bytes(), gk) + if err != nil { + panic(err) + } + envelope := &gossipv1.GossipMessage{ + Message: &gossipv1.GossipMessage_SignedQueryResponse{ + SignedQueryResponse: &gossipv1.SignedQueryResponse{ + QueryResponse: msgBytes, + Signature: sig, + }, + }, + } + b, err := proto.Marshal(envelope) + if err != nil { + panic(err) + } + err = th.Publish(ctx, b) + p2pMessagesSent.Inc() + if err != nil { + logger.Error("failed to publish query response", zap.Error(err)) + } else { + logger.Info("published signed query response", zap.Any("query_response", msg), zap.Any("signature", sig)) + } } } }() @@ -657,7 +688,9 @@ func Run( } case *gossipv1.GossipMessage_SignedQueryRequest: if signedQueryReqC != nil { - signedQueryReqC <- m.SignedQueryRequest + if err := node_common.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { + logger.Warn("failed to handle query request", zap.Error(err)) + } } default: p2pMessagesReceived.WithLabelValues("unknown").Inc() diff --git a/node/pkg/p2p/watermark_test.go b/node/pkg/p2p/watermark_test.go index 59eab05ba6..87d6a5f45e 100644 --- a/node/pkg/p2p/watermark_test.go +++ b/node/pkg/p2p/watermark_test.go @@ -186,5 +186,6 @@ func startGuardian(t *testing.T, ctx context.Context, g *G) { nil, // ibc feature string false, // gateway relayer enabled nil, // signed query request channel + nil, // query response channel )) } diff --git a/node/pkg/proto/gossip/v1/gossip.pb.go b/node/pkg/proto/gossip/v1/gossip.pb.go index b8733cfb25..134c798fcc 100644 --- a/node/pkg/proto/gossip/v1/gossip.pb.go +++ b/node/pkg/proto/gossip/v1/gossip.pb.go @@ -36,6 +36,7 @@ type GossipMessage struct { // *GossipMessage_SignedChainGovernorConfig // *GossipMessage_SignedChainGovernorStatus // *GossipMessage_SignedQueryRequest + // *GossipMessage_SignedQueryResponse Message isGossipMessage_Message `protobuf_oneof:"message"` } @@ -141,6 +142,13 @@ func (x *GossipMessage) GetSignedQueryRequest() *SignedQueryRequest { return nil } +func (x *GossipMessage) GetSignedQueryResponse() *SignedQueryResponse { + if x, ok := x.GetMessage().(*GossipMessage_SignedQueryResponse); ok { + return x.SignedQueryResponse + } + return nil +} + type isGossipMessage_Message interface { isGossipMessage_Message() } @@ -181,6 +189,10 @@ type GossipMessage_SignedQueryRequest struct { SignedQueryRequest *SignedQueryRequest `protobuf:"bytes,10,opt,name=signed_query_request,json=signedQueryRequest,proto3,oneof"` } +type GossipMessage_SignedQueryResponse struct { + SignedQueryResponse *SignedQueryResponse `protobuf:"bytes,11,opt,name=signed_query_response,json=signedQueryResponse,proto3,oneof"` +} + func (*GossipMessage_SignedObservation) isGossipMessage_Message() {} func (*GossipMessage_SignedHeartbeat) isGossipMessage_Message() {} @@ -199,6 +211,8 @@ func (*GossipMessage_SignedChainGovernorStatus) isGossipMessage_Message() {} func (*GossipMessage_SignedQueryRequest) isGossipMessage_Message() {} +func (*GossipMessage_SignedQueryResponse) isGossipMessage_Message() {} + type SignedHeartbeat struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1344,6 +1358,63 @@ func (x *EthCallQueryRequest) GetBlock() string { return "" } +type SignedQueryResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Serialized QueryResponse message. + QueryResponse []byte `protobuf:"bytes,1,opt,name=query_response,json=queryResponse,proto3" json:"query_response,omitempty"` + // ECDSA signature using the node's guardian public key. + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` +} + +func (x *SignedQueryResponse) Reset() { + *x = SignedQueryResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignedQueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignedQueryResponse) ProtoMessage() {} + +func (x *SignedQueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignedQueryResponse.ProtoReflect.Descriptor instead. +func (*SignedQueryResponse) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16} +} + +func (x *SignedQueryResponse) GetQueryResponse() []byte { + if x != nil { + return x.QueryResponse + } + return nil +} + +func (x *SignedQueryResponse) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + type Heartbeat_Network struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1362,7 +1433,7 @@ type Heartbeat_Network struct { func (x *Heartbeat_Network) Reset() { *x = Heartbeat_Network{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1375,7 +1446,7 @@ func (x *Heartbeat_Network) String() string { func (*Heartbeat_Network) ProtoMessage() {} func (x *Heartbeat_Network) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1432,7 +1503,7 @@ type ChainGovernorConfig_Chain struct { func (x *ChainGovernorConfig_Chain) Reset() { *x = ChainGovernorConfig_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1445,7 +1516,7 @@ func (x *ChainGovernorConfig_Chain) String() string { func (*ChainGovernorConfig_Chain) ProtoMessage() {} func (x *ChainGovernorConfig_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1495,7 +1566,7 @@ type ChainGovernorConfig_Token struct { func (x *ChainGovernorConfig_Token) Reset() { *x = ChainGovernorConfig_Token{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1508,7 +1579,7 @@ func (x *ChainGovernorConfig_Token) String() string { func (*ChainGovernorConfig_Token) ProtoMessage() {} func (x *ChainGovernorConfig_Token) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1559,7 +1630,7 @@ type ChainGovernorStatus_EnqueuedVAA struct { func (x *ChainGovernorStatus_EnqueuedVAA) Reset() { *x = ChainGovernorStatus_EnqueuedVAA{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1572,7 +1643,7 @@ func (x *ChainGovernorStatus_EnqueuedVAA) String() string { func (*ChainGovernorStatus_EnqueuedVAA) ProtoMessage() {} func (x *ChainGovernorStatus_EnqueuedVAA) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1629,7 +1700,7 @@ type ChainGovernorStatus_Emitter struct { func (x *ChainGovernorStatus_Emitter) Reset() { *x = ChainGovernorStatus_Emitter{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1642,7 +1713,7 @@ func (x *ChainGovernorStatus_Emitter) String() string { func (*ChainGovernorStatus_Emitter) ProtoMessage() {} func (x *ChainGovernorStatus_Emitter) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1692,7 +1763,7 @@ type ChainGovernorStatus_Chain struct { func (x *ChainGovernorStatus_Chain) Reset() { *x = ChainGovernorStatus_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1705,7 +1776,7 @@ func (x *ChainGovernorStatus_Chain) String() string { func (*ChainGovernorStatus_Chain) ProtoMessage() {} func (x *ChainGovernorStatus_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1747,7 +1818,7 @@ var File_gossip_v1_gossip_proto protoreflect.FileDescriptor var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x0a, 0x16, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, - 0x2e, 0x76, 0x31, 0x22, 0xd9, 0x06, 0x0a, 0x0d, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x4d, 0x65, + 0x2e, 0x76, 0x31, 0x22, 0xaf, 0x07, 0x0a, 0x0d, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, @@ -1800,201 +1871,212 @@ var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x1d, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x72, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, - 0x61, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, - 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, - 0x64, 0x64, 0x72, 0x22, 0xbb, 0x03, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, - 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x38, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x2e, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, - 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, - 0x25, 0x0a, 0x0e, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x32, 0x70, 0x4e, 0x6f, 0x64, 0x65, - 0x49, 0x64, 0x1a, 0x7d, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, - 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, - 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, - 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, - 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, - 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, - 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, - 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x49, 0x64, 0x22, 0x27, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x56, - 0x41, 0x41, 0x57, 0x69, 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x10, 0x0a, 0x03, - 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x61, 0x22, 0x8e, - 0x01, 0x0a, 0x18, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x13, 0x6f, - 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, - 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, - 0x48, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x54, 0x0a, 0x15, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x13, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x72, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, + 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, + 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x68, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, + 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0xbb, 0x03, 0x0a, 0x09, 0x48, 0x65, + 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x38, 0x0a, 0x08, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, + 0x62, 0x65, 0x61, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, + 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, + 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, + 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, + 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, + 0x32, 0x70, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x1a, 0x7d, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x53, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x64, 0x22, 0x27, 0x0a, 0x13, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x56, 0x41, 0x41, 0x57, 0x69, 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, + 0x75, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x03, 0x76, 0x61, 0x61, 0x22, 0x8e, 0x01, 0x0a, 0x18, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2f, 0x0a, 0x13, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, + 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, + 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0x48, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x22, + 0xbf, 0x01, 0x0a, 0x16, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, + 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, + 0x64, 0x22, 0x98, 0x01, 0x0a, 0x18, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x56, 0x41, 0x41, 0x57, 0x69, 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x1b, + 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x56, 0x61, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, + 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, + 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x19, + 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, + 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, + 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, + 0x41, 0x64, 0x64, 0x72, 0x22, 0xd1, 0x03, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, + 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, + 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, + 0x3c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, + 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x1a, 0x7b, 0x0a, + 0x05, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, + 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x62, 0x69, 0x67, 0x5f, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x62, 0x69, 0x67, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x69, 0x7a, 0x65, 0x1a, 0x6c, 0x0a, 0x05, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, + 0x22, 0x98, 0x05, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, + 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, + 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, + 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, + 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x68, + 0x61, 0x69, 0x6e, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x8c, 0x01, 0x0a, 0x0b, + 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x41, 0x41, 0x12, 0x1a, 0x0a, 0x08, 0x73, + 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, + 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, + 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x45, + 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, + 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, + 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x12, + 0x4f, 0x0a, 0x0d, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, + 0x41, 0x41, 0x52, 0x0c, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, + 0x1a, 0xa8, 0x01, 0x0a, 0x05, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, + 0x6e, 0x67, 0x5f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1a, 0x72, 0x65, 0x6d, + 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4e, + 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x42, 0x0a, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, + 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x73, 0x73, + 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, + 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, + 0x72, 0x52, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x73, 0x22, 0x57, 0x0a, 0x12, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, - 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x22, 0xbf, 0x01, 0x0a, 0x16, 0x53, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, - 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, - 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, - 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, - 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, - 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x18, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x56, 0x41, 0x41, 0x57, 0x69, - 0x74, 0x68, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, - 0x68, 0x5f, 0x76, 0x61, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, - 0x63, 0x68, 0x56, 0x61, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, - 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, - 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, - 0x61, 0x74, 0x63, 0x68, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, - 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, - 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x0c, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0xd1, - 0x03, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, - 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, - 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x68, 0x61, 0x69, - 0x6e, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x06, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, - 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, - 0x6e, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x1a, 0x7b, 0x0a, 0x05, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6e, - 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x62, 0x69, 0x67, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x12, 0x62, 0x69, 0x67, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x69, 0x7a, 0x65, 0x1a, 0x6c, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x26, 0x0a, - 0x0f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x43, 0x68, - 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, - 0x63, 0x65, 0x22, 0x76, 0x0a, 0x19, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, - 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, 0x61, + 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, 0x6c, + 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, + 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, 0x12, + 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x5a, 0x0a, 0x13, 0x53, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, - 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x67, 0x75, - 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x22, 0x98, 0x05, 0x0a, 0x13, 0x43, - 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x8c, 0x01, 0x0a, 0x0b, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, - 0x65, 0x64, 0x56, 0x41, 0x41, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6e, 0x6f, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, - 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, - 0x48, 0x61, 0x73, 0x68, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, - 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x6d, 0x69, 0x74, 0x74, - 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x45, 0x6e, 0x71, - 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x12, 0x4f, 0x0a, 0x0d, 0x65, 0x6e, 0x71, - 0x75, 0x65, 0x75, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2a, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, - 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x2e, 0x45, 0x6e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x41, 0x41, 0x52, 0x0c, 0x65, 0x6e, - 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x56, 0x61, 0x61, 0x73, 0x1a, 0xa8, 0x01, 0x0a, 0x05, 0x43, - 0x68, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, - 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, - 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x12, 0x42, 0x0a, 0x08, 0x65, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x72, 0x52, 0x08, 0x65, 0x6d, 0x69, - 0x74, 0x74, 0x65, 0x72, 0x73, 0x22, 0x57, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, - 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xa1, - 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, - 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, - 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, - 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, - 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, - 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, - 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, - 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, + 0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, + 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2009,7 +2091,7 @@ func file_gossip_v1_gossip_proto_rawDescGZIP() []byte { return file_gossip_v1_gossip_proto_rawDescData } -var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*GossipMessage)(nil), // 0: gossip.v1.GossipMessage (*SignedHeartbeat)(nil), // 1: gossip.v1.SignedHeartbeat @@ -2027,12 +2109,13 @@ var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*SignedQueryRequest)(nil), // 13: gossip.v1.SignedQueryRequest (*QueryRequest)(nil), // 14: gossip.v1.QueryRequest (*EthCallQueryRequest)(nil), // 15: gossip.v1.EthCallQueryRequest - (*Heartbeat_Network)(nil), // 16: gossip.v1.Heartbeat.Network - (*ChainGovernorConfig_Chain)(nil), // 17: gossip.v1.ChainGovernorConfig.Chain - (*ChainGovernorConfig_Token)(nil), // 18: gossip.v1.ChainGovernorConfig.Token - (*ChainGovernorStatus_EnqueuedVAA)(nil), // 19: gossip.v1.ChainGovernorStatus.EnqueuedVAA - (*ChainGovernorStatus_Emitter)(nil), // 20: gossip.v1.ChainGovernorStatus.Emitter - (*ChainGovernorStatus_Chain)(nil), // 21: gossip.v1.ChainGovernorStatus.Chain + (*SignedQueryResponse)(nil), // 16: gossip.v1.SignedQueryResponse + (*Heartbeat_Network)(nil), // 17: gossip.v1.Heartbeat.Network + (*ChainGovernorConfig_Chain)(nil), // 18: gossip.v1.ChainGovernorConfig.Chain + (*ChainGovernorConfig_Token)(nil), // 19: gossip.v1.ChainGovernorConfig.Token + (*ChainGovernorStatus_EnqueuedVAA)(nil), // 20: gossip.v1.ChainGovernorStatus.EnqueuedVAA + (*ChainGovernorStatus_Emitter)(nil), // 21: gossip.v1.ChainGovernorStatus.Emitter + (*ChainGovernorStatus_Chain)(nil), // 22: gossip.v1.ChainGovernorStatus.Chain } var file_gossip_v1_gossip_proto_depIdxs = []int32{ 3, // 0: gossip.v1.GossipMessage.signed_observation:type_name -> gossip.v1.SignedObservation @@ -2044,18 +2127,19 @@ var file_gossip_v1_gossip_proto_depIdxs = []int32{ 9, // 6: gossip.v1.GossipMessage.signed_chain_governor_config:type_name -> gossip.v1.SignedChainGovernorConfig 11, // 7: gossip.v1.GossipMessage.signed_chain_governor_status:type_name -> gossip.v1.SignedChainGovernorStatus 13, // 8: gossip.v1.GossipMessage.signed_query_request:type_name -> gossip.v1.SignedQueryRequest - 16, // 9: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network - 17, // 10: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain - 18, // 11: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token - 21, // 12: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain - 15, // 13: gossip.v1.QueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest - 19, // 14: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA - 20, // 15: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 16, // 9: gossip.v1.GossipMessage.signed_query_response:type_name -> gossip.v1.SignedQueryResponse + 17, // 10: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network + 18, // 11: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain + 19, // 12: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token + 22, // 13: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain + 15, // 14: gossip.v1.QueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest + 20, // 15: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA + 21, // 16: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_gossip_v1_gossip_proto_init() } @@ -2257,7 +2341,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Heartbeat_Network); i { + switch v := v.(*SignedQueryResponse); i { case 0: return &v.state case 1: @@ -2269,7 +2353,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Chain); i { + switch v := v.(*Heartbeat_Network); i { case 0: return &v.state case 1: @@ -2281,7 +2365,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Token); i { + switch v := v.(*ChainGovernorConfig_Chain); i { case 0: return &v.state case 1: @@ -2293,7 +2377,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { + switch v := v.(*ChainGovernorConfig_Token); i { case 0: return &v.state case 1: @@ -2305,7 +2389,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_Emitter); i { + switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { case 0: return &v.state case 1: @@ -2317,6 +2401,18 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChainGovernorStatus_Emitter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gossip_v1_gossip_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_Chain); i { case 0: return &v.state @@ -2339,6 +2435,7 @@ func file_gossip_v1_gossip_proto_init() { (*GossipMessage_SignedChainGovernorConfig)(nil), (*GossipMessage_SignedChainGovernorStatus)(nil), (*GossipMessage_SignedQueryRequest)(nil), + (*GossipMessage_SignedQueryResponse)(nil), } file_gossip_v1_gossip_proto_msgTypes[14].OneofWrappers = []interface{}{ (*QueryRequest_EthCallQueryRequest)(nil), @@ -2349,7 +2446,7 @@ func file_gossip_v1_gossip_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gossip_v1_gossip_proto_rawDesc, NumEnums: 0, - NumMessages: 22, + NumMessages: 23, NumExtensions: 0, NumServices: 0, }, diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 71b77c9340..39f44376c7 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -22,6 +22,7 @@ import ( eth_common "github.com/ethereum/go-ethereum/common" eth_hexutil "github.com/ethereum/go-ethereum/common/hexutil" "go.uber.org/zap" + "google.golang.org/protobuf/proto" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/readiness" @@ -96,7 +97,10 @@ type ( // Incoming query requests from the network. Pre-filtered to only // include requests for our chainID. - queryReqC <-chan *gossipv1.QueryRequest + queryReqC <-chan *gossipv1.SignedQueryRequest + + // Outbound query responses to query requests + queryResponseC chan<- *common.QueryResponsePublication pending map[pendingKey]*pendingMessage pendingMu sync.Mutex @@ -147,7 +151,8 @@ func NewEthWatcher( msgC chan<- *common.MessagePublication, setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, - queryReqC <-chan *gossipv1.QueryRequest, + queryReqC <-chan *gossipv1.SignedQueryRequest, + queryResponseC chan<- *common.QueryResponsePublication, unsafeDevMode bool, ) *Watcher { @@ -163,6 +168,7 @@ func NewEthWatcher( setC: setC, obsvReqC: obsvReqC, queryReqC: queryReqC, + queryResponseC: queryResponseC, pending: map[pendingKey]*pendingMessage{}, unsafeDevMode: unsafeDevMode, } @@ -526,7 +532,15 @@ func (w *Watcher) Run(parentCtx context.Context) error { select { case <-ctx.Done(): return nil - case queryRequest := <-w.queryReqC: + case signedQueryRequest := <-w.queryReqC: + // TODO: only receive the unmarshalled query request (see note in query.go) + var queryRequest gossipv1.QueryRequest + err := proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) + if err != nil { + logger.Error("received invalid message from query module") + continue + } + // This can't happen unless there is a programming error - the caller // is expected to send us only requests for our chainID. if vaa.ChainID(queryRequest.ChainId) != w.chainID { @@ -577,7 +591,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { } var blockResult connectors.BlockMarshaller var blockError error - var callResult string + var callResult eth_hexutil.Bytes var callErr error err := w.ethConn.RawBatchCallContext(timeout, []rpc.BatchElem{ { @@ -637,19 +651,27 @@ func (w *Watcher) Run(parentCtx context.Context) error { continue } - // Empty results are not valid - // Yes, I mean "0x", we saw this as empty when testing endpoints in JS - // Empty results can occur when the queried block state is no longer available - if callResult == "" || callResult == "0x" { + // Nil or Empty results are not valid + // eth_call will return empty when the state doesn't exist for a block + if len(callResult) == 0 { logger.Error("invalid call result", zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block), - zap.String("result", callResult)) + zap.String("block", block)) continue } + queryResponse := common.QueryResponsePublication{ + Request: signedQueryRequest, + Response: common.EthCallQueryResponse{ + Number: blockResult.Number, + Hash: blockResult.Hash, + Time: blockResult.Time, + Result: callResult, + }, + } + logger.Info("query result", zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), @@ -658,7 +680,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("blockNumber", blockResult.Number.String()), zap.String("blockHash", blockResult.Hash.Hex()), zap.String("blockTime", blockResult.Time.String()), - zap.String("result", callResult)) + zap.String("result", callResult.String())) + + w.queryResponseC <- &queryResponse default: logger.Warn("received unsupported request type", zap.Any("payload", queryRequest.Message)) diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index 84884af110..b68135cd8e 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -15,6 +15,7 @@ message GossipMessage { SignedChainGovernorConfig signed_chain_governor_config = 8; SignedChainGovernorStatus signed_chain_governor_status = 9; SignedQueryRequest signed_query_request = 10; + SignedQueryResponse signed_query_response = 11; } } @@ -254,3 +255,11 @@ message EthCallQueryRequest { bytes data = 2; string block = 3; } + +message SignedQueryResponse { + // Serialized QueryResponse message. + bytes query_response = 1; + + // ECDSA signature using the node's guardian public key. + bytes signature = 2; +} From 6fb836d5c5dcc9cc9101bf30c8153da5ee48a5bc Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Wed, 24 May 2023 18:43:41 +0000 Subject: [PATCH 04/37] WIP: CCQ deserialize response --- node/hack/query/send_req.go | 39 +++++- node/pkg/common/queryResponse.go | 197 +++++++++++++++++++++---------- node/pkg/watchers/evm/watcher.go | 4 +- 3 files changed, 173 insertions(+), 67 deletions(-) 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 39f44376c7..c6f3a0af87 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -665,9 +665,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, }, } From fd1cdacc88a5b492d98be60eb2116a2cc6fc3936 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 24 May 2023 17:38:49 -0500 Subject: [PATCH 05/37] CCQ: Add enable flag and feature flag (#2986) * CCQ: Add enable flag and feature flag * Fix build error --- Tiltfile | 7 +++++++ devnet/node.yaml | 1 + node/cmd/guardiand/node.go | 2 ++ node/cmd/guardiand/query.go | 9 +++++++++ node/cmd/spy/spy.go | 1 + node/pkg/p2p/p2p.go | 4 ++++ node/pkg/p2p/watermark_test.go | 1 + 7 files changed, 25 insertions(+) diff --git a/Tiltfile b/Tiltfile index 40d6ffea4a..c41b2a1739 100644 --- a/Tiltfile +++ b/Tiltfile @@ -73,6 +73,7 @@ config.define_bool("ibc_relayer", False, "Enable IBC relayer between cosmos chai config.define_bool("redis", False, "Enable a redis instance") config.define_bool("generic_relayer", False, "Enable the generic relayer off-chain component") +config.define_bool("ccq", False, "Enable cross chain queries in guardiand") cfg = config.parse() num_guardians = int(cfg.get("num", "1")) @@ -97,6 +98,7 @@ ibc_relayer = cfg.get("ibc_relayer", ci) btc = cfg.get("btc", False) redis = cfg.get('redis', ci) generic_relayer = cfg.get("generic_relayer", ci) +ccq = cfg.get("ccq", False) if ci: guardiand_loglevel = cfg.get("guardiand_loglevel", "warn") @@ -303,6 +305,11 @@ def build_node_yaml(): "--gatewayLCD", "http://wormchain:1317" ] + + if ccq: + container["command"] += [ + "--ccqEnabled=true" + ] return encode_yaml_stream(node_yaml_with_replicas) diff --git a/devnet/node.yaml b/devnet/node.yaml index 77bc9f6b5b..7a5efaeeb0 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -163,6 +163,7 @@ spec: - --publicRpcLogDetail - "full" # - --chainGovernorEnabled=true + # - --ccqEnabled=true # - --logLevel=debug securityContext: capabilities: diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index 12ba4a8473..2c01293990 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -204,6 +204,7 @@ var ( telemetryLokiURL *string chainGovernorEnabled *bool + ccqEnabled *bool gatewayRelayerContract *string gatewayRelayerKeyPath *string @@ -369,6 +370,7 @@ func init() { telemetryLokiURL = NodeCmd.Flags().String("telemetryLokiURL", "", "Loki cloud logging URL") chainGovernorEnabled = NodeCmd.Flags().Bool("chainGovernorEnabled", false, "Run the chain governor") + ccqEnabled = NodeCmd.Flags().Bool("ccqEnabled", false, "Enable cross chain query support") gatewayRelayerContract = NodeCmd.Flags().String("gatewayRelayerContract", "", "Address of the smart contract on wormchain to receive relayed VAAs") gatewayRelayerKeyPath = NodeCmd.Flags().String("gatewayRelayerKeyPath", "", "Path to gateway relayer private key for signing transactions") diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 68acbda63a..ae41469529 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -26,13 +26,22 @@ func handleQueryRequests( logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, + enableFlag bool, ) { qLogger := logger.With(zap.String("component", "queryHandler")) + if enableFlag { + qLogger.Info("cross chain queries are enabled") + } + for { select { case <-ctx.Done(): return case signedQueryRequest := <-signedQueryReqC: + if !enableFlag { + qLogger.Debug("dropping query request, feature is not enabled") + continue + } // requestor validation happens here // request type validation is currently handled by the watcher // in the future, it may be worthwhile to catch certain types of diff --git a/node/cmd/spy/spy.go b/node/cmd/spy/spy.go index ca2d2757c3..8262996601 100644 --- a/node/cmd/spy/spy.go +++ b/node/cmd/spy/spy.go @@ -356,6 +356,7 @@ func runSpy(cmd *cobra.Command, args []string) { components, nil, // ibc feature string false, // gateway relayer enabled + nil, // cross chain query feature string nil, // query requests nil, // query responses diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 935be65623..5fce20f192 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -210,6 +210,7 @@ func Run( components *Components, ibcFeaturesFunc func() string, gatewayRelayerEnabled bool, + ccqFeatures *string, signedQueryReqC chan<- *gossipv1.SignedQueryRequest, queryResponseReadC <-chan *node_common.QueryResponsePublication, ) func(ctx context.Context) error { @@ -400,6 +401,9 @@ func Run( if gatewayRelayerEnabled { features = append(features, "gwrelayer") } + if ccqFeatures != nil && *ccqFeatures != "" { + features = append(features, *ccqFeatures) + } heartbeat := &gossipv1.Heartbeat{ NodeName: nodeName, diff --git a/node/pkg/p2p/watermark_test.go b/node/pkg/p2p/watermark_test.go index 87d6a5f45e..029db990d8 100644 --- a/node/pkg/p2p/watermark_test.go +++ b/node/pkg/p2p/watermark_test.go @@ -185,6 +185,7 @@ func startGuardian(t *testing.T, ctx context.Context, g *G) { g.components, nil, // ibc feature string false, // gateway relayer enabled + nil, // cross chain query feature string nil, // signed query request channel nil, // query response channel )) From 2490182c09d2beb32bc74b70128d54e4f5c9c98c Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Thu, 25 May 2023 12:42:16 -0500 Subject: [PATCH 06/37] Move flag check to p2p (#2987) --- node/cmd/guardiand/query.go | 4 ++-- node/cmd/spy/spy.go | 2 +- node/pkg/p2p/p2p.go | 24 ++++++++++++++++-------- node/pkg/p2p/watermark_test.go | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index ae41469529..8994ef8696 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -28,7 +28,7 @@ func handleQueryRequests( chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, enableFlag bool, ) { - qLogger := logger.With(zap.String("component", "queryHandler")) + qLogger := logger.With(zap.String("component", "ccqhandler")) if enableFlag { qLogger.Info("cross chain queries are enabled") } @@ -39,7 +39,7 @@ func handleQueryRequests( return case signedQueryRequest := <-signedQueryReqC: if !enableFlag { - qLogger.Debug("dropping query request, feature is not enabled") + qLogger.Error("received a query request when the feature is disabled, dropping it") continue } // requestor validation happens here diff --git a/node/cmd/spy/spy.go b/node/cmd/spy/spy.go index 8262996601..c5b24492c1 100644 --- a/node/cmd/spy/spy.go +++ b/node/cmd/spy/spy.go @@ -356,7 +356,7 @@ func runSpy(cmd *cobra.Command, args []string) { components, nil, // ibc feature string false, // gateway relayer enabled - nil, // cross chain query feature string + false, // ccqEnabled nil, // query requests nil, // query responses diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 5fce20f192..3abea56666 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -210,7 +210,7 @@ func Run( components *Components, ibcFeaturesFunc func() string, gatewayRelayerEnabled bool, - ccqFeatures *string, + ccqEnabled bool, signedQueryReqC chan<- *gossipv1.SignedQueryRequest, queryResponseReadC <-chan *node_common.QueryResponsePublication, ) func(ctx context.Context) error { @@ -401,8 +401,8 @@ func Run( if gatewayRelayerEnabled { features = append(features, "gwrelayer") } - if ccqFeatures != nil && *ccqFeatures != "" { - features = append(features, *ccqFeatures) + if ccqEnabled { + features = append(features, "ccq") } heartbeat := &gossipv1.Heartbeat{ @@ -503,9 +503,13 @@ func Run( logger.Info("published signed observation request", zap.Any("signed_observation_request", sReq)) } case msg := <-queryResponseReadC: + if !ccqEnabled { + logger.Error("received a cross chain query response when the feature is disabled, dropping it", zap.String("component", "ccqp2p")) + continue + } msgBytes, err := msg.Marshal() if err != nil { - logger.Error("failed to marshal query response", zap.Error(err)) + logger.Error("failed to marshal query response", zap.Error(err), zap.String("component", "ccqp2p")) continue } digest := node_common.GetQueryResponseDigestFromBytes(msgBytes) @@ -528,9 +532,9 @@ func Run( err = th.Publish(ctx, b) p2pMessagesSent.Inc() if err != nil { - logger.Error("failed to publish query response", zap.Error(err)) + logger.Error("failed to publish query response", zap.Error(err), zap.String("component", "ccqp2p")) } else { - logger.Info("published signed query response", zap.Any("query_response", msg), zap.Any("signature", sig)) + logger.Info("published signed query response", zap.Any("query_response", msg), zap.Any("signature", sig), zap.String("component", "ccqp2p")) } } } @@ -692,8 +696,12 @@ func Run( } case *gossipv1.GossipMessage_SignedQueryRequest: if signedQueryReqC != nil { - if err := node_common.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { - logger.Warn("failed to handle query request", zap.Error(err)) + if ccqEnabled { + if err := node_common.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { + logger.Warn("failed to handle query request", zap.Error(err), zap.String("component", "ccqp2p")) + } + } else { + logger.Debug("dropping cross chain query request because the feature is not enabled", zap.String("component", "ccqp2p")) } } default: diff --git a/node/pkg/p2p/watermark_test.go b/node/pkg/p2p/watermark_test.go index 029db990d8..a298690ac1 100644 --- a/node/pkg/p2p/watermark_test.go +++ b/node/pkg/p2p/watermark_test.go @@ -185,7 +185,7 @@ func startGuardian(t *testing.T, ctx context.Context, g *G) { g.components, nil, // ibc feature string false, // gateway relayer enabled - nil, // cross chain query feature string + false, // ccqEnabled nil, // signed query request channel nil, // query response channel )) From 050b96edaacb0fef8ab75d45a0d43a1b25d0ea0f Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Thu, 25 May 2023 13:00:30 -0500 Subject: [PATCH 07/37] CCQ: Add ccqAllowedRequesters parameter (#2990) --- Tiltfile | 4 +++- node/cmd/guardiand/node.go | 4 ++++ node/cmd/guardiand/query.go | 38 ++++++++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Tiltfile b/Tiltfile index c41b2a1739..73db0fd7f2 100644 --- a/Tiltfile +++ b/Tiltfile @@ -308,7 +308,9 @@ def build_node_yaml(): if ccq: container["command"] += [ - "--ccqEnabled=true" + "--ccqEnabled=true", + "--ccqAllowedRequesters", + "beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe" ] return encode_yaml_stream(node_yaml_with_replicas) diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index 2c01293990..6c811bd1fe 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -204,7 +204,9 @@ var ( telemetryLokiURL *string chainGovernorEnabled *bool + ccqEnabled *bool + ccqAllowedRequesters *string gatewayRelayerContract *string gatewayRelayerKeyPath *string @@ -370,7 +372,9 @@ func init() { telemetryLokiURL = NodeCmd.Flags().String("telemetryLokiURL", "", "Loki cloud logging URL") chainGovernorEnabled = NodeCmd.Flags().Bool("chainGovernorEnabled", false, "Run the chain governor") + ccqEnabled = NodeCmd.Flags().Bool("ccqEnabled", false, "Enable cross chain query support") + ccqAllowedRequesters = NodeCmd.Flags().String("ccqAllowedRequesters", "", "Comma separated list of signers allowed to submit cross chain queries") gatewayRelayerContract = NodeCmd.Flags().String("gatewayRelayerContract", "", "Address of the smart contract on wormchain to receive relayed VAAs") gatewayRelayerKeyPath = NodeCmd.Flags().String("gatewayRelayerKeyPath", "", "Path to gateway relayer private key for signing transactions") diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 8994ef8696..823a1ac537 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -2,6 +2,8 @@ package guardiand import ( "context" + "fmt" + "strings" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" ethCommon "github.com/ethereum/go-ethereum/common" @@ -18,30 +20,22 @@ func queryRequestDigest(b []byte) ethCommon.Hash { return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) } -var allowedRequestor = ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")) - // Multiplex observation requests to the appropriate chain func handleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, - enableFlag bool, + allowedRequestors map[ethCommon.Address]struct{}, ) { qLogger := logger.With(zap.String("component", "ccqhandler")) - if enableFlag { - qLogger.Info("cross chain queries are enabled") - } + qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors)) for { select { case <-ctx.Done(): return case signedQueryRequest := <-signedQueryReqC: - if !enableFlag { - qLogger.Error("received a query request when the feature is disabled, dropping it") - continue - } // requestor validation happens here // request type validation is currently handled by the watcher // in the future, it may be worthwhile to catch certain types of @@ -61,7 +55,7 @@ func handleQueryRequests( signerAddress := ethCommon.BytesToAddress(ethCrypto.Keccak256(signerBytes[1:])[12:]) - if signerAddress != allowedRequestor { + if _, exists := allowedRequestors[signerAddress]; !exists { qLogger.Error("invalid requestor", zap.String("requestor", signerAddress.Hex())) continue } @@ -89,3 +83,25 @@ func handleQueryRequests( } } } + +func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { + if ccqAllowedRequesters == "" { + return nil, fmt.Errorf("if cross chain query is enabled `--ccqAllowedRequesters` must be specified") + } + + var nullAddr ethCommon.Address + result := make(map[ethCommon.Address]struct{}) + for _, str := range strings.Split(ccqAllowedRequesters, ",") { + addr := ethCommon.BytesToAddress(ethCommon.Hex2Bytes(str)) + if addr == nullAddr { + return nil, fmt.Errorf("invalid value in `--ccqAllowedRequesters`: `%s`", str) + } + result[addr] = struct{}{} + } + + if len(result) == 0 { + return nil, fmt.Errorf("no allowed requestors specified, ccqAllowedRequesters: `%s`", ccqAllowedRequesters) + } + + return result, nil +} From 947975f36b08132e4bf5259bd9c3c919ca950482 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 26 May 2023 09:17:35 -0500 Subject: [PATCH 08/37] CCQ: Use environment specific prefix to sign (#2994) Change-Id: Iaca52c115f6a851568b5e988610cbcbd205306cb --- node/cmd/guardiand/query.go | 11 +++-------- node/hack/query/send_req.go | 9 +-------- node/pkg/common/queryRequest.go | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 823a1ac537..7301beccb5 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/certusone/wormhole/node/pkg/common" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -13,13 +14,6 @@ import ( "google.golang.org/protobuf/proto" ) -// TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 -var queryRequestPrefix = []byte("query_request_00000000000000000000|") - -func queryRequestDigest(b []byte) ethCommon.Hash { - return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) -} - // Multiplex observation requests to the appropriate chain func handleQueryRequests( ctx context.Context, @@ -27,6 +21,7 @@ func handleQueryRequests( signedQueryReqC <-chan *gossipv1.SignedQueryRequest, chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, allowedRequestors map[ethCommon.Address]struct{}, + env common.Environment, ) { qLogger := logger.With(zap.String("component", "ccqhandler")) qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors)) @@ -45,7 +40,7 @@ func handleQueryRequests( // - length check on "to" address 20 bytes // - valid "block" strings - digest := queryRequestDigest(signedQueryRequest.QueryRequest) + digest := common.QueryRequestDigest(env, signedQueryRequest.QueryRequest) signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) if err != nil { diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index e91ee5ad0c..a2bd529fa7 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -19,7 +19,6 @@ import ( 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" @@ -38,12 +37,6 @@ import ( "google.golang.org/protobuf/proto" ) -var queryRequestPrefix = []byte("query_request_00000000000000000000|") - -func queryRequestDigest(b []byte) ethCommon.Hash { - return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) -} - // this script has to be run inside kubernetes since it relies on UDP // https://github.com/kubernetes/kubernetes/issues/47862 // kubectl --namespace=wormhole exec -it spy-0 -- sh -c "cd node/hack/query/ && go run send_req.go" @@ -212,7 +205,7 @@ func main() { } // Sign the query request using our private key. - digest := queryRequestDigest(queryRequestBytes) + digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) sig, err := ethCrypto.Sign(digest.Bytes(), sk) if err != nil { panic(err) diff --git a/node/pkg/common/queryRequest.go b/node/pkg/common/queryRequest.go index c29a788b05..4cc08566a7 100644 --- a/node/pkg/common/queryRequest.go +++ b/node/pkg/common/queryRequest.go @@ -2,10 +2,27 @@ package common import ( gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" ) const SignedQueryRequestChannelSize = 50 +func QueryRequestDigest(env Environment, b []byte) ethCommon.Hash { + // TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 + var queryRequestPrefix []byte + if env == MainNet { + queryRequestPrefix = []byte("mainnet_query_request_000000000000|") + } else if env == TestNet { + queryRequestPrefix = []byte("testnet_query_request_000000000000|") + } else { + queryRequestPrefix = []byte("devnet_query_request_0000000000000|") + } + + return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) +} + func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequest, req *gossipv1.SignedQueryRequest) error { select { case signedQueryReqSendC <- req: From b6787cc188b0f79a20f280714edbaa01d33fc1f4 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 26 May 2023 15:12:51 -0500 Subject: [PATCH 09/37] CCQ: Add request retries (#2995) * Node/CCQ: Make responses pass through handler * A work in progress * More tweaks * Minor tweaks * More tweaks --- node/cmd/guardiand/query.go | 142 +++++++++++++++++++++++++++++-- node/pkg/common/queryResponse.go | 20 +++++ node/pkg/p2p/p2p.go | 7 +- node/pkg/watchers/evm/watcher.go | 57 ++++++++----- 4 files changed, 197 insertions(+), 29 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 7301beccb5..f708a0b122 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -2,34 +2,69 @@ package guardiand import ( "context" + "encoding/hex" "fmt" "strings" + "time" "github.com/certusone/wormhole/node/pkg/common" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" "google.golang.org/protobuf/proto" ) -// Multiplex observation requests to the appropriate chain +const ( + // requestTimeout indicates how long before a request is considered to have timed out. + requestTimeout = 1 * time.Minute + + // retryInterval specifies how long we will wait between retry intervals. This is the interval of our ticker. + retryInterval = 10 * time.Second +) + +type ( + // pendingQuery is the cache entry for a given query. + pendingQuery struct { + req *gossipv1.SignedQueryRequest + reqId string + chainId vaa.ChainID + channel chan *gossipv1.SignedQueryRequest + receiveTime time.Time + lastUpdateTime time.Time + + // resp is only populated when we need to retry sending the response to p2p. + resp *common.QueryResponse + } +) + +// handleQueryRequests multiplexes observation requests to the appropriate chain func handleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, allowedRequestors map[ethCommon.Address]struct{}, + queryResponseReadC <-chan *common.QueryResponse, + queryResponseWriteC chan<- *common.QueryResponsePublication, env common.Environment, ) { qLogger := logger.With(zap.String("component", "ccqhandler")) qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors)) + pendingQueries := make(map[string]*pendingQuery) // Key is requestID. + + ticker := time.NewTicker(retryInterval) + defer ticker.Stop() + for { select { case <-ctx.Done(): return + case signedQueryRequest := <-signedQueryReqC: // requestor validation happens here // request type validation is currently handled by the watcher @@ -63,22 +98,89 @@ func handleQueryRequests( continue } - if channel, ok := chainQueryReqC[vaa.ChainID(queryRequest.ChainId)]; ok { + reqId := requestID(signedQueryRequest) + chainId := vaa.ChainID(queryRequest.ChainId) + + // Look up the channel for this chain. + channel, channelExists := chainQueryReqC[chainId] + if !channelExists { + qLogger.Error("unknown chain ID for query request, dropping it", zap.String("requestID", reqId), zap.Uint32("chain_id", queryRequest.ChainId)) + continue + } + + // Make sure this is not a duplicate request. TODO: Should we do something smarter here than just dropping the duplicate? + if oldReq, exists := pendingQueries[reqId]; exists { + qLogger.Warn("dropping duplicate query request", zap.String("requestID", reqId), zap.Stringer("origRecvTime", oldReq.receiveTime)) + continue + } + + // Add the query to our cache. + pq := &pendingQuery{ + req: signedQueryRequest, + reqId: reqId, + chainId: chainId, + channel: channel, + receiveTime: time.Now(), + } + pendingQueries[reqId] = pq + + // Forward the request to the watcher. + ccqForwardToWatcher(qLogger, pq) + + case resp := <-queryResponseReadC: + reqId := resp.RequestID() + if resp.Success { + // Send the response to be published. select { - // TODO: only send the query request itself and reassemble in this module - case channel <- signedQueryRequest: + case queryResponseWriteC <- resp.Msg: + qLogger.Debug("forwarded query response to p2p", zap.String("requestID", reqId)) + delete(pendingQueries, reqId) default: - qLogger.Warn("failed to send query request to watcher", - zap.Uint16("chain_id", uint16(queryRequest.ChainId))) + if pq, exists := pendingQueries[reqId]; exists { + qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", reqId)) + pq.resp = resp + } else { + qLogger.Warn("failed to publish query response to p2p, request is no longer in cache, dropping it", zap.String("requestID", reqId)) + delete(pendingQueries, reqId) + } } } else { - qLogger.Error("unknown chain ID for query request", - zap.Uint16("chain_id", uint16(queryRequest.ChainId))) + if _, exists := pendingQueries[reqId]; exists { + qLogger.Warn("query failed, will retry next interval", zap.String("requestID", reqId)) + } else { + qLogger.Warn("query failed, request is no longer in cache, dropping it", zap.String("requestID", reqId)) + } + } + + case <-ticker.C: + now := time.Now() + for reqId, pq := range pendingQueries { + timeout := pq.receiveTime.Add(requestTimeout) + qLogger.Debug("audit", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime), zap.Stringer("retryTime", pq.lastUpdateTime.Add(retryInterval)), zap.Stringer("timeout", timeout)) + if timeout.Before(now) { + qLogger.Warn("query request timed out, dropping it", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime)) + delete(pendingQueries, reqId) + } else { + if pq.resp != nil { + // Resend the response to be published. + select { + case queryResponseWriteC <- pq.resp.Msg: + qLogger.Debug("resend of query response to p2p succeeded", zap.String("requestID", reqId)) + delete(pendingQueries, reqId) + default: + qLogger.Warn("resend of query response to p2p failed again, will keep retrying", zap.String("requestID", reqId)) + } + } else if pq.lastUpdateTime.Add(retryInterval).Before(now) { + qLogger.Info("retrying query request", zap.String("requestId", pq.reqId), zap.Stringer("receiveTime", pq.receiveTime)) + ccqForwardToWatcher(qLogger, pq) + } + } } } } } +// ccqParseAllowedRequesters parses a comma separated list of allowed requesters into a map to be used for look ups. func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { if ccqAllowedRequesters == "" { return nil, fmt.Errorf("if cross chain query is enabled `--ccqAllowedRequesters` must be specified") @@ -100,3 +202,25 @@ func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Addre return result, nil } + +// ccqForwardToWatcher submits a query request to the appropriate watcher. It updates the request object if the write succeeds. +// If the write fails, it does not update the last update time, which will cause a retry next interval (until it times out) +func ccqForwardToWatcher(qLogger *zap.Logger, pq *pendingQuery) { + select { + // TODO: only send the query request itself and reassemble in this module + case pq.channel <- pq.req: + qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pq.reqId), zap.Stringer("chainID", pq.chainId)) + pq.lastUpdateTime = pq.receiveTime + default: + // By leaving lastUpdateTime unset, we will retry next interval. + qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pq.reqId), zap.Uint16("chain_id", uint16(pq.chainId))) + } +} + +// requestID returns the request signature as a hex string. +func requestID(req *gossipv1.SignedQueryRequest) string { + if req == nil { + return "nil" + } + return hex.EncodeToString(req.Signature) +} diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index 2fbc340aa6..b28a26c73f 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -3,6 +3,7 @@ package common import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "math" "math/big" @@ -15,6 +16,18 @@ import ( "google.golang.org/protobuf/proto" ) +type QueryResponse struct { + Success bool + Msg *QueryResponsePublication +} + +func (resp *QueryResponse) RequestID() string { + if resp == nil || resp.Msg == nil { + return "nil" + } + return resp.Msg.RequestID() +} + var queryResponsePrefix = []byte("query_response_0000000000000000000|") type EthCallQueryResponse struct { @@ -29,6 +42,13 @@ type QueryResponsePublication struct { Response EthCallQueryResponse } +func (resp *QueryResponsePublication) RequestID() string { + if resp == nil || resp.Request == nil { + return "nil" + } + return hex.EncodeToString(resp.Request.Signature) +} + // 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 diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 3abea56666..6d18899457 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -534,7 +534,12 @@ func Run( if err != nil { logger.Error("failed to publish query response", zap.Error(err), zap.String("component", "ccqp2p")) } else { - logger.Info("published signed query response", zap.Any("query_response", msg), zap.Any("signature", sig), zap.String("component", "ccqp2p")) + logger.Info("published signed query response", + zap.String("requestID", msg.RequestID()), + zap.Any("query_response", msg), + zap.Any("signature", sig), + zap.String("component", "ccqp2p"), + ) } } } diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index c6f3a0af87..3383797eae 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -100,7 +100,7 @@ type ( queryReqC <-chan *gossipv1.SignedQueryRequest // Outbound query responses to query requests - queryResponseC chan<- *common.QueryResponsePublication + queryResponseC chan<- *common.QueryResponse pending map[pendingKey]*pendingMessage pendingMu sync.Mutex @@ -152,7 +152,7 @@ func NewEthWatcher( setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, queryReqC <-chan *gossipv1.SignedQueryRequest, - queryResponseC chan<- *common.QueryResponsePublication, + queryResponseC chan<- *common.QueryResponse, unsafeDevMode bool, ) *Watcher { @@ -537,14 +537,14 @@ func (w *Watcher) Run(parentCtx context.Context) error { var queryRequest gossipv1.QueryRequest err := proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) if err != nil { - logger.Error("received invalid message from query module") + logger.Error("received invalid message from query module", zap.String("component", "ccqevm")) continue } // This can't happen unless there is a programming error - the caller // is expected to send us only requests for our chainID. if vaa.ChainID(queryRequest.ChainId) != w.chainID { - panic("invalid chain ID") + panic("ccqevm: invalid chain ID") } switch req := queryRequest.Message.(type) { @@ -556,7 +556,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) timeout, cancel := context.WithTimeout(ctx, 5*time.Second) // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 @@ -620,7 +622,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Error(err), zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) continue } @@ -629,7 +633,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Error(blockError), zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) continue } @@ -638,7 +644,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) continue } @@ -647,7 +655,9 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Error(callErr), zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) continue } @@ -658,17 +668,22 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), zap.Any("data", data), - zap.String("block", block)) + zap.String("block", block), + zap.String("component", "ccqevm"), + ) continue } - queryResponse := common.QueryResponsePublication{ - Request: signedQueryRequest, - Response: common.EthCallQueryResponse{ - Number: blockResult.Number.ToInt(), - Hash: blockResult.Hash, - Time: time.Unix(int64(blockResult.Time), 0), - Result: callResult, + queryResponse := common.QueryResponse{ + Success: true, + Msg: &common.QueryResponsePublication{ + Request: signedQueryRequest, + Response: common.EthCallQueryResponse{ + Number: blockResult.Number.ToInt(), + Hash: blockResult.Hash, + Time: time.Unix(int64(blockResult.Time), 0), + Result: callResult, + }, }, } @@ -680,12 +695,16 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("blockNumber", blockResult.Number.String()), zap.String("blockHash", blockResult.Hash.Hex()), zap.String("blockTime", blockResult.Time.String()), - zap.String("result", callResult.String())) + zap.String("result", callResult.String()), + zap.String("component", "ccqevm"), + ) w.queryResponseC <- &queryResponse default: logger.Warn("received unsupported request type", - zap.Any("payload", queryRequest.Message)) + zap.Any("payload", queryRequest.Message), + zap.String("component", "ccqevm"), + ) } } } From 4458d0a3a389cd4260da70b530efd2a3deedb96a Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 31 May 2023 07:42:23 -0500 Subject: [PATCH 10/37] CCQ: Improve retry logic (#3008) --- node/cmd/guardiand/query.go | 24 +++++++++++---- node/pkg/common/queryResponse.go | 18 ++++++++++-- node/pkg/watchers/evm/watcher.go | 50 +++++++++++++++++++++++--------- 3 files changed, 70 insertions(+), 22 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index f708a0b122..a88a49bda0 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -35,6 +35,7 @@ type ( channel chan *gossipv1.SignedQueryRequest receiveTime time.Time lastUpdateTime time.Time + inProgress bool // resp is only populated when we need to retry sending the response to p2p. resp *common.QueryResponse @@ -53,7 +54,7 @@ func handleQueryRequests( env common.Environment, ) { qLogger := logger.With(zap.String("component", "ccqhandler")) - qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors)) + qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors), zap.String("env", string(env))) pendingQueries := make(map[string]*pendingQuery) // Key is requestID. @@ -121,6 +122,7 @@ func handleQueryRequests( chainId: chainId, channel: channel, receiveTime: time.Now(), + inProgress: true, } pendingQueries[reqId] = pq @@ -129,7 +131,7 @@ func handleQueryRequests( case resp := <-queryResponseReadC: reqId := resp.RequestID() - if resp.Success { + if resp.Status == common.QuerySuccess { // Send the response to be published. select { case queryResponseWriteC <- resp.Msg: @@ -139,17 +141,25 @@ func handleQueryRequests( if pq, exists := pendingQueries[reqId]; exists { qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", reqId)) pq.resp = resp + pq.inProgress = false } else { qLogger.Warn("failed to publish query response to p2p, request is no longer in cache, dropping it", zap.String("requestID", reqId)) delete(pendingQueries, reqId) } } - } else { - if _, exists := pendingQueries[reqId]; exists { + } else if resp.Status == common.QueryRetryNeeded { + if pq, exists := pendingQueries[reqId]; exists { qLogger.Warn("query failed, will retry next interval", zap.String("requestID", reqId)) + pq.inProgress = false } else { qLogger.Warn("query failed, request is no longer in cache, dropping it", zap.String("requestID", reqId)) } + } else if resp.Status == common.QueryFatalError { + qLogger.Error("query encountered a fatal error, dropping it", zap.String("requestID", reqId)) + delete(pendingQueries, reqId) + } else { + qLogger.Error("received an unexpected query status, dropping it", zap.String("requestID", reqId), zap.Int("status", int(resp.Status))) + delete(pendingQueries, reqId) } case <-ticker.C: @@ -170,8 +180,9 @@ func handleQueryRequests( default: qLogger.Warn("resend of query response to p2p failed again, will keep retrying", zap.String("requestID", reqId)) } - } else if pq.lastUpdateTime.Add(retryInterval).Before(now) { + } else if !pq.inProgress && pq.lastUpdateTime.Add(retryInterval).Before(now) { qLogger.Info("retrying query request", zap.String("requestId", pq.reqId), zap.Stringer("receiveTime", pq.receiveTime)) + pq.inProgress = true ccqForwardToWatcher(qLogger, pq) } } @@ -212,8 +223,9 @@ func ccqForwardToWatcher(qLogger *zap.Logger, pq *pendingQuery) { qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pq.reqId), zap.Stringer("chainID", pq.chainId)) pq.lastUpdateTime = pq.receiveTime default: - // By leaving lastUpdateTime unset, we will retry next interval. + // By leaving lastUpdateTime unset and setting inProgress to false, we will retry next interval. qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pq.reqId), zap.Uint16("chain_id", uint16(pq.chainId))) + pq.inProgress = false } } diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index b28a26c73f..a42e0be0bf 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -16,9 +16,23 @@ import ( "google.golang.org/protobuf/proto" ) +// QueryStatus is the status returned from the watcher to the query handler. +type QueryStatus int + +const ( + // QuerySuccess means the query was successful and the response should be returned to the requester. + QuerySuccess QueryStatus = 1 + + // QueryRetryNeeded means the query failed, but a retry may be helpful. + QueryRetryNeeded QueryStatus = 0 + + // QueryFatalError means the query failed, and there is no point in retrying it. + QueryFatalError QueryStatus = -1 +) + type QueryResponse struct { - Success bool - Msg *QueryResponsePublication + Status QueryStatus + Msg *QueryResponsePublication } func (resp *QueryResponse) RequestID() string { diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 3383797eae..6c0546d278 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -538,6 +538,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { err := proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) if err != nil { logger.Error("received invalid message from query module", zap.String("component", "ccqevm")) + w.ccqSendQueryResponse(logger, common.QueryFatalError, signedQueryRequest, nil) continue } @@ -625,6 +626,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) continue } @@ -636,6 +638,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) continue } @@ -647,6 +650,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) continue } @@ -658,6 +662,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) continue } @@ -671,22 +676,10 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) continue } - queryResponse := common.QueryResponse{ - Success: true, - Msg: &common.QueryResponsePublication{ - Request: signedQueryRequest, - Response: common.EthCallQueryResponse{ - Number: blockResult.Number.ToInt(), - Hash: blockResult.Hash, - Time: time.Unix(int64(blockResult.Time), 0), - Result: callResult, - }, - }, - } - logger.Info("query result", zap.String("eth_network", w.networkName), zap.String("to", to.Hex()), @@ -699,12 +692,21 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("component", "ccqevm"), ) - w.queryResponseC <- &queryResponse + resp := &common.EthCallQueryResponse{ + Number: blockResult.Number.ToInt(), + Hash: blockResult.Hash, + Time: time.Unix(int64(blockResult.Time), 0), + Result: callResult, + } + + w.ccqSendQueryResponse(logger, common.QuerySuccess, signedQueryRequest, resp) + default: logger.Warn("received unsupported request type", zap.Any("payload", queryRequest.Message), zap.String("component", "ccqevm"), ) + w.ccqSendQueryResponse(logger, common.QueryFatalError, signedQueryRequest, nil) } } } @@ -1125,3 +1127,23 @@ func (w *Watcher) SetWaitForConfirmations(waitForConfirmations bool) { func (w *Watcher) SetMaxWaitConfirmations(maxWaitConfirmations uint64) { w.maxWaitConfirmations = maxWaitConfirmations } + +// ccqSendQueryResponse sends an error response back to the query handler. +func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, status common.QueryStatus, req *gossipv1.SignedQueryRequest, resp *common.EthCallQueryResponse) { + queryResponse := common.QueryResponse{ + Status: status, + Msg: &common.QueryResponsePublication{ + Request: req, + }, + } + + if resp != nil { + queryResponse.Msg.Response = *resp + } + select { + case w.queryResponseC <- &queryResponse: + logger.Debug("published query response error to handler", zap.String("component", "ccqevm")) + default: + logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm")) + } +} From dbcf8d5e00d3cddeccef00be7a7749f7ed7d4ba9 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 31 May 2023 17:08:42 -0500 Subject: [PATCH 11/37] CCQ: Validation and marshalling changes (#3017) --- node/cmd/guardiand/query.go | 101 +++++++++++++++---------------- node/pkg/common/queryRequest.go | 52 ++++++++++++++++ node/pkg/common/queryResponse.go | 47 +++++++------- node/pkg/watchers/evm/watcher.go | 51 +++++----------- 4 files changed, 141 insertions(+), 110 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index a88a49bda0..09e73da291 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -2,7 +2,6 @@ package guardiand import ( "context" - "encoding/hex" "fmt" "strings" "time" @@ -29,16 +28,14 @@ const ( type ( // pendingQuery is the cache entry for a given query. pendingQuery struct { - req *gossipv1.SignedQueryRequest - reqId string - chainId vaa.ChainID - channel chan *gossipv1.SignedQueryRequest + req *common.QueryRequest + channel chan *common.QueryRequest receiveTime time.Time lastUpdateTime time.Time inProgress bool - // resp is only populated when we need to retry sending the response to p2p. - resp *common.QueryResponse + // respPub is only populated when we need to retry sending the response to p2p. + respPub *common.QueryResponsePublication } ) @@ -47,7 +44,7 @@ func handleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *gossipv1.SignedQueryRequest, + chainQueryReqC map[vaa.ChainID]chan *common.QueryRequest, allowedRequestors map[ethCommon.Address]struct{}, queryResponseReadC <-chan *common.QueryResponse, queryResponseWriteC chan<- *common.QueryResponsePublication, @@ -91,75 +88,85 @@ func handleQueryRequests( continue } - var queryRequest gossipv1.QueryRequest - err = proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) + var qr gossipv1.QueryRequest + err = proto.Unmarshal(signedQueryRequest.QueryRequest, &qr) if err != nil { - qLogger.Error("received invalid message", - zap.String("requestor", signerAddress.Hex())) + qLogger.Error("failed to unmarshal query request", zap.String("requestor", signerAddress.Hex()), zap.Error(err)) continue } - reqId := requestID(signedQueryRequest) - chainId := vaa.ChainID(queryRequest.ChainId) + if err := common.ValidateQueryRequest(&qr); err != nil { + qLogger.Error("received invalid message", zap.String("requestor", signerAddress.Hex()), zap.Error(err)) + continue + } + + queryRequest := common.CreateQueryRequest(signedQueryRequest, &qr) // Look up the channel for this chain. - channel, channelExists := chainQueryReqC[chainId] + channel, channelExists := chainQueryReqC[queryRequest.ChainID] if !channelExists { - qLogger.Error("unknown chain ID for query request, dropping it", zap.String("requestID", reqId), zap.Uint32("chain_id", queryRequest.ChainId)) + qLogger.Error("unknown chain ID for query request, dropping it", zap.String("requestID", queryRequest.RequestID), zap.Stringer("chain_id", queryRequest.ChainID)) continue } // Make sure this is not a duplicate request. TODO: Should we do something smarter here than just dropping the duplicate? - if oldReq, exists := pendingQueries[reqId]; exists { - qLogger.Warn("dropping duplicate query request", zap.String("requestID", reqId), zap.Stringer("origRecvTime", oldReq.receiveTime)) + if oldReq, exists := pendingQueries[queryRequest.RequestID]; exists { + qLogger.Warn("dropping duplicate query request", zap.String("requestID", queryRequest.RequestID), zap.Stringer("origRecvTime", oldReq.receiveTime)) continue } // Add the query to our cache. pq := &pendingQuery{ - req: signedQueryRequest, - reqId: reqId, - chainId: chainId, + req: queryRequest, channel: channel, receiveTime: time.Now(), inProgress: true, } - pendingQueries[reqId] = pq + pendingQueries[queryRequest.RequestID] = pq // Forward the request to the watcher. ccqForwardToWatcher(qLogger, pq) case resp := <-queryResponseReadC: - reqId := resp.RequestID() if resp.Status == common.QuerySuccess { + if resp.Result == nil { + qLogger.Error("received a successful query response with a nil result, dropping it!", zap.String("requestID", resp.RequestID)) + continue + } + + respPub := &common.QueryResponsePublication{ + Request: resp.SignedRequest, + Response: *resp.Result, + } + // Send the response to be published. select { - case queryResponseWriteC <- resp.Msg: - qLogger.Debug("forwarded query response to p2p", zap.String("requestID", reqId)) - delete(pendingQueries, reqId) + case queryResponseWriteC <- respPub: + qLogger.Debug("forwarded query response to p2p", zap.String("requestID", resp.RequestID)) + delete(pendingQueries, resp.RequestID) default: - if pq, exists := pendingQueries[reqId]; exists { - qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", reqId)) - pq.resp = resp + if pq, exists := pendingQueries[resp.RequestID]; exists { + qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", resp.RequestID)) + pq.respPub = respPub pq.inProgress = false } else { - qLogger.Warn("failed to publish query response to p2p, request is no longer in cache, dropping it", zap.String("requestID", reqId)) - delete(pendingQueries, reqId) + qLogger.Warn("failed to publish query response to p2p, request is no longer in cache, dropping it", zap.String("requestID", resp.RequestID)) + delete(pendingQueries, resp.RequestID) } } } else if resp.Status == common.QueryRetryNeeded { - if pq, exists := pendingQueries[reqId]; exists { - qLogger.Warn("query failed, will retry next interval", zap.String("requestID", reqId)) + if pq, exists := pendingQueries[resp.RequestID]; exists { + qLogger.Warn("query failed, will retry next interval", zap.String("requestID", resp.RequestID)) pq.inProgress = false } else { - qLogger.Warn("query failed, request is no longer in cache, dropping it", zap.String("requestID", reqId)) + qLogger.Warn("query failed, request is no longer in cache, dropping it", zap.String("requestID", resp.RequestID)) } } else if resp.Status == common.QueryFatalError { - qLogger.Error("query encountered a fatal error, dropping it", zap.String("requestID", reqId)) - delete(pendingQueries, reqId) + qLogger.Error("query encountered a fatal error, dropping it", zap.String("requestID", resp.RequestID)) + delete(pendingQueries, resp.RequestID) } else { - qLogger.Error("received an unexpected query status, dropping it", zap.String("requestID", reqId), zap.Int("status", int(resp.Status))) - delete(pendingQueries, reqId) + qLogger.Error("received an unexpected query status, dropping it", zap.String("requestID", resp.RequestID), zap.Int("status", int(resp.Status))) + delete(pendingQueries, resp.RequestID) } case <-ticker.C: @@ -171,17 +178,17 @@ func handleQueryRequests( qLogger.Warn("query request timed out, dropping it", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime)) delete(pendingQueries, reqId) } else { - if pq.resp != nil { + if pq.respPub != nil { // Resend the response to be published. select { - case queryResponseWriteC <- pq.resp.Msg: + case queryResponseWriteC <- pq.respPub: qLogger.Debug("resend of query response to p2p succeeded", zap.String("requestID", reqId)) delete(pendingQueries, reqId) default: qLogger.Warn("resend of query response to p2p failed again, will keep retrying", zap.String("requestID", reqId)) } } else if !pq.inProgress && pq.lastUpdateTime.Add(retryInterval).Before(now) { - qLogger.Info("retrying query request", zap.String("requestId", pq.reqId), zap.Stringer("receiveTime", pq.receiveTime)) + qLogger.Info("retrying query request", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime)) pq.inProgress = true ccqForwardToWatcher(qLogger, pq) } @@ -220,19 +227,11 @@ func ccqForwardToWatcher(qLogger *zap.Logger, pq *pendingQuery) { select { // TODO: only send the query request itself and reassemble in this module case pq.channel <- pq.req: - qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pq.reqId), zap.Stringer("chainID", pq.chainId)) + qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pq.req.RequestID), zap.Stringer("chainID", pq.req.ChainID)) pq.lastUpdateTime = pq.receiveTime default: // By leaving lastUpdateTime unset and setting inProgress to false, we will retry next interval. - qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pq.reqId), zap.Uint16("chain_id", uint16(pq.chainId))) + qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pq.req.RequestID), zap.Stringer("chain_id", pq.req.ChainID)) pq.inProgress = false } } - -// requestID returns the request signature as a hex string. -func requestID(req *gossipv1.SignedQueryRequest) string { - if req == nil { - return "nil" - } - return hex.EncodeToString(req.Signature) -} diff --git a/node/pkg/common/queryRequest.go b/node/pkg/common/queryRequest.go index 4cc08566a7..f96be4cbb7 100644 --- a/node/pkg/common/queryRequest.go +++ b/node/pkg/common/queryRequest.go @@ -1,7 +1,13 @@ package common import ( + "encoding/hex" + "fmt" + "math" + "strings" + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/wormhole-foundation/wormhole/sdk/vaa" ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -9,6 +15,25 @@ import ( const SignedQueryRequestChannelSize = 50 +// QueryRequest is an internal representation of a query request. +type QueryRequest struct { + SignedRequest *gossipv1.SignedQueryRequest + Request *gossipv1.QueryRequest + RequestID string + ChainID vaa.ChainID +} + +// CreateQueryRequest creates a QueryRequest object from the signed query request. +func CreateQueryRequest(signedRequest *gossipv1.SignedQueryRequest, request *gossipv1.QueryRequest) *QueryRequest { + return &QueryRequest{ + SignedRequest: signedRequest, + Request: request, + RequestID: hex.EncodeToString(signedRequest.Signature), + ChainID: vaa.ChainID(request.ChainId), + } +} + +// QueryRequestDigest returns the query signing prefix based on the environment. func QueryRequestDigest(env Environment, b []byte) ethCommon.Hash { // TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 var queryRequestPrefix []byte @@ -23,6 +48,7 @@ func QueryRequestDigest(env Environment, b []byte) ethCommon.Hash { return ethCrypto.Keccak256Hash(append(queryRequestPrefix, b...)) } +// PostSignedQueryRequest posts a signed query request to the specified channel. func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequest, req *gossipv1.SignedQueryRequest) error { select { case signedQueryReqSendC <- req: @@ -31,3 +57,29 @@ func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequ return ErrChanFull } } + +// ValidateQueryRequest does basic validation on a received query request. +func ValidateQueryRequest(queryRequest *gossipv1.QueryRequest) error { + if queryRequest.ChainId > math.MaxUint16 { + return fmt.Errorf("invalid chain id: %d is out of bounds", queryRequest.ChainId) + } + switch req := queryRequest.Message.(type) { + case *gossipv1.QueryRequest_EthCallQueryRequest: + if len(req.EthCallQueryRequest.To) != 20 { + return fmt.Errorf("invalid length for To contract") + } + if len(req.EthCallQueryRequest.Data) > math.MaxUint32 { + return fmt.Errorf("request data too long") + } + if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { + return fmt.Errorf("request block too long") + } + if !strings.HasPrefix(req.EthCallQueryRequest.Block, "0x") { + return fmt.Errorf("request block must be a hex number or hash starting with 0x") + } + default: + return fmt.Errorf("received invalid message from query module") + } + + return nil +} diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index a42e0be0bf..ec148ca2e2 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -31,15 +31,21 @@ const ( ) type QueryResponse struct { - Status QueryStatus - Msg *QueryResponsePublication + RequestID string + ChainID vaa.ChainID + Status QueryStatus + SignedRequest *gossipv1.SignedQueryRequest + Result *EthCallQueryResponse } -func (resp *QueryResponse) RequestID() string { - if resp == nil || resp.Msg == nil { - return "nil" +func CreateQueryResponse(req *QueryRequest, status QueryStatus, result *EthCallQueryResponse) *QueryResponse { + return &QueryResponse{ + RequestID: req.RequestID, + ChainID: vaa.ChainID(req.Request.ChainId), + SignedRequest: req.SignedRequest, + Status: status, + Result: result, } - return resp.Msg.RequestID() } var queryResponsePrefix = []byte("query_response_0000000000000000000|") @@ -73,6 +79,17 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { return nil, fmt.Errorf("received invalid message from query module") } + if err := ValidateQueryRequest(&queryRequest); err != nil { + return nil, fmt.Errorf("queryRequest is invalid: %w", err) + } + + if len(msg.Response.Hash) != 32 { + return nil, fmt.Errorf("invalid length for block hash") + } + if len(msg.Response.Result) > math.MaxUint32 { + return nil, fmt.Errorf("response data too long") + } + buf := new(bytes.Buffer) // Source @@ -86,23 +103,11 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { switch req := queryRequest.Message.(type) { case *gossipv1.QueryRequest_EthCallQueryRequest: vaa.MustWrite(buf, binary.BigEndian, uint8(1)) - 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") - } buf.Write(req.EthCallQueryRequest.To) - if len(req.EthCallQueryRequest.Data) > math.MaxUint32 { - return nil, fmt.Errorf("request data too long") - } vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Data))) buf.Write(req.EthCallQueryRequest.Data) - if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { - return nil, fmt.Errorf("request block too long") - } vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Block))) // TODO: should this be an enum or the literal string? buf.Write([]byte(req.EthCallQueryRequest.Block)) @@ -111,14 +116,8 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { // TODO: probably some kind of request/response pair validation // 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(msg.Response.Time.Unix())) - if len(msg.Response.Result) > math.MaxUint32 { - return nil, fmt.Errorf("response data too long") - } vaa.MustWrite(buf, binary.BigEndian, uint32(len(msg.Response.Result))) buf.Write(msg.Response.Result) return buf.Bytes(), nil diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 6c0546d278..4e43b4c009 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -22,7 +22,6 @@ import ( eth_common "github.com/ethereum/go-ethereum/common" eth_hexutil "github.com/ethereum/go-ethereum/common/hexutil" "go.uber.org/zap" - "google.golang.org/protobuf/proto" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/readiness" @@ -97,7 +96,7 @@ type ( // Incoming query requests from the network. Pre-filtered to only // include requests for our chainID. - queryReqC <-chan *gossipv1.SignedQueryRequest + queryReqC <-chan *common.QueryRequest // Outbound query responses to query requests queryResponseC chan<- *common.QueryResponse @@ -151,7 +150,7 @@ func NewEthWatcher( msgC chan<- *common.MessagePublication, setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, - queryReqC <-chan *gossipv1.SignedQueryRequest, + queryReqC <-chan *common.QueryRequest, queryResponseC chan<- *common.QueryResponse, unsafeDevMode bool, ) *Watcher { @@ -532,23 +531,14 @@ func (w *Watcher) Run(parentCtx context.Context) error { select { case <-ctx.Done(): return nil - case signedQueryRequest := <-w.queryReqC: - // TODO: only receive the unmarshalled query request (see note in query.go) - var queryRequest gossipv1.QueryRequest - err := proto.Unmarshal(signedQueryRequest.QueryRequest, &queryRequest) - if err != nil { - logger.Error("received invalid message from query module", zap.String("component", "ccqevm")) - w.ccqSendQueryResponse(logger, common.QueryFatalError, signedQueryRequest, nil) - continue - } - + case queryRequest := <-w.queryReqC: // This can't happen unless there is a programming error - the caller // is expected to send us only requests for our chainID. - if vaa.ChainID(queryRequest.ChainId) != w.chainID { + if queryRequest.ChainID != w.chainID { panic("ccqevm: invalid chain ID") } - switch req := queryRequest.Message.(type) { + switch req := queryRequest.Request.Message.(type) { case *gossipv1.QueryRequest_EthCallQueryRequest: to := eth_common.BytesToAddress(req.EthCallQueryRequest.To) data := eth_hexutil.Encode(req.EthCallQueryRequest.Data) @@ -626,7 +616,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } @@ -638,7 +628,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } @@ -650,7 +640,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } @@ -662,7 +652,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } @@ -676,7 +666,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("block", block), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryRetryNeeded, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } @@ -699,14 +689,14 @@ func (w *Watcher) Run(parentCtx context.Context) error { Result: callResult, } - w.ccqSendQueryResponse(logger, common.QuerySuccess, signedQueryRequest, resp) + w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, resp) default: logger.Warn("received unsupported request type", - zap.Any("payload", queryRequest.Message), + zap.Any("payload", queryRequest.Request.Message), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, common.QueryFatalError, signedQueryRequest, nil) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryFatalError, nil) } } } @@ -1129,19 +1119,10 @@ func (w *Watcher) SetMaxWaitConfirmations(maxWaitConfirmations uint64) { } // ccqSendQueryResponse sends an error response back to the query handler. -func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, status common.QueryStatus, req *gossipv1.SignedQueryRequest, resp *common.EthCallQueryResponse) { - queryResponse := common.QueryResponse{ - Status: status, - Msg: &common.QueryResponsePublication{ - Request: req, - }, - } - - if resp != nil { - queryResponse.Msg.Response = *resp - } +func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.QueryRequest, status common.QueryStatus, result *common.EthCallQueryResponse) { + queryResponse := common.CreateQueryResponse(req, status, result) select { - case w.queryResponseC <- &queryResponse: + case w.queryResponseC <- queryResponse: logger.Debug("published query response error to handler", zap.String("component", "ccqevm")) default: logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm")) From 072022408580a040d90c8688fe1901fb8c311eb8 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Tue, 30 May 2023 17:54:47 +0000 Subject: [PATCH 12/37] CCQ: Added integration test --- Tiltfile | 16 +- devnet/node.yaml | 3 +- devnet/tests.yaml | 25 ++ node/hack/query/test/query_test.go | 349 ++++++++++++++++++ node/hack/query/test/test_query.sh | 4 + .../query/utils/fetchCurrentGuardianSet.go | 92 +++++ 6 files changed, 478 insertions(+), 11 deletions(-) create mode 100644 node/hack/query/test/query_test.go create mode 100644 node/hack/query/test/test_query.sh create mode 100644 node/hack/query/utils/fetchCurrentGuardianSet.go diff --git a/Tiltfile b/Tiltfile index 73db0fd7f2..159499c5f7 100644 --- a/Tiltfile +++ b/Tiltfile @@ -73,8 +73,6 @@ config.define_bool("ibc_relayer", False, "Enable IBC relayer between cosmos chai config.define_bool("redis", False, "Enable a redis instance") config.define_bool("generic_relayer", False, "Enable the generic relayer off-chain component") -config.define_bool("ccq", False, "Enable cross chain queries in guardiand") - cfg = config.parse() num_guardians = int(cfg.get("num", "1")) namespace = cfg.get("namespace", "wormhole") @@ -98,7 +96,6 @@ ibc_relayer = cfg.get("ibc_relayer", ci) btc = cfg.get("btc", False) redis = cfg.get('redis', ci) generic_relayer = cfg.get("generic_relayer", ci) -ccq = cfg.get("ccq", False) if ci: guardiand_loglevel = cfg.get("guardiand_loglevel", "warn") @@ -306,13 +303,6 @@ def build_node_yaml(): "http://wormchain:1317" ] - if ccq: - container["command"] += [ - "--ccqEnabled=true", - "--ccqAllowedRequesters", - "beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe" - ] - return encode_yaml_stream(node_yaml_with_replicas) k8s_yaml_with_ns(build_node_yaml()) @@ -593,6 +583,12 @@ if ci_tests: trigger_mode = trigger_mode, resource_deps = [], # uses devnet-consts.json, but wormchain/contracts/tools/test_accountant.sh handles waiting for guardian, not having deps gets the build earlier ) + k8s_resource( + "query-ci-tests", + labels = ["ci"], + trigger_mode = trigger_mode, + resource_deps = [], # node/hack/query/test/test_query.sh handles waiting for guardian, not having deps gets the build earlier + ) if terra_classic: docker_build( diff --git a/devnet/node.yaml b/devnet/node.yaml index 7a5efaeeb0..2c47d84775 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -163,7 +163,8 @@ spec: - --publicRpcLogDetail - "full" # - --chainGovernorEnabled=true - # - --ccqEnabled=true + - --ccqEnabled=true + - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe # - --logLevel=debug securityContext: capabilities: diff --git a/devnet/tests.yaml b/devnet/tests.yaml index 6c70bdcb27..56d640a866 100644 --- a/devnet/tests.yaml +++ b/devnet/tests.yaml @@ -72,3 +72,28 @@ spec: - "/app/accountant/success" initialDelaySeconds: 5 periodSeconds: 5 +--- +kind: Job +apiVersion: batch/v1 +metadata: + name: query-ci-tests +spec: + backoffLimit: 0 + template: + spec: + restartPolicy: Never + containers: + - name: query-ci-tests + image: guardiand-image + command: + - /bin/sh + - -c + - "cd /app/node/hack/query/test && bash test_query.sh && touch success" + readinessProbe: + exec: + command: + - test + - -e + - "/app/node/hack/query/test/success" + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go new file mode 100644 index 0000000000..d0c04b1401 --- /dev/null +++ b/node/hack/query/test/query_test.go @@ -0,0 +1,349 @@ +package query_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + + "github.com/certusone/wormhole/node/hack/query/utils" + "github.com/certusone/wormhole/node/pkg/common" + "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" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/libp2p/go-libp2p/core/routing" + libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" + libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "github.com/multiformats/go-multiaddr" + "go.uber.org/zap" + "golang.org/x/crypto/openpgp/armor" //nolint + "google.golang.org/protobuf/proto" +) + +func TestCrossChainQuery(t *testing.T) { + if os.Getenv("INTEGRATION") == "" { + t.Skip("Skipping integration test, set environment variable INTEGRATION") + } + + p2pNetworkID := "/wormhole/dev" + var p2pPort uint = 8997 + p2pBootstrap := "/dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" + nodeKeyPath := "/tmp/querier.key" + + ctx := context.Background() + logger, _ := zap.NewDevelopment() + + signingKeyPath := string("../dev.guardian.key") + + logger.Info("Loading signing key", zap.String("signingKeyPath", signingKeyPath)) + sk, err := loadGuardianKey(signingKeyPath) + if err != nil { + logger.Fatal("failed to load guardian key", zap.Error(err)) + } + logger.Info("Signing key loaded", zap.String("publicKey", ethCrypto.PubkeyToAddress(sk.PublicKey).Hex())) + + // Fetch the current guardian set + idx, sgs, err := utils.FetchCurrentGuardianSet(common.GoTest) + if err != nil { + logger.Fatal("Failed to fetch current guardian set", zap.Error(err)) + } + gs := common.GuardianSet{ + Keys: sgs.Keys, + Index: idx, + } + + // Fetch the latest block number + blockNum, err := utils.FetchLatestBlockNumber(ctx, common.GoTest) + if err != nil { + logger.Fatal("Failed to fetch latest block number", zap.Error(err)) + } + + // Load p2p private key + var priv crypto.PrivKey + priv, err = common.GetOrCreateNodeKey(logger, nodeKeyPath) + if err != nil { + logger.Fatal("Failed to load node key", zap.Error(err)) + } + + // Manual p2p setup + components := p2p.DefaultComponents() + components.Port = p2pPort + bootstrapPeers := p2pBootstrap + networkID := p2pNetworkID + h, err := libp2p.New( + // Use the keypair we generated + libp2p.Identity(priv), + + // Multiple listen addresses + libp2p.ListenAddrStrings( + components.ListeningAddresses()..., + ), + + // Enable TLS security as the only security protocol. + libp2p.Security(libp2ptls.ID, libp2ptls.New), + + // Enable QUIC transport as the only transport. + libp2p.Transport(libp2pquic.NewTransport), + + // Let's prevent our peer from having too many + // connections by attaching a connection manager. + libp2p.ConnectionManager(components.ConnMgr), + + // Let this host use the DHT to find other hosts + libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { + logger.Info("Connecting to bootstrap peers", zap.String("bootstrap_peers", bootstrapPeers)) + bootstrappers := make([]peer.AddrInfo, 0) + for _, addr := range strings.Split(bootstrapPeers, ",") { + if addr == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + if pi.ID == h.ID() { + logger.Info("We're a bootstrap node") + continue + } + bootstrappers = append(bootstrappers, *pi) + } + // TODO(leo): Persistent data store (i.e. address book) + idht, err := dht.New(ctx, h, dht.Mode(dht.ModeServer), + // This intentionally makes us incompatible with the global IPFS DHT + dht.ProtocolPrefix(protocol.ID("/"+networkID)), + dht.BootstrapPeers(bootstrappers...), + ) + return idht, err + }), + ) + + if err != nil { + panic(err) + } + + topic := fmt.Sprintf("%s/%s", networkID, "broadcast") + + logger.Info("Subscribing pubsub topic", zap.String("topic", topic)) + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + panic(err) + } + + th, err := ps.Join(topic) + if err != nil { + logger.Panic("failed to join topic", zap.Error(err)) + } + + sub, err := th.Subscribe() + if err != nil { + logger.Panic("failed to subscribe topic", zap.Error(err)) + } + + logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), + zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) + + // Wait for peers + for len(th.ListPeers()) < 1 { + time.Sleep(time.Millisecond * 100) + } + + 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 := "name" + data, err := wethAbi.Pack(methodName) + if err != nil { + panic(err) + } + to, _ := hex.DecodeString("DDb64fE46a91D46ee29420539FC25FD07c5FEa3E") // WETH + callRequest := &gossipv1.EthCallQueryRequest{ + To: to, + Data: data, + Block: hexutil.EncodeBig(blockNum), + } + queryRequest := &gossipv1.QueryRequest{ + ChainId: 2, + Nonce: 0, + Message: &gossipv1.QueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest}} + + queryRequestBytes, err := proto.Marshal(queryRequest) + if err != nil { + panic(err) + } + + // Sign the query request using our private key. + digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) + sig, err := ethCrypto.Sign(digest.Bytes(), sk) + if err != nil { + panic(err) + } + + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig, + } + + msg := gossipv1.GossipMessage{ + Message: &gossipv1.GossipMessage_SignedQueryRequest{ + SignedQueryRequest: signedQueryRequest, + }, + } + + b, err := proto.Marshal(&msg) + if err != nil { + panic(err) + } + + err = th.Publish(ctx, b) + if err != nil { + panic(err) + } + + logger.Info("Waiting for message...") + var success bool + signers := map[int]bool{} + subCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + for { + envelope, err := sub.Next(subCtx) + if err != nil { + break + } + var msg gossipv1.GossipMessage + err = proto.Unmarshal(envelope.Data, &msg) + if err != nil { + logger.Fatal("received invalid message", + zap.Binary("data", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + } + switch m := msg.Message.(type) { + case *gossipv1.GossipMessage_SignedQueryResponse: + logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) + response, err := common.UnmarshalQueryResponsePublication(m.SignedQueryResponse.QueryResponse) + if err != nil { + logger.Fatal("failed to unmarshal response", zap.Error(err)) + } + if bytes.Equal(response.Request.QueryRequest, queryRequestBytes) && bytes.Equal(response.Request.Signature, sig) { + digest := common.GetQueryResponseDigestFromBytes(m.SignedQueryResponse.QueryResponse) + signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), m.SignedQueryResponse.Signature) + if err != nil { + logger.Fatal("failed to verify signature on response", + zap.String("digest", digest.Hex()), + zap.String("signature", hex.EncodeToString(m.SignedQueryResponse.Signature)), + zap.Error(err)) + } + signerAddress := ethCommon.BytesToAddress(ethCrypto.Keccak256(signerBytes[1:])[12:]) + if keyIdx, ok := gs.KeyIndex(signerAddress); !ok { + logger.Fatal("received observation by unknown guardian - is our guardian set outdated?", + zap.String("digest", digest.Hex()), + zap.String("address", signerAddress.Hex()), + zap.Uint32("index", gs.Index), + zap.Any("keys", gs.KeysAsHexStrings()), + ) + } else { + signers[keyIdx] = true + } + quorum := vaa.CalculateQuorum(len(gs.Keys)) + if len(signers) < quorum { + logger.Sugar().Infof("not enough signers, have %d need %d", len(signers), quorum) + continue + } + + result, err := wethAbi.Methods[methodName].Outputs.Unpack(response.Response.Result) + if err != nil { + logger.Fatal("failed to unpack result", zap.Error(err)) + } + + 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)) + + success = true + } + default: + continue + } + if success { + break + } + } + + assert.True(t, success) + + // Cleanly shutdown + // Without this the same host won't properly discover peers until some timeout + sub.Cancel() + if err := th.Close(); err != nil { + logger.Error("Error closing the topic", zap.Error(err)) + } + if err := h.Close(); err != nil { + logger.Error("Error closing the host", zap.Error(err)) + } +} + +const ( + GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" +) + +// loadGuardianKey loads a serialized guardian key from disk. +func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { + f, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + p, err := armor.Decode(f) + if err != nil { + return nil, fmt.Errorf("failed to read armored file: %w", err) + } + + if p.Type != GuardianKeyArmoredBlock { + return nil, fmt.Errorf("invalid block type: %s", p.Type) + } + + b, err := io.ReadAll(p.Body) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var m nodev1.GuardianKey + err = proto.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) + } + + gk, err := ethCrypto.ToECDSA(m.Data) + if err != nil { + return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) + } + + return gk, nil +} diff --git a/node/hack/query/test/test_query.sh b/node/hack/query/test/test_query.sh new file mode 100644 index 0000000000..0e23b9b0a4 --- /dev/null +++ b/node/hack/query/test/test_query.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' guardian:6060/readyz)" != "200" ]]; do sleep 5; done +INTEGRATION=true go test -v . diff --git a/node/hack/query/utils/fetchCurrentGuardianSet.go b/node/hack/query/utils/fetchCurrentGuardianSet.go new file mode 100644 index 0000000000..9e6e2152c0 --- /dev/null +++ b/node/hack/query/utils/fetchCurrentGuardianSet.go @@ -0,0 +1,92 @@ +package utils + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/certusone/wormhole/node/pkg/common" + "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi" + 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" + ethClient "github.com/ethereum/go-ethereum/ethclient" + ethRpc "github.com/ethereum/go-ethereum/rpc" +) + +func GetRpcUrl(network common.Environment) string { + switch network { + case common.MainNet: + return "https://rpc.ankr.com/eth" + case common.TestNet: + return "https://rpc.ankr.com/eth_goerli" + case common.UnsafeDevNet: + return "http://localhost:8545" + case common.GoTest: + return "http://eth-devnet:8545" + default: + return "" + } +} + +func FetchLatestBlockNumber(ctx context.Context, network common.Environment) (*big.Int, error) { + rawUrl := GetRpcUrl(network) + if rawUrl == "" { + return nil, fmt.Errorf("unable to get rpc url") + } + rawClient, err := ethRpc.DialContext(ctx, rawUrl) + if err != nil { + return nil, fmt.Errorf("unable to dial eth context: %w", err) + } + client := ethClient.NewClient(rawClient) + header, err := client.HeaderByNumber(ctx, nil) + if err != nil { + return nil, fmt.Errorf("unable to fetch latest header: %w", err) + } + return header.Number, nil +} + +func FetchCurrentGuardianSet(network common.Environment) (uint32, *ethabi.StructsGuardianSet, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + rawUrl := GetRpcUrl(network) + if rawUrl == "" { + return 0, nil, fmt.Errorf("unable to get rpc url") + } + var ethContract string + switch network { + case common.MainNet: + ethContract = "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" + case common.TestNet: + ethContract = "0x706abc4E45D419950511e474C7B9Ed348A4a716c" + case common.UnsafeDevNet: + case common.GoTest: + ethContract = "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" + default: + return 0, nil, fmt.Errorf("unable to fetch guardian set for unknown network %s", network) + } + + contract := eth_common.HexToAddress(ethContract) + rawClient, err := ethRpc.DialContext(ctx, rawUrl) + if err != nil { + return 0, nil, fmt.Errorf("failed to connect to ethereum") + } + client := ethClient.NewClient(rawClient) + caller, err := ethAbi.NewAbiCaller(contract, client) + if err != nil { + return 0, nil, fmt.Errorf("failed to create caller") + } + currentIndex, err := caller.GetCurrentGuardianSetIndex(ðBind.CallOpts{Context: ctx}) + if err != nil { + return 0, nil, fmt.Errorf("error requesting current guardian set index: %w", err) + } + + gs, err := caller.GetGuardianSet(ðBind.CallOpts{Context: ctx}, currentIndex) + if err != nil { + return 0, nil, fmt.Errorf("error requesting current guardian set value: %w", err) + } + + return currentIndex, &gs, nil +} From 20799c8638813bfa75e3efd07be1af631afaabaf Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:14:45 -0500 Subject: [PATCH 13/37] CCQ: Batch queries (#3023) * CCQ: Add marshalling tests * More marshalling changes * Start of support for multiple calls in a batch * Multiple responses to go with multiple requests * Support multiple EVM queries in a batch * Multiple queries in a batch * Start of handler tests * More handler tests * More handler tests * node: add multi batch query tests * Added comments to tests * Per chain query time out not getting retried * node: chang rand package * Tweak serialization of per call request --------- Co-authored-by: Paul Noel --- node/cmd/guardiand/query.go | 260 +++++-- node/cmd/guardiand/query_test.go | 725 ++++++++++++++++++ node/hack/query/send_req.go | 180 +++-- node/hack/query/test/query_test.go | 53 +- .../query/utils/fetchCurrentGuardianSet.go | 4 + node/pkg/common/queryRequest.go | 261 ++++++- node/pkg/common/queryResponse.go | 315 +++++--- node/pkg/common/query_test.go | 181 +++++ node/pkg/p2p/p2p.go | 2 +- node/pkg/proto/gossip/v1/gossip.pb.go | 350 ++++++--- node/pkg/watchers/evm/watcher.go | 186 +++-- proto/gossip/v1/gossip.proto | 16 +- 12 files changed, 2062 insertions(+), 471 deletions(-) create mode 100644 node/cmd/guardiand/query_test.go create mode 100644 node/pkg/common/query_test.go diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 09e73da291..1fd86be287 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -2,6 +2,7 @@ package guardiand import ( "context" + "encoding/hex" "fmt" "strings" "time" @@ -28,15 +29,23 @@ const ( type ( // pendingQuery is the cache entry for a given query. pendingQuery struct { - req *common.QueryRequest - channel chan *common.QueryRequest - receiveTime time.Time - lastUpdateTime time.Time - inProgress bool + signedRequest *gossipv1.SignedQueryRequest + request *gossipv1.QueryRequest + requestID string + receiveTime time.Time + queries []*perChainQuery + responses []*common.PerChainQueryResponseInternal // respPub is only populated when we need to retry sending the response to p2p. respPub *common.QueryResponsePublication } + + // perChainQuery is the data associated with a single per chain query in a query request. + perChainQuery struct { + req *common.PerChainQueryInternal + channel chan *common.PerChainQueryInternal + lastUpdateTime time.Time + } ) // handleQueryRequests multiplexes observation requests to the appropriate chain @@ -44,18 +53,55 @@ func handleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *common.QueryRequest, + chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal, + allowedRequestors map[ethCommon.Address]struct{}, + queryResponseReadC <-chan *common.PerChainQueryResponseInternal, + queryResponseWriteC chan<- *common.QueryResponsePublication, + env common.Environment, +) { + handleQueryRequestsImpl(ctx, logger, signedQueryReqC, chainQueryReqC, allowedRequestors, queryResponseReadC, queryResponseWriteC, env, requestTimeout, retryInterval) +} + +// handleQueryRequestsImpl allows instantiating the handler in the test environment with shorter timeout and retry parameters. +func handleQueryRequestsImpl( + ctx context.Context, + logger *zap.Logger, + signedQueryReqC <-chan *gossipv1.SignedQueryRequest, + chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal, allowedRequestors map[ethCommon.Address]struct{}, - queryResponseReadC <-chan *common.QueryResponse, + queryResponseReadC <-chan *common.PerChainQueryResponseInternal, queryResponseWriteC chan<- *common.QueryResponsePublication, env common.Environment, + requestTimeoutImpl time.Duration, + retryIntervalImpl time.Duration, ) { qLogger := logger.With(zap.String("component", "ccqhandler")) qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors), zap.String("env", string(env))) pendingQueries := make(map[string]*pendingQuery) // Key is requestID. - ticker := time.NewTicker(retryInterval) + // TODO: This should only include watchers that are actually running. Also need to test all these chains. + supportedChains := map[vaa.ChainID]struct{}{ + vaa.ChainIDEthereum: {}, + vaa.ChainIDBSC: {}, + vaa.ChainIDPolygon: {}, + vaa.ChainIDAvalanche: {}, + vaa.ChainIDOasis: {}, + vaa.ChainIDAurora: {}, + vaa.ChainIDFantom: {}, + vaa.ChainIDKarura: {}, + vaa.ChainIDAcala: {}, + vaa.ChainIDKlaytn: {}, + vaa.ChainIDCelo: {}, + vaa.ChainIDMoonbeam: {}, + vaa.ChainIDNeon: {}, + vaa.ChainIDArbitrum: {}, + vaa.ChainIDOptimism: {}, + vaa.ChainIDBase: {}, + vaa.ChainIDSepolia: {}, + } + + ticker := time.NewTicker(retryIntervalImpl) defer ticker.Stop() for { @@ -63,7 +109,7 @@ func handleQueryRequests( case <-ctx.Done(): return - case signedQueryRequest := <-signedQueryReqC: + case signedRequest := <-signedQueryReqC: // Inbound query request. // requestor validation happens here // request type validation is currently handled by the watcher // in the future, it may be worthwhile to catch certain types of @@ -73,107 +119,169 @@ func handleQueryRequests( // - length check on "to" address 20 bytes // - valid "block" strings - digest := common.QueryRequestDigest(env, signedQueryRequest.QueryRequest) + requestID := hex.EncodeToString(signedRequest.Signature) + digest := common.QueryRequestDigest(env, signedRequest.QueryRequest) - signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedQueryRequest.Signature) + signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedRequest.Signature) if err != nil { - qLogger.Error("failed to recover public key") + qLogger.Error("failed to recover public key", zap.String("requestID", requestID)) continue } signerAddress := ethCommon.BytesToAddress(ethCrypto.Keccak256(signerBytes[1:])[12:]) if _, exists := allowedRequestors[signerAddress]; !exists { - qLogger.Error("invalid requestor", zap.String("requestor", signerAddress.Hex())) + qLogger.Error("invalid requestor", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID)) + continue + } + + // Make sure this is not a duplicate request. TODO: Should we do something smarter here than just dropping the duplicate? + if oldReq, exists := pendingQueries[requestID]; exists { + qLogger.Warn("dropping duplicate query request", zap.String("requestID", requestID), zap.Stringer("origRecvTime", oldReq.receiveTime)) continue } - var qr gossipv1.QueryRequest - err = proto.Unmarshal(signedQueryRequest.QueryRequest, &qr) + var queryRequest gossipv1.QueryRequest + err = proto.Unmarshal(signedRequest.QueryRequest, &queryRequest) if err != nil { - qLogger.Error("failed to unmarshal query request", zap.String("requestor", signerAddress.Hex()), zap.Error(err)) + qLogger.Error("failed to unmarshal query request", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID), zap.Error(err)) continue } - if err := common.ValidateQueryRequest(&qr); err != nil { - qLogger.Error("received invalid message", zap.String("requestor", signerAddress.Hex()), zap.Error(err)) + if err := common.ValidateQueryRequest(&queryRequest); err != nil { + qLogger.Error("received invalid message", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID), zap.Error(err)) continue } - queryRequest := common.CreateQueryRequest(signedQueryRequest, &qr) + // Build the set of per chain queries and placeholders for the per chain responses. + errorFound := false + queries := []*perChainQuery{} + responses := make([]*common.PerChainQueryResponseInternal, len(queryRequest.PerChainQueries)) + receiveTime := time.Now() + + for requestIdx, pcq := range queryRequest.PerChainQueries { + chainID := vaa.ChainID(pcq.ChainId) + if _, exists := supportedChains[chainID]; !exists { + qLogger.Error("chain does not support cross chain queries", zap.String("requestID", requestID), zap.Stringer("chainID", chainID)) + errorFound = true + break + } - // Look up the channel for this chain. - channel, channelExists := chainQueryReqC[queryRequest.ChainID] - if !channelExists { - qLogger.Error("unknown chain ID for query request, dropping it", zap.String("requestID", queryRequest.RequestID), zap.Stringer("chain_id", queryRequest.ChainID)) - continue + channel, channelExists := chainQueryReqC[chainID] + if !channelExists { + qLogger.Error("unknown chain ID for query request, dropping it", zap.String("requestID", requestID), zap.Stringer("chain_id", chainID)) + errorFound = true + break + } + + queries = append(queries, &perChainQuery{ + req: &common.PerChainQueryInternal{ + RequestID: requestID, + RequestIdx: requestIdx, + ChainID: chainID, + Request: pcq, + }, + channel: channel, + }) } - // Make sure this is not a duplicate request. TODO: Should we do something smarter here than just dropping the duplicate? - if oldReq, exists := pendingQueries[queryRequest.RequestID]; exists { - qLogger.Warn("dropping duplicate query request", zap.String("requestID", queryRequest.RequestID), zap.Stringer("origRecvTime", oldReq.receiveTime)) + if errorFound { continue } - // Add the query to our cache. + // Create the pending query and add it to the cache. pq := &pendingQuery{ - req: queryRequest, - channel: channel, - receiveTime: time.Now(), - inProgress: true, + signedRequest: signedRequest, + request: &queryRequest, + requestID: requestID, + receiveTime: receiveTime, + queries: queries, + responses: responses, } - pendingQueries[queryRequest.RequestID] = pq + pendingQueries[requestID] = pq - // Forward the request to the watcher. - ccqForwardToWatcher(qLogger, pq) + // Forward the requests to the watchers. + for _, pcq := range pq.queries { + pcq.ccqForwardToWatcher(qLogger, pq.receiveTime) + } - case resp := <-queryResponseReadC: + case resp := <-queryResponseReadC: // Response from a watcher. if resp.Status == common.QuerySuccess { - if resp.Result == nil { - qLogger.Error("received a successful query response with a nil result, dropping it!", zap.String("requestID", resp.RequestID)) + if len(resp.Results) == 0 { + qLogger.Error("received a successful query response with no results, dropping it!", zap.String("requestID", resp.RequestID)) continue } + pq, exists := pendingQueries[resp.RequestID] + if !exists { + qLogger.Warn("received a success response with no outstanding query, dropping it", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) + continue + } + + if resp.RequestIdx >= len(pq.responses) { + qLogger.Error("received a response with an invalid index", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) + continue + } + + // Store the result, which will mark this per-chain query as completed. + pq.responses[resp.RequestIdx] = resp + + // If we still have other outstanding per chain queries for this request, keep waiting. + numStillPending := pq.numPendingRequests() + if numStillPending > 0 { + qLogger.Info("received a per chain query response, still waiting for more", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx), zap.Int("numStillPending", numStillPending)) + continue + } else { + qLogger.Info("received final per chain query response, ready to publish", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) + } + + // Build the list of per chain response publications and the overall query response publication. + responses := []common.PerChainQueryResponse{} + for _, resp := range pq.responses { + if resp == nil { + qLogger.Error("unexpected null response in pending query!", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) + continue + } + + responses = append(responses, common.PerChainQueryResponse{ + ChainID: uint32(resp.ChainID), + Responses: resp.Results, + }) + } + respPub := &common.QueryResponsePublication{ - Request: resp.SignedRequest, - Response: *resp.Result, + Request: pq.signedRequest, + PerChainResponses: responses, } // Send the response to be published. select { case queryResponseWriteC <- respPub: - qLogger.Debug("forwarded query response to p2p", zap.String("requestID", resp.RequestID)) + qLogger.Info("forwarded query response to p2p", zap.String("requestID", resp.RequestID)) delete(pendingQueries, resp.RequestID) default: - if pq, exists := pendingQueries[resp.RequestID]; exists { - qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", resp.RequestID)) - pq.respPub = respPub - pq.inProgress = false - } else { - qLogger.Warn("failed to publish query response to p2p, request is no longer in cache, dropping it", zap.String("requestID", resp.RequestID)) - delete(pendingQueries, resp.RequestID) - } + qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", resp.RequestID)) + pq.respPub = respPub } } else if resp.Status == common.QueryRetryNeeded { - if pq, exists := pendingQueries[resp.RequestID]; exists { - qLogger.Warn("query failed, will retry next interval", zap.String("requestID", resp.RequestID)) - pq.inProgress = false + if _, exists := pendingQueries[resp.RequestID]; exists { + qLogger.Warn("query failed, will retry next interval", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) } else { - qLogger.Warn("query failed, request is no longer in cache, dropping it", zap.String("requestID", resp.RequestID)) + qLogger.Warn("received a retry needed response with no outstanding query, dropping it", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) } } else if resp.Status == common.QueryFatalError { - qLogger.Error("query encountered a fatal error, dropping it", zap.String("requestID", resp.RequestID)) + qLogger.Warn("received a fatal error response, dropping the whole request", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) delete(pendingQueries, resp.RequestID) } else { - qLogger.Error("received an unexpected query status, dropping it", zap.String("requestID", resp.RequestID), zap.Int("status", int(resp.Status))) + qLogger.Warn("received an unexpected query status, dropping the whole request", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx), zap.Int("status", int(resp.Status))) delete(pendingQueries, resp.RequestID) } - case <-ticker.C: + case <-ticker.C: // Retry audit timer. now := time.Now() for reqId, pq := range pendingQueries { - timeout := pq.receiveTime.Add(requestTimeout) - qLogger.Debug("audit", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime), zap.Stringer("retryTime", pq.lastUpdateTime.Add(retryInterval)), zap.Stringer("timeout", timeout)) + timeout := pq.receiveTime.Add(requestTimeoutImpl) + qLogger.Debug("audit", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime), zap.Stringer("timeout", timeout)) if timeout.Before(now) { qLogger.Warn("query request timed out, dropping it", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime)) delete(pendingQueries, reqId) @@ -187,10 +295,13 @@ func handleQueryRequests( default: qLogger.Warn("resend of query response to p2p failed again, will keep retrying", zap.String("requestID", reqId)) } - } else if !pq.inProgress && pq.lastUpdateTime.Add(retryInterval).Before(now) { - qLogger.Info("retrying query request", zap.String("requestId", reqId), zap.Stringer("receiveTime", pq.receiveTime)) - pq.inProgress = true - ccqForwardToWatcher(qLogger, pq) + } else { + for requestIdx, pcq := range pq.queries { + if pq.responses[requestIdx] == nil && pcq.lastUpdateTime.Add(retryIntervalImpl).Before(now) { + qLogger.Info("retrying query request", zap.String("requestId", reqId), zap.Int("requestIdx", requestIdx), zap.Stringer("receiveTime", pq.receiveTime), zap.Stringer("lastUpdateTime", pcq.lastUpdateTime)) + pcq.ccqForwardToWatcher(qLogger, pq.receiveTime) + } + } } } } @@ -223,15 +334,26 @@ func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Addre // ccqForwardToWatcher submits a query request to the appropriate watcher. It updates the request object if the write succeeds. // If the write fails, it does not update the last update time, which will cause a retry next interval (until it times out) -func ccqForwardToWatcher(qLogger *zap.Logger, pq *pendingQuery) { +func (pcq *perChainQuery) ccqForwardToWatcher(qLogger *zap.Logger, receiveTime time.Time) { select { // TODO: only send the query request itself and reassemble in this module - case pq.channel <- pq.req: - qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pq.req.RequestID), zap.Stringer("chainID", pq.req.ChainID)) - pq.lastUpdateTime = pq.receiveTime + case pcq.channel <- pcq.req: + qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chainID", pcq.req.ChainID)) + pcq.lastUpdateTime = receiveTime default: - // By leaving lastUpdateTime unset and setting inProgress to false, we will retry next interval. - qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pq.req.RequestID), zap.Stringer("chain_id", pq.req.ChainID)) - pq.inProgress = false + // By leaving lastUpdateTime unset, we will retry next interval. + qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chain_id", pcq.req.ChainID)) + } +} + +// numPendingRequests returns the number of per chain queries in a request that are still awaiting responses. Zero means the request can now be published. +func (pq *pendingQuery) numPendingRequests() int { + numPending := 0 + for _, resp := range pq.responses { + if resp == nil { + numPending += 1 + } } + + return numPending } diff --git a/node/cmd/guardiand/query_test.go b/node/cmd/guardiand/query_test.go new file mode 100644 index 0000000000..bd4d0cb8d3 --- /dev/null +++ b/node/cmd/guardiand/query_test.go @@ -0,0 +1,725 @@ +package guardiand + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math" + "math/big" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/certusone/wormhole/node/pkg/common" + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +const ( + testSigner = "beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe" + + // Magic retry values used to cause special behavior in the watchers. + fatalError = math.MaxInt + ignoreQuery = math.MaxInt - 1 + + // Speed things up for testing purposes. + requestTimeoutForTest = 100 * time.Millisecond + retryIntervalForTest = 10 * time.Millisecond + pollIntervalForTest = 5 * time.Millisecond +) + +var ( + nonce = uint32(0) + + watcherChainsForTest = []vaa.ChainID{vaa.ChainIDPolygon, vaa.ChainIDBSC} +) + +// createPerChainQueryForTesting creates a per chain query for use in tests. The To and Data fields are meaningless gibberish, not ABI. +func createPerChainQueryForTesting( + chainId vaa.ChainID, + block string, + numCalls int, +) *gossipv1.PerChainQueryRequest { + callData := []*gossipv1.EthCallQueryRequest_EthCallData{} + for count := 0; count < numCalls; count++ { + callData = append(callData, &gossipv1.EthCallQueryRequest_EthCallData{ + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d:%d", chainId, count))), + Data: []byte(fmt.Sprintf("CallData for %d:%d", chainId, count)), + }) + } + + callRequest := &gossipv1.EthCallQueryRequest{ + Block: block, + CallData: callData, + } + + return &gossipv1.PerChainQueryRequest{ + ChainId: uint32(chainId), + Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest, + }, + } +} + +// createSignedQueryRequestForTesting creates a query request object and signs it using the specified key. +func createSignedQueryRequestForTesting( + sk *ecdsa.PrivateKey, + perChainQueries []*gossipv1.PerChainQueryRequest, +) (*gossipv1.SignedQueryRequest, *gossipv1.QueryRequest) { + nonce += 1 + queryRequest := &gossipv1.QueryRequest{ + Nonce: nonce, + PerChainQueries: perChainQueries, + } + + queryRequestBytes, err := proto.Marshal(queryRequest) + if err != nil { + panic(err) + } + + digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) + sig, err := ethCrypto.Sign(digest.Bytes(), sk) + if err != nil { + panic(err) + } + + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig, + } + + return signedQueryRequest, queryRequest +} + +// createExpectedResultsForTest generates an array of the results expected for a request. These results are returned by the watcher, and used to validate the response. +func createExpectedResultsForTest(perChainQueries []*gossipv1.PerChainQueryRequest) []common.PerChainQueryResponse { + expectedResults := []common.PerChainQueryResponse{} + for _, pcq := range perChainQueries { + switch req := pcq.Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + now := time.Now() + blockNum, err := strconv.ParseInt(strings.TrimPrefix(req.EthCallQueryRequest.Block, "0x"), 16, 64) + if err != nil { + panic("invalid blockNum!") + } + resp := []common.EthCallQueryResponse{} + for _, cd := range req.EthCallQueryRequest.CallData { + resp = append(resp, common.EthCallQueryResponse{ + Number: big.NewInt(blockNum), + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), + Time: timeForTest(timeForTest(now)), + Result: []byte(hex.EncodeToString(cd.To) + ":" + hex.EncodeToString(cd.Data)), + }) + } + expectedResults = append(expectedResults, common.PerChainQueryResponse{ + ChainID: pcq.ChainId, + Responses: resp, + }) + + default: + panic("Invalid call data type!") + } + } + + return expectedResults +} + +// validateResponseForTest performs validation on the responses generated by these tests. Note that it is not a generalized validate function. +func validateResponseForTest( + t *testing.T, + response *common.QueryResponsePublication, + signedRequest *gossipv1.SignedQueryRequest, + queryRequest *gossipv1.QueryRequest, + expectedResults []common.PerChainQueryResponse, +) bool { + require.NotNil(t, response) + require.True(t, common.SignedQueryRequestEqual(signedRequest, response.Request)) + require.Equal(t, len(queryRequest.PerChainQueries), len(response.PerChainResponses)) + require.True(t, bytes.Equal(response.Request.Signature, signedRequest.Signature)) + require.Equal(t, len(response.PerChainResponses), len(expectedResults)) + for idx := range response.PerChainResponses { + require.True(t, response.PerChainResponses[idx].Equal(&expectedResults[idx])) + } + + return true +} + +// A timestamp has nanos, but we only marshal down to micros, so trim our time to micros for testing purposes. +func timeForTest(t time.Time) time.Time { + return time.UnixMicro(t.UnixMicro()) +} + +func TestCcqParseAllowedRequestersSuccess(t *testing.T) { + ccqAllowedRequestersList, err := ccqParseAllowedRequesters(testSigner) + require.NoError(t, err) + require.NotNil(t, ccqAllowedRequestersList) + require.Equal(t, 1, len(ccqAllowedRequestersList)) + + _, exists := ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes(testSigner))] + require.True(t, exists) + _, exists = ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf"))] + require.False(t, exists) + + ccqAllowedRequestersList, err = ccqParseAllowedRequesters("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf") + require.NoError(t, err) + require.NotNil(t, ccqAllowedRequestersList) + require.Equal(t, 2, len(ccqAllowedRequestersList)) + + _, exists = ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes(testSigner))] + require.True(t, exists) + _, exists = ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf"))] + require.True(t, exists) +} + +func TestCcqParseAllowedRequestersFailsIfParameterEmpty(t *testing.T) { + ccqAllowedRequestersList, err := ccqParseAllowedRequesters("") + require.Error(t, err) + require.Nil(t, ccqAllowedRequestersList) + + ccqAllowedRequestersList, err = ccqParseAllowedRequesters(",") + require.Error(t, err) + require.Nil(t, ccqAllowedRequestersList) +} + +func TestCcqParseAllowedRequestersFailsIfInvalidParameter(t *testing.T) { + ccqAllowedRequestersList, err := ccqParseAllowedRequesters("Hello") + require.Error(t, err) + require.Nil(t, ccqAllowedRequestersList) +} + +// mockData is the data structure used to mock up the query handler environment. +type mockData struct { + sk *ecdsa.PrivateKey + + signedQueryReqReadC <-chan *gossipv1.SignedQueryRequest + signedQueryReqWriteC chan<- *gossipv1.SignedQueryRequest + + chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal + + queryResponseReadC <-chan *common.PerChainQueryResponseInternal + queryResponseWriteC chan<- *common.PerChainQueryResponseInternal + + queryResponsePublicationReadC <-chan *common.QueryResponsePublication + queryResponsePublicationWriteC chan<- *common.QueryResponsePublication + + mutex sync.Mutex + queryResponsePublication *common.QueryResponsePublication + expectedResults []common.PerChainQueryResponse + requestsPerChain map[vaa.ChainID]int + retriesPerChain map[vaa.ChainID]int +} + +// resetState() is used to reset mock data between queries in the same test. +func (md *mockData) resetState() { + md.mutex.Lock() + defer md.mutex.Unlock() + md.queryResponsePublication = nil + md.expectedResults = nil + md.requestsPerChain = make(map[vaa.ChainID]int) + md.retriesPerChain = make(map[vaa.ChainID]int) +} + +// setExpectedResults sets the results to be returned by the watchers. +func (md *mockData) setExpectedResults(expectedResults []common.PerChainQueryResponse) { + md.mutex.Lock() + defer md.mutex.Unlock() + md.expectedResults = expectedResults +} + +// setRetries allows a test to specify how many times a given watcher should retry before returning success. +// If the count is the special value `fatalError`, the watcher will return common.QueryFatalError. +func (md *mockData) setRetries(chainId vaa.ChainID, count int) { + md.mutex.Lock() + defer md.mutex.Unlock() + md.retriesPerChain[chainId] = count +} + +// incrementRequestsPerChainAlreadyLocked is used by the watchers to keep track of how many times they were invoked in a given test. +func (md *mockData) incrementRequestsPerChainAlreadyLocked(chainId vaa.ChainID) { + if val, exists := md.requestsPerChain[chainId]; exists { + md.requestsPerChain[chainId] = val + 1 + } else { + md.requestsPerChain[chainId] = 1 + } +} + +// getQueryResponsePublication returns the latest query response publication received by the mock. +func (md *mockData) getQueryResponsePublication() *common.QueryResponsePublication { + md.mutex.Lock() + defer md.mutex.Unlock() + return md.queryResponsePublication +} + +// getRequestsPerChain returns the count of the number of times the given watcher was invoked in a given test. +func (md *mockData) getRequestsPerChain(chainId vaa.ChainID) int { + md.mutex.Lock() + defer md.mutex.Unlock() + if ret, exists := md.requestsPerChain[chainId]; exists { + return ret + } + return 0 +} + +// shouldIgnoreAlreadyLocked is used by the watchers to see if they should ignore a query (causing a retry). +func (md *mockData) shouldIgnoreAlreadyLocked(chainId vaa.ChainID) bool { + if val, exists := md.retriesPerChain[chainId]; exists { + if val == ignoreQuery { + delete(md.retriesPerChain, chainId) + return true + } + } + return false +} + +// getStatusAlreadyLocked is used by the watchers to determine what query status they should return, based on the `retriesPerChain`. +func (md *mockData) getStatusAlreadyLocked(chainId vaa.ChainID) common.QueryStatus { + if val, exists := md.retriesPerChain[chainId]; exists { + if val == fatalError { + return common.QueryFatalError + } + val -= 1 + if val > 0 { + md.retriesPerChain[chainId] = val + } else { + delete(md.retriesPerChain, chainId) + } + return common.QueryRetryNeeded + } + return common.QuerySuccess +} + +// createQueryHandlerForTest creates the query handler mock environment, including the set of watchers and the response listener. +// Most tests will use this function to set up the mock. +func createQueryHandlerForTest(t *testing.T, ctx context.Context, logger *zap.Logger, chains []vaa.ChainID) *mockData { + md := createQueryHandlerForTestWithoutPublisher(t, ctx, logger, chains) + md.startResponseListener(ctx) + return md +} + +// createQueryHandlerForTestWithoutPublisher creates the query handler mock environment, including the set of watchers but not the response listener. +// This function can be invoked directly to test retries of response publication (by delaying the start of the response listener). +func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context, logger *zap.Logger, chains []vaa.ChainID) *mockData { + md := mockData{} + var err error + + *unsafeDevMode = true + md.sk, err = loadGuardianKey("../../hack/query/dev.guardian.key") + require.NoError(t, err) + require.NotNil(t, md.sk) + + ccqAllowedRequestersList, err := ccqParseAllowedRequesters(testSigner) + require.NoError(t, err) + + // Inbound observation requests from the p2p service (for all chains) + md.signedQueryReqReadC, md.signedQueryReqWriteC = makeChannelPair[*gossipv1.SignedQueryRequest](common.SignedQueryRequestChannelSize) + + // Per-chain query requests + md.chainQueryReqC = make(map[vaa.ChainID]chan *common.PerChainQueryInternal) + for _, chainId := range chains { + md.chainQueryReqC[chainId] = make(chan *common.PerChainQueryInternal) + } + + // Query responses from watchers to query handler aggregated across all chains + md.queryResponseReadC, md.queryResponseWriteC = makeChannelPair[*common.PerChainQueryResponseInternal](0) + + // Query responses from query handler to p2p + md.queryResponsePublicationReadC, md.queryResponsePublicationWriteC = makeChannelPair[*common.QueryResponsePublication](0) + + md.resetState() + + go handleQueryRequestsImpl(ctx, logger, md.signedQueryReqReadC, md.chainQueryReqC, ccqAllowedRequestersList, + md.queryResponseReadC, md.queryResponsePublicationWriteC, common.GoTest, requestTimeoutForTest, retryIntervalForTest) + + // Create a routine for each configured watcher. It will take a per chain query and return the corresponding expected result. + // It also pegs a counter of the number of requests the watcher received, for verification purposes. + for chainId := range md.chainQueryReqC { + go func(chainId vaa.ChainID, chainQueryReqC <-chan *common.PerChainQueryInternal) { + for { + select { + case <-ctx.Done(): + return + case pcqr := <-chainQueryReqC: + require.Equal(t, chainId, pcqr.ChainID) + md.mutex.Lock() + md.incrementRequestsPerChainAlreadyLocked(chainId) + if md.shouldIgnoreAlreadyLocked(chainId) { + logger.Info("watcher ignoring query", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx)) + } else { + results := md.expectedResults[pcqr.RequestIdx].Responses + result := md.getStatusAlreadyLocked(chainId) + logger.Info("watcher returning", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx), zap.Int("result", int(result))) + queryResponse := common.CreatePerChainQueryResponseInternal(pcqr.RequestID, pcqr.RequestIdx, pcqr.ChainID, result, results) + md.queryResponseWriteC <- queryResponse + } + md.mutex.Unlock() + } + } + }(chainId, md.chainQueryReqC[chainId]) + } + + return &md +} + +// startResponseListener starts the response listener routine. It is called as part of the standard mock environment set up. Or, it can be used +// along with `createQueryHandlerForTestWithoutPublisher“ to test retries of response publication (by delaying the start of the response listener). +func (md *mockData) startResponseListener(ctx context.Context) { + go func() { + for { + select { + case <-ctx.Done(): + return + case qrp := <-md.queryResponsePublicationReadC: + md.mutex.Lock() + md.queryResponsePublication = qrp + md.mutex.Unlock() + } + } + }() +} + +// waitForResponse is used by the tests to wait for a response publication. It will eventually timeout if the query fails. +func (md *mockData) waitForResponse() *common.QueryResponsePublication { + for count := 0; count < 50; count++ { + time.Sleep(pollIntervalForTest) + ret := md.getQueryResponsePublication() + if ret != nil { + return ret + } + } + return nil +} + +// TestInvalidQueries tests all the obvious reasons why a query may fail (aside from watcher failures). +func TestInvalidQueries(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + var perChainQueries []*gossipv1.PerChainQueryRequest + var signedQueryRequest *gossipv1.SignedQueryRequest + + // Query with a bad signature should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + signedQueryRequest.Signature[0] += 1 // Corrupt the signature. + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Query for an unsupported chain should fail. The supported chains are defined in supportedChains in query.go + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDAlgorand, "0x28d9630", 2)} + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Query with no per-chain queries should fail. + md.resetState() + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, []*gossipv1.PerChainQueryRequest{}) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Query for an invalid chain should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries[0].ChainId = uint32(math.MaxUint16) + 1 // Corrupt the chain ID. + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Query for a chain that supports queries but that is not in the watcher channel map should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDSepolia, "0x28d9630", 2)} + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Query for "latest" should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + switch req := perChainQueries[0].Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + req.EthCallQueryRequest.Block = "latest" + } + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // A per-chain query with no call data should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 0)} + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Wrong length "To" contract should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + switch req := perChainQueries[0].Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + req.EthCallQueryRequest.CallData[0].To = req.EthCallQueryRequest.CallData[0].To[2:] + } + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) + + // Invalid type of per-chain query should fail. + md.resetState() + perChainQueries = []*gossipv1.PerChainQueryRequest{{ChainId: uint32(vaa.ChainIDPolygon)}} + signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) + md.signedQueryReqWriteC <- signedQueryRequest + require.Nil(t, md.waitForResponse()) +} + +func TestSingleQueryShouldSucceed(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // Wait until we receive a response or timeout. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} + +func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{ + createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), + createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), + } + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // Wait until we receive a response or timeout. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDBSC)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} + +func TestQueryWithLimitedRetriesShouldSucceed(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Make it retry a couple of times, but not enough to make it fail. + retries := 2 + md.setRetries(vaa.ChainIDPolygon, retries) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // The request should eventually succeed. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, retries+1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} + +func TestQueryWithRetryDueToTimeoutShouldSucceed(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Make the first per chain query timeout, but the retry should succeed. + md.setRetries(vaa.ChainIDPolygon, ignoreQuery) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // The request should eventually succeed. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, 2, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} + +func TestQueryWithTooManyRetriesShouldFail(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{ + createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), + createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), + } + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Make polygon retry a couple of times, but not enough to make it fail. + retriesForPolygon := 2 + md.setRetries(vaa.ChainIDPolygon, retriesForPolygon) + + // Make BSC retry so many times that the request times out. + md.setRetries(vaa.ChainIDBSC, 1000) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // The request should timeout. + queryResponsePublication := md.waitForResponse() + require.Nil(t, queryResponsePublication) + + assert.Equal(t, retriesForPolygon+1, md.getRequestsPerChain(vaa.ChainIDPolygon)) +} + +func TestQueryWithLimitedRetriesOnMultipleChainsShouldSucceed(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{ + createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), + createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), + } + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Make both chains retry a couple of times, but not enough to make it fail. + retriesForPolygon := 2 + md.setRetries(vaa.ChainIDPolygon, retriesForPolygon) + + retriesForBSC := 3 + md.setRetries(vaa.ChainIDBSC, retriesForBSC) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // The request should eventually succeed. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, retriesForPolygon+1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.Equal(t, retriesForBSC+1, md.getRequestsPerChain(vaa.ChainIDBSC)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} + +func TestFatalErrorOnPerChainQueryShouldCauseRequestToFail(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{ + createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), + createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), + } + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Make BSC return a fatal error. + md.setRetries(vaa.ChainIDBSC, fatalError) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // The request should timeout. + queryResponsePublication := md.waitForResponse() + require.Nil(t, queryResponsePublication) + + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDBSC)) +} + +func TestPublishRetrySucceeds(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + md := createQueryHandlerForTestWithoutPublisher(t, ctx, logger, watcherChainsForTest) + + // Create the request and the expected results. Give the expected results to the mock. + perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) + expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) + md.setExpectedResults(expectedResults) + + // Submit the query request to the handler. + md.signedQueryReqWriteC <- signedQueryRequest + + // Sleep for a bit before we start listening for published results. + // If you look in the log, you should see one of these: "failed to publish query response to p2p, will retry publishing next interval" + // and at least one of these: "resend of query response to p2p failed again, will keep retrying". + time.Sleep(retryIntervalForTest * 3) + + // Now start the publisher routine. + // If you look in the log, you should see one of these: "resend of query response to p2p succeeded". + md.startResponseListener(ctx) + + // The response should still get published. + queryResponsePublication := md.waitForResponse() + require.NotNil(t, queryResponsePublication) + + assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon)) + assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults)) +} diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index a2bd529fa7..d01bf24874 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -10,10 +10,12 @@ import ( "encoding/hex" "fmt" "io" + "math/big" "os" "strings" "time" + "github.com/certusone/wormhole/node/hack/query/utils" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/p2p" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" @@ -32,6 +34,7 @@ import ( libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" "github.com/multiformats/go-multiaddr" + "github.com/tendermint/tendermint/libs/rand" "go.uber.org/zap" "golang.org/x/crypto/openpgp/armor" //nolint "google.golang.org/protobuf/proto" @@ -177,32 +180,129 @@ func main() { panic(err) } - // methodName := "totalSupply" - methodName := "name" - data, err := wethAbi.Pack(methodName) + methods := []string{"name", "totalSupply"} + callData := []*gossipv1.EthCallQueryRequest_EthCallData{} + to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") + + for _, method := range methods { + data, err := wethAbi.Pack(method) + if err != nil { + panic(err) + } + + callData = append(callData, &gossipv1.EthCallQueryRequest_EthCallData{ + To: to, + Data: data, + }) + } + + // Fetch the latest block number + url := "https://rpc.ankr.com/polygon" + logger.Info("Querying for latest block height", zap.String("url", url)) + blockNum, err := utils.FetchLatestBlockNumberFromUrl(ctx, url) if err != nil { - panic(err) + logger.Fatal("Failed to fetch latest block number", zap.Error(err)) } - to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") + logger.Info("latest block", zap.String("num", blockNum.String()), zap.String("encoded", hexutil.EncodeBig(blockNum))) + // block := "0x28d9630" - block := "latest" + // block := "latest" // block := "0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2" + + // Start of query creation... callRequest := &gossipv1.EthCallQueryRequest{ - To: to, - Data: data, - Block: block, + Block: hexutil.EncodeBig(blockNum), + CallData: callData, + } + + // Send 2 individual requests for the same thing but 5 blocks apart + // First request... + logger.Info("calling sendQueryAndGetRsp for ", zap.String("blockNum", blockNum.String())) + queryRequest := createQueryRequest(callRequest) + sendQueryAndGetRsp(queryRequest, sk, th, ctx, logger, sub, wethAbi, methods) + + // This is just so that when I look at the output, it is easier for me. (Paul) + logger.Info("sleeping for 5 seconds") + time.Sleep(time.Second * 5) + + // Second request... + blockNum = blockNum.Sub(blockNum, big.NewInt(5)) + callRequest2 := &gossipv1.EthCallQueryRequest{ + Block: hexutil.EncodeBig(blockNum), + CallData: callData, + } + queryRequest2 := createQueryRequest(callRequest2) + logger.Info("calling sendQueryAndGetRsp for ", zap.String("blockNum", blockNum.String())) + sendQueryAndGetRsp(queryRequest2, sk, th, ctx, logger, sub, wethAbi, methods) + + // Now, want to send a single query with multiple requests... + logger.Info("Starting multiquery test in 5...") + time.Sleep(time.Second * 5) + multiCallRequest := []*gossipv1.EthCallQueryRequest{callRequest, callRequest2} + multQueryRequest := createQueryRequestWithMultipleRequests(multiCallRequest) + sendQueryAndGetRsp(multQueryRequest, sk, th, ctx, logger, sub, wethAbi, methods) + + // Cleanly shutdown + // Without this the same host won't properly discover peers until some timeout + sub.Cancel() + if err := th.Close(); err != nil { + logger.Fatal("Error closing the topic", zap.Error(err)) } + if err := h.Close(); err != nil { + logger.Fatal("Error closing the host", zap.Error(err)) + } + + // + // END SHUTDOWN + // + + logger.Info("Success! All tests passed!") +} + +const ( + GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" +) + +func createQueryRequest(callRequest *gossipv1.EthCallQueryRequest) *gossipv1.QueryRequest { queryRequest := &gossipv1.QueryRequest{ - ChainId: 5, - Nonce: 0, - Message: &gossipv1.QueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest}} + Nonce: rand.Uint32(), + PerChainQueries: []*gossipv1.PerChainQueryRequest{ + { + ChainId: 5, + Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest, + }, + }, + }, + } + return queryRequest +} + +func createQueryRequestWithMultipleRequests(callRequests []*gossipv1.EthCallQueryRequest) *gossipv1.QueryRequest { + perChainQueries := []*gossipv1.PerChainQueryRequest{} + for _, req := range callRequests { + perChainQueries = append(perChainQueries, &gossipv1.PerChainQueryRequest{ + ChainId: 5, + Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: req, + }, + }) + } + + queryRequest := &gossipv1.QueryRequest{ + Nonce: rand.Uint32(), + PerChainQueries: perChainQueries, + } + return queryRequest +} +func sendQueryAndGetRsp(queryRequest *gossipv1.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription, wethAbi abi.ABI, methods []string) { queryRequestBytes, err := proto.Marshal(queryRequest) if err != nil { panic(err) } + numQueries := len(queryRequest.PerChainQueries) // Sign the query request using our private key. digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) @@ -261,14 +361,32 @@ func main() { // 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)) + if len(response.PerChainResponses) != numQueries { + logger.Warn("unexpected number of per chain query responses", zap.Int("expectedNum", numQueries), zap.Int("actualNum", len(response.PerChainResponses))) 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)) + // Do double loop over responses + for index, pcq := range response.PerChainResponses { + logger.Info("per chain query response index", zap.Int("index", index)) + + localCallData := queryRequest.PerChainQueries[index].GetEthCallQueryRequest().GetCallData() + + if len(pcq.Responses) != len(localCallData) { + logger.Warn("unexpected number of results", zap.Int("expectedNum", len(localCallData)), zap.Int("expectedNum", len(pcq.Responses))) + break + } + + for idx, resp := range pcq.Responses { + result, err := wethAbi.Methods[methods[idx]].Outputs.Unpack(resp.Result) + if err != nil { + logger.Warn("failed to unpack result", zap.Error(err)) + break + } + + resultStr := hexutil.Encode(resp.Result) + logger.Info("found matching response", zap.Int("idx", idx), zap.String("number", resp.Number.String()), zap.String("hash", resp.Hash.String()), zap.String("time", resp.Time.String()), zap.String("method", methods[idx]), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) + } + } } default: continue @@ -277,32 +395,8 @@ func main() { break } } - - // - // BEGIN SHUTDOWN - // - - // Cleanly shutdown - // Without this the same host won't properly discover peers until some timeout - sub.Cancel() - if err := th.Close(); err != nil { - logger.Fatal("Error closing the topic", zap.Error(err)) - } - if err := h.Close(); err != nil { - logger.Fatal("Error closing the host", zap.Error(err)) - } - - // - // END SHUTDOWN - // - - logger.Info("Success! All tests passed!") } -const ( - GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" -) - // loadGuardianKey loads a serialized guardian key from disk. func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { f, err := os.Open(filename) diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index d0c04b1401..a8a0c7559f 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -184,16 +184,30 @@ func TestCrossChainQuery(t *testing.T) { panic(err) } to, _ := hex.DecodeString("DDb64fE46a91D46ee29420539FC25FD07c5FEa3E") // WETH + + callData := []*gossipv1.EthCallQueryRequest_EthCallData{ + { + To: to, + Data: data, + }, + } + callRequest := &gossipv1.EthCallQueryRequest{ - To: to, - Data: data, - Block: hexutil.EncodeBig(blockNum), + Block: hexutil.EncodeBig(blockNum), + CallData: callData, } + queryRequest := &gossipv1.QueryRequest{ - ChainId: 2, - Nonce: 0, - Message: &gossipv1.QueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest}} + Nonce: 1, + PerChainQueries: []*gossipv1.PerChainQueryRequest{ + { + ChainId: 2, + Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest, + }, + }, + }, + } queryRequestBytes, err := proto.Marshal(queryRequest) if err != nil { @@ -278,13 +292,28 @@ func TestCrossChainQuery(t *testing.T) { continue } - result, err := wethAbi.Methods[methodName].Outputs.Unpack(response.Response.Result) - if err != nil { - logger.Fatal("failed to unpack result", zap.Error(err)) + if len(response.PerChainResponses) != 1 { + logger.Warn("unexpected number of per chain query responses", zap.Int("expectedNum", 1), zap.Int("actualNum", len(response.PerChainResponses))) + break + } + + pcq := response.PerChainResponses[0] + + if len(pcq.Responses) == 0 { + logger.Warn("response did not contain any results", 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)) + for idx, resp := range pcq.Responses { + result, err := wethAbi.Methods[methodName].Outputs.Unpack(resp.Result) + if err != nil { + logger.Warn("failed to unpack result", zap.Error(err)) + break + } + + resultStr := hexutil.Encode(resp.Result) + logger.Info("found matching response", zap.Int("idx", idx), zap.String("number", resp.Number.String()), zap.String("hash", resp.Hash.String()), zap.String("time", resp.Time.String()), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) + } success = true } diff --git a/node/hack/query/utils/fetchCurrentGuardianSet.go b/node/hack/query/utils/fetchCurrentGuardianSet.go index 9e6e2152c0..67d40ee0f8 100644 --- a/node/hack/query/utils/fetchCurrentGuardianSet.go +++ b/node/hack/query/utils/fetchCurrentGuardianSet.go @@ -35,6 +35,10 @@ func FetchLatestBlockNumber(ctx context.Context, network common.Environment) (*b if rawUrl == "" { return nil, fmt.Errorf("unable to get rpc url") } + return FetchLatestBlockNumberFromUrl(ctx, rawUrl) +} + +func FetchLatestBlockNumberFromUrl(ctx context.Context, rawUrl string) (*big.Int, error) { rawClient, err := ethRpc.DialContext(ctx, rawUrl) if err != nil { return nil, fmt.Errorf("unable to dial eth context: %w", err) diff --git a/node/pkg/common/queryRequest.go b/node/pkg/common/queryRequest.go index f96be4cbb7..d3e0045458 100644 --- a/node/pkg/common/queryRequest.go +++ b/node/pkg/common/queryRequest.go @@ -1,7 +1,8 @@ package common import ( - "encoding/hex" + "bytes" + "encoding/binary" "fmt" "math" "strings" @@ -14,23 +15,14 @@ import ( ) const SignedQueryRequestChannelSize = 50 +const EvmContractAddressLength = 20 -// QueryRequest is an internal representation of a query request. -type QueryRequest struct { - SignedRequest *gossipv1.SignedQueryRequest - Request *gossipv1.QueryRequest - RequestID string - ChainID vaa.ChainID -} - -// CreateQueryRequest creates a QueryRequest object from the signed query request. -func CreateQueryRequest(signedRequest *gossipv1.SignedQueryRequest, request *gossipv1.QueryRequest) *QueryRequest { - return &QueryRequest{ - SignedRequest: signedRequest, - Request: request, - RequestID: hex.EncodeToString(signedRequest.Signature), - ChainID: vaa.ChainID(request.ChainId), - } +// PerChainQueryInternal is an internal representation of a query request that is passed to the watcher. +type PerChainQueryInternal struct { + RequestID string + RequestIdx int + ChainID vaa.ChainID + Request *gossipv1.PerChainQueryRequest } // QueryRequestDigest returns the query signing prefix based on the environment. @@ -58,28 +50,233 @@ func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequ } } +// MarshalQueryRequest serializes the binary representation of a query request +func MarshalQueryRequest(queryRequest *gossipv1.QueryRequest) ([]byte, error) { + buf := new(bytes.Buffer) + + vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 + + vaa.MustWrite(buf, binary.BigEndian, uint8(len(queryRequest.PerChainQueries))) + for _, perChainQuery := range queryRequest.PerChainQueries { + pcqBuf, err := MarshalPerChainQueryRequest(perChainQuery) + if err != nil { + return nil, fmt.Errorf("failed to marshal per chain query") + } + buf.Write(pcqBuf) + } + + return buf.Bytes(), nil +} + +// MarshalQueryRequest serializes the binary representation of a per chain query request +func MarshalPerChainQueryRequest(perChainQuery *gossipv1.PerChainQueryRequest) ([]byte, error) { + buf := new(bytes.Buffer) + switch req := perChainQuery.Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + vaa.MustWrite(buf, binary.BigEndian, QUERY_REQUEST_TYPE_ETH_CALL) + vaa.MustWrite(buf, binary.BigEndian, uint16(perChainQuery.ChainId)) + vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Block))) + buf.Write([]byte(req.EthCallQueryRequest.Block)) + vaa.MustWrite(buf, binary.BigEndian, uint8(len(req.EthCallQueryRequest.CallData))) + for _, callData := range req.EthCallQueryRequest.CallData { + buf.Write(callData.To) + vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) + buf.Write(callData.Data) + } + default: + return nil, fmt.Errorf("invalid request type") + } + return buf.Bytes(), nil +} + +// UnmarshalQueryRequest deserializes the binary representation of a query request from a byte array +func UnmarshalQueryRequest(data []byte) (*gossipv1.QueryRequest, error) { + reader := bytes.NewReader(data[:]) + return UnmarshalQueryRequestFromReader(reader) +} + +// UnmarshalQueryRequestFromReader deserializes the binary representation of a query request from an existing reader +func UnmarshalQueryRequestFromReader(reader *bytes.Reader) (*gossipv1.QueryRequest, error) { + queryRequest := &gossipv1.QueryRequest{} + + 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 + + numPerChainQueries := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numPerChainQueries); err != nil { + return nil, fmt.Errorf("failed to read number of per chain queries: %w", err) + } + + for count := 0; count < int(numPerChainQueries); count++ { + perChainQuery, err := UnmarshalPerChainQueryRequestFromReader(reader) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal per chain query: %w", err) + } + queryRequest.PerChainQueries = append(queryRequest.PerChainQueries, perChainQuery) + } + + return queryRequest, nil +} + +// UnmarshalPerChainQueryRequest deserializes the binary representation of a per chain query request from a byte array +func UnmarshalPerChainQueryRequest(data []byte) (*gossipv1.PerChainQueryRequest, error) { + reader := bytes.NewReader(data[:]) + return UnmarshalPerChainQueryRequestFromReader(reader) +} + +// UnmarshalPerChainQueryRequestFromReader deserializes the binary representation of a per chain query request from an existing reader +func UnmarshalPerChainQueryRequestFromReader(reader *bytes.Reader) (*gossipv1.PerChainQueryRequest, error) { + perChainQuery := &gossipv1.PerChainQueryRequest{} + + 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 != QUERY_REQUEST_TYPE_ETH_CALL { + // TODO: support reading different types of request/response pairs + return nil, fmt.Errorf("unsupported request type: %d", requestType) + } + + 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) + } + perChainQuery.ChainId = uint32(queryChain) + + ethCallQueryRequest := &gossipv1.EthCallQueryRequest{} + + 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[:]) + + numCallData := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil { + return nil, fmt.Errorf("failed to read number of call data entries: %w", err) + } + + for count := 0; count < int(numCallData); count++ { + queryEthCallTo := [EvmContractAddressLength]byte{} + if n, err := reader.Read(queryEthCallTo[:]); err != nil || n != EvmContractAddressLength { + return nil, fmt.Errorf("failed to read call To [%d]: %w", n, err) + } + + 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) + } + + callData := &gossipv1.EthCallQueryRequest_EthCallData{ + To: queryEthCallTo[:], + Data: queryEthCallData[:], + } + + ethCallQueryRequest.CallData = append(ethCallQueryRequest.CallData, callData) + } + + perChainQuery.Message = &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: ethCallQueryRequest, + } + + return perChainQuery, nil +} + // ValidateQueryRequest does basic validation on a received query request. func ValidateQueryRequest(queryRequest *gossipv1.QueryRequest) error { - if queryRequest.ChainId > math.MaxUint16 { - return fmt.Errorf("invalid chain id: %d is out of bounds", queryRequest.ChainId) + if len(queryRequest.PerChainQueries) == 0 { + return fmt.Errorf("request does not contain any queries") } - switch req := queryRequest.Message.(type) { - case *gossipv1.QueryRequest_EthCallQueryRequest: - if len(req.EthCallQueryRequest.To) != 20 { - return fmt.Errorf("invalid length for To contract") + for _, perChainQuery := range queryRequest.PerChainQueries { + if perChainQuery.ChainId > math.MaxUint16 { + return fmt.Errorf("invalid chain id: %d is out of bounds", perChainQuery.ChainId) } - if len(req.EthCallQueryRequest.Data) > math.MaxUint32 { - return fmt.Errorf("request data too long") + switch req := perChainQuery.Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { + return fmt.Errorf("request block too long") + } + if !strings.HasPrefix(req.EthCallQueryRequest.Block, "0x") { + return fmt.Errorf("request block must be a hex number or hash starting with 0x") + } + if len(req.EthCallQueryRequest.CallData) == 0 { + return fmt.Errorf("per chain query does not contain any requests") + } + for _, callData := range req.EthCallQueryRequest.CallData { + if len(callData.To) != EvmContractAddressLength { + return fmt.Errorf("invalid length for To contract") + } + if len(callData.Data) > math.MaxUint32 { + return fmt.Errorf("request data too long") + } + } + default: + return fmt.Errorf("received invalid message from query module") } - if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { - return fmt.Errorf("request block too long") + } + + return nil +} + +func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { + if !bytes.Equal(left.QueryRequest, right.QueryRequest) { + return false + } + if !bytes.Equal(left.Signature, right.Signature) { + return false + } + return true +} + +func QueryRequestEqual(left *gossipv1.QueryRequest, right *gossipv1.QueryRequest) bool { + if left.Nonce != right.Nonce { + return false + } + if len(left.PerChainQueries) != len(right.PerChainQueries) { + return false + } + + for idx := range left.PerChainQueries { + if left.PerChainQueries[idx].ChainId != right.PerChainQueries[idx].ChainId { + return false } - if !strings.HasPrefix(req.EthCallQueryRequest.Block, "0x") { - return fmt.Errorf("request block must be a hex number or hash starting with 0x") + + switch reqLeft := left.PerChainQueries[idx].Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + switch reqRight := right.PerChainQueries[idx].Message.(type) { + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + if reqLeft.EthCallQueryRequest.Block != reqRight.EthCallQueryRequest.Block { + return false + } + if len(reqLeft.EthCallQueryRequest.CallData) != len(reqRight.EthCallQueryRequest.CallData) { + return false + } + for idx := range reqLeft.EthCallQueryRequest.CallData { + if !bytes.Equal(reqLeft.EthCallQueryRequest.CallData[idx].To, reqRight.EthCallQueryRequest.CallData[idx].To) { + return false + } + if !bytes.Equal(reqLeft.EthCallQueryRequest.CallData[idx].Data, reqRight.EthCallQueryRequest.CallData[idx].Data) { + return false + } + } + default: + return false + } + default: + return false } - default: - return fmt.Errorf("received invalid message from query module") } - return nil + return true } diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index ec148ca2e2..c8b2c3ac22 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -30,37 +30,49 @@ const ( QueryFatalError QueryStatus = -1 ) -type QueryResponse struct { - RequestID string - ChainID vaa.ChainID - Status QueryStatus - SignedRequest *gossipv1.SignedQueryRequest - Result *EthCallQueryResponse +// This is the query response returned from the watcher to the query handler. +type PerChainQueryResponseInternal struct { + RequestID string + RequestIdx int + ChainID vaa.ChainID + Status QueryStatus + Results []EthCallQueryResponse } -func CreateQueryResponse(req *QueryRequest, status QueryStatus, result *EthCallQueryResponse) *QueryResponse { - return &QueryResponse{ - RequestID: req.RequestID, - ChainID: vaa.ChainID(req.Request.ChainId), - SignedRequest: req.SignedRequest, - Status: status, - Result: result, +// CreatePerChainQueryResponseInternal creates a PerChainQueryResponseInternal and returns a pointer to it. +func CreatePerChainQueryResponseInternal(reqId string, reqIdx int, chainID vaa.ChainID, status QueryStatus, results []EthCallQueryResponse) *PerChainQueryResponseInternal { + return &PerChainQueryResponseInternal{ + RequestID: reqId, + RequestIdx: reqIdx, + ChainID: chainID, + Status: status, + Results: results, } } var queryResponsePrefix = []byte("query_response_0000000000000000000|") +type QueryResponsePublication struct { + Request *gossipv1.SignedQueryRequest + PerChainResponses []PerChainQueryResponse +} + +type PerChainQueryResponse struct { + ChainID uint32 + Responses []EthCallQueryResponse +} + type EthCallQueryResponse struct { Number *big.Int Hash common.Hash Time time.Time Result []byte + // NOTE: If you modify this struct, please update the Equal() method for QueryResponsePublication. } -type QueryResponsePublication struct { - Request *gossipv1.SignedQueryRequest - Response EthCallQueryResponse -} +const ( + QUERY_REQUEST_TYPE_ETH_CALL = uint8(1) +) func (resp *QueryResponsePublication) RequestID() string { if resp == nil || resp.Request == nil { @@ -69,8 +81,8 @@ func (resp *QueryResponsePublication) RequestID() string { return hex.EncodeToString(resp.Request.Signature) } -// Marshal serializes the binary representation of a query response -func (msg *QueryResponsePublication) Marshal() ([]byte, error) { +// MarshalQueryResponsePublication serializes the binary representation of a query response +func MarshalQueryResponsePublication(msg *QueryResponsePublication) ([]byte, error) { // TODO: copy request write checks to query module request handling // TODO: only receive the unmarshalled query request (see note in query.go) var queryRequest gossipv1.QueryRequest @@ -79,15 +91,15 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { return nil, fmt.Errorf("received invalid message from query module") } + // Validate things before we start marshalling. if err := ValidateQueryRequest(&queryRequest); err != nil { return nil, fmt.Errorf("queryRequest is invalid: %w", err) } - if len(msg.Response.Hash) != 32 { - return nil, fmt.Errorf("invalid length for block hash") - } - if len(msg.Response.Result) > math.MaxUint32 { - return nil, fmt.Errorf("response data too long") + for idx := range msg.PerChainResponses { + if err := ValidatePerChainResponse(&msg.PerChainResponses[idx]); err != nil { + return nil, fmt.Errorf("invalid per chain response: %w", err) + } } buf := new(bytes.Buffer) @@ -99,31 +111,56 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { buf.Write(msg.Request.Signature[:]) // Request - // TODO: support writing different types of request/response pairs - switch req := queryRequest.Message.(type) { - case *gossipv1.QueryRequest_EthCallQueryRequest: - vaa.MustWrite(buf, binary.BigEndian, uint8(1)) - vaa.MustWrite(buf, binary.BigEndian, uint16(queryRequest.ChainId)) - vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 - buf.Write(req.EthCallQueryRequest.To) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Data))) - buf.Write(req.EthCallQueryRequest.Data) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Block))) - // TODO: should this be an enum or the literal string? - buf.Write([]byte(req.EthCallQueryRequest.Block)) - - // Response - // TODO: probably some kind of request/response pair validation - // TODO: is uint64 safe? - vaa.MustWrite(buf, binary.BigEndian, msg.Response.Number.Uint64()) - buf.Write(msg.Response.Hash[:]) - vaa.MustWrite(buf, binary.BigEndian, uint32(msg.Response.Time.Unix())) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(msg.Response.Result))) - buf.Write(msg.Response.Result) - return buf.Bytes(), nil - default: - return nil, fmt.Errorf("received invalid message from query module") + qrBuf, err := MarshalQueryRequest(&queryRequest) + if err != nil { + return nil, fmt.Errorf("failed to marshal query request") + } + buf.Write(qrBuf) + + // Per chain responses + vaa.MustWrite(buf, binary.BigEndian, uint8(len(msg.PerChainResponses))) + for idx := range msg.PerChainResponses { + pcrBuf, err := MarshalPerChainResponse(&msg.PerChainResponses[idx]) + if err != nil { + return nil, fmt.Errorf("failed to marshal per chain response: %w", err) + } + buf.Write(pcrBuf) + } + + return buf.Bytes(), nil +} + +// MarshalPerChainResponse marshalls a per chain query response. +func MarshalPerChainResponse(pcr *PerChainQueryResponse) ([]byte, error) { + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, pcr.ChainID) + vaa.MustWrite(buf, binary.BigEndian, uint8(len(pcr.Responses))) + for _, resp := range pcr.Responses { + vaa.MustWrite(buf, binary.BigEndian, resp.Number.Uint64()) + buf.Write(resp.Hash[:]) + vaa.MustWrite(buf, binary.BigEndian, resp.Time.UnixMicro()) + vaa.MustWrite(buf, binary.BigEndian, uint32(len(resp.Result))) + buf.Write(resp.Result) } + return buf.Bytes(), nil +} + +// ValidatePerChainResponse performs basic validation on a per chain query response. +func ValidatePerChainResponse(pcr *PerChainQueryResponse) error { + if pcr.ChainID > math.MaxUint16 { + return fmt.Errorf("invalid chain ID") + } + + for _, resp := range pcr.Responses { + if len(resp.Hash) != 32 { + return fmt.Errorf("invalid length for block hash") + } + if len(resp.Result) > math.MaxUint32 { + return fmt.Errorf("response data too long") + } + } + + return nil } // Unmarshal deserializes the binary representation of a query response @@ -153,59 +190,11 @@ func UnmarshalQueryResponsePublication(data []byte) (*QueryResponsePublication, } 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) + queryRequest, err := UnmarshalQueryRequestFromReader(reader) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal query request: %w", err) } - ethCallQueryRequest.Block = string(queryEthCallBlockBytes[:]) - queryRequest.Message = &gossipv1.QueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: ethCallQueryRequest, - } queryRequestBytes, err := proto.Marshal(queryRequest) if err != nil { return nil, err @@ -214,41 +203,73 @@ func UnmarshalQueryResponsePublication(data []byte) (*QueryResponsePublication, 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) + // Responses + numPerChainResponses := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numPerChainResponses); err != nil { + return nil, fmt.Errorf("failed to read number of per chain responses: %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) + for count := 0; count < int(numPerChainResponses); count++ { + pcr, err := UnmarshalQueryPerChainResponseFromReader(reader) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal per chain response: %w", err) + } + msg.PerChainResponses = append(msg.PerChainResponses, *pcr) } - 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) + return msg, nil +} + +func UnmarshalQueryPerChainResponseFromReader(reader *bytes.Reader) (*PerChainQueryResponse, error) { + pcr := PerChainQueryResponse{} - responseResultLen := uint32(0) - if err := binary.Read(reader, binary.BigEndian, &responseResultLen); err != nil { - return nil, fmt.Errorf("failed to read response len: %w", err) + chainID := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &chainID); err != nil { + return nil, fmt.Errorf("failed to read chain ID: %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) + pcr.ChainID = chainID + + numResponses := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numResponses); err != nil { + return nil, fmt.Errorf("failed to read number of responses: %w", err) } - queryResponse.Result = responseResult[:] - msg.Response = queryResponse + for count := 0; count < int(numResponses); count++ { + 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 + + unixMicros := int64(0) + if err := binary.Read(reader, binary.BigEndian, &unixMicros); err != nil { + return nil, fmt.Errorf("failed to read response timestamp: %w", err) + } + queryResponse.Time = time.UnixMicro(unixMicros) + + 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[:] + + pcr.Responses = append(pcr.Responses, queryResponse) + } - return msg, nil + return &pcr, nil } // Similar to sdk/vaa/structs.go, @@ -256,13 +277,55 @@ func UnmarshalQueryResponsePublication(data []byte) (*QueryResponsePublication, // the first hash (32 bytes) vs the full body data. // TODO: confirm if this works / is worthwhile. func (msg *QueryResponsePublication) SigningDigest() (common.Hash, error) { - msgBytes, err := msg.Marshal() + msgBytes, err := MarshalQueryResponsePublication(msg) if err != nil { return common.Hash{}, err } return GetQueryResponseDigestFromBytes(msgBytes), nil } +// GetQueryResponseDigestFromBytes computes the digest bytes for a query response byte array. func GetQueryResponseDigestFromBytes(b []byte) common.Hash { return crypto.Keccak256Hash(append(queryResponsePrefix, crypto.Keccak256Hash(b).Bytes()...)) } + +// Equal checks for equality on two query response publications. +func (left *QueryResponsePublication) Equal(right *QueryResponsePublication) bool { + if !bytes.Equal(left.Request.QueryRequest, right.Request.QueryRequest) || !bytes.Equal(left.Request.Signature, right.Request.Signature) { + return false + } + if len(left.PerChainResponses) != len(right.PerChainResponses) { + return false + } + for idx := range left.PerChainResponses { + if !left.PerChainResponses[idx].Equal(&right.PerChainResponses[idx]) { + return false + } + } + return true +} + +// Equal checks for equality on two per chain query responses. +func (left *PerChainQueryResponse) Equal(right *PerChainQueryResponse) bool { + if left.ChainID != right.ChainID { + return false + } + if len(left.Responses) != len(right.Responses) { + return false + } + for idx := range left.Responses { + if left.Responses[idx].Number.Cmp(right.Responses[idx].Number) != 0 { + return false + } + if !bytes.Equal(left.Responses[idx].Hash.Bytes(), right.Responses[idx].Hash.Bytes()) { + return false + } + if left.Responses[idx].Time != right.Responses[idx].Time { + return false + } + if !bytes.Equal(left.Responses[idx].Result, right.Responses[idx].Result) { + return false + } + } + return true +} diff --git a/node/pkg/common/query_test.go b/node/pkg/common/query_test.go new file mode 100644 index 0000000000..9004ff9ee4 --- /dev/null +++ b/node/pkg/common/query_test.go @@ -0,0 +1,181 @@ +package common + +import ( + "encoding/hex" + "math/big" + "strings" + "testing" + "time" + + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi" + ethCommon "github.com/ethereum/go-ethereum/common" + + "google.golang.org/protobuf/proto" +) + +func createQueryRequestForTesting() *gossipv1.QueryRequest { + // Create a query request. + 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) + } + + data1, err := wethAbi.Pack("name") + if err != nil { + panic(err) + } + data2, err := wethAbi.Pack("totalSupply") + if err != nil { + panic(err) + } + + to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") + block := "0x28d9630" + callData := []*gossipv1.EthCallQueryRequest_EthCallData{ + { + To: to, + Data: data1, + }, + { + To: to, + Data: data2, + }, + } + callRequest := &gossipv1.EthCallQueryRequest{ + Block: block, + CallData: callData, + } + + perChainQuery := &gossipv1.PerChainQueryRequest{ + ChainId: 5, + Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ + EthCallQueryRequest: callRequest, + }, + } + + queryRequest := &gossipv1.QueryRequest{ + Nonce: 1, + PerChainQueries: []*gossipv1.PerChainQueryRequest{perChainQuery}, + } + + return queryRequest +} + +// A timestamp has nanos, but we only marshal down to micros, so trim our time to micros for testing purposes. +func timeForTest(t time.Time) time.Time { + return time.UnixMicro(t.UnixMicro()) +} + +func TestQueryRequestProtoMarshalUnMarshal(t *testing.T) { + queryRequest := createQueryRequestForTesting() + queryRequestBytes, err := proto.Marshal(queryRequest) + require.NoError(t, err) + + var queryRequest2 gossipv1.QueryRequest + err = proto.Unmarshal(queryRequestBytes, &queryRequest2) + require.NoError(t, err) + + assert.True(t, QueryRequestEqual(queryRequest, &queryRequest2)) +} + +func TestQueryRequestMarshalUnMarshal(t *testing.T) { + queryRequest := createQueryRequestForTesting() + queryRequestBytes, err := MarshalQueryRequest(queryRequest) + require.NoError(t, err) + + queryRequest2, err := UnmarshalQueryRequest(queryRequestBytes) + require.NoError(t, err) + + assert.True(t, QueryRequestEqual(queryRequest, queryRequest2)) +} + +func TestQueryResponseMarshalUnMarshal(t *testing.T) { + queryRequest := createQueryRequestForTesting() + queryRequestBytes, err := proto.Marshal(queryRequest) + require.NoError(t, err) + + sig := [65]byte{} + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig[:], + } + + results, err := hex.DecodeString("010203040506070809") + require.NoError(t, err) + + respPub := &QueryResponsePublication{ + Request: signedQueryRequest, + PerChainResponses: []PerChainQueryResponse{ + { + ChainID: 5, + Responses: []EthCallQueryResponse{ + { + Number: big.NewInt(42), + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), + Time: timeForTest(time.Now()), + Result: results, + }, + { + Number: big.NewInt(43), + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef9deadbeef"), + Time: timeForTest(time.Now()), + Result: results, + }, + }, + }, + { + ChainID: 11, + Responses: []EthCallQueryResponse{ + { + Number: big.NewInt(44), + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e3"), + Time: timeForTest(time.Now()), + Result: results, + }, + }, + }, + }, + } + + respPubBytes, err := MarshalQueryResponsePublication(respPub) + require.NoError(t, err) + + respPub2, err := UnmarshalQueryResponsePublication(respPubBytes) + require.NoError(t, err) + require.NotNil(t, respPub2) + + assert.True(t, respPub.Equal(respPub2)) +} + +/* +func TesMarshalUnMarshalQueryResponseWithNoResults(t *testing.T) { + queryRequest := createQueryRequestForTesting() + queryRequestBytes, err := proto.Marshal(queryRequest) + require.NoError(t, err) + + sig := [65]byte{} + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig[:], + } + + respPub := &QueryResponsePublication{ + Request: signedQueryRequest, + Responses: nil, + } + + respPubBytes, err := MarshalQueryResponsePublication(respPub) + require.NoError(t, err) + + respPub2, err := UnmarshalQueryResponsePublication(respPubBytes) + require.NoError(t, err) + require.NotNil(t, respPub2) + + assert.True(t, respPub.Equal(respPub2)) +} +*/ diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 6d18899457..55d0412943 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -507,7 +507,7 @@ func Run( logger.Error("received a cross chain query response when the feature is disabled, dropping it", zap.String("component", "ccqp2p")) continue } - msgBytes, err := msg.Marshal() + msgBytes, err := node_common.MarshalQueryResponsePublication(msg) if err != nil { logger.Error("failed to marshal query response", zap.Error(err), zap.String("component", "ccqp2p")) continue diff --git a/node/pkg/proto/gossip/v1/gossip.pb.go b/node/pkg/proto/gossip/v1/gossip.pb.go index 134c798fcc..04691f6769 100644 --- a/node/pkg/proto/gossip/v1/gossip.pb.go +++ b/node/pkg/proto/gossip/v1/gossip.pb.go @@ -1217,12 +1217,8 @@ type QueryRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ChainId uint32 `protobuf:"varint,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` - Nonce uint32 `protobuf:"varint,2,opt,name=nonce,proto3" json:"nonce,omitempty"` - // Types that are assignable to Message: - // - // *QueryRequest_EthCallQueryRequest - Message isQueryRequest_Message `protobuf_oneof:"message"` + Nonce uint32 `protobuf:"varint,1,opt,name=nonce,proto3" json:"nonce,omitempty"` + PerChainQueries []*PerChainQueryRequest `protobuf:"bytes,2,rep,name=per_chain_queries,json=perChainQueries,proto3" json:"per_chain_queries,omitempty"` } func (x *QueryRequest) Reset() { @@ -1257,58 +1253,108 @@ func (*QueryRequest) Descriptor() ([]byte, []int) { return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{14} } -func (x *QueryRequest) GetChainId() uint32 { +func (x *QueryRequest) GetNonce() uint32 { if x != nil { - return x.ChainId + return x.Nonce } return 0 } -func (x *QueryRequest) GetNonce() uint32 { +func (x *QueryRequest) GetPerChainQueries() []*PerChainQueryRequest { if x != nil { - return x.Nonce + return x.PerChainQueries + } + return nil +} + +type PerChainQueryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChainId uint32 `protobuf:"varint,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + // Types that are assignable to Message: + // + // *PerChainQueryRequest_EthCallQueryRequest + Message isPerChainQueryRequest_Message `protobuf_oneof:"message"` +} + +func (x *PerChainQueryRequest) Reset() { + *x = PerChainQueryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PerChainQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PerChainQueryRequest) ProtoMessage() {} + +func (x *PerChainQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PerChainQueryRequest.ProtoReflect.Descriptor instead. +func (*PerChainQueryRequest) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{15} +} + +func (x *PerChainQueryRequest) GetChainId() uint32 { + if x != nil { + return x.ChainId } return 0 } -func (m *QueryRequest) GetMessage() isQueryRequest_Message { +func (m *PerChainQueryRequest) GetMessage() isPerChainQueryRequest_Message { if m != nil { return m.Message } return nil } -func (x *QueryRequest) GetEthCallQueryRequest() *EthCallQueryRequest { - if x, ok := x.GetMessage().(*QueryRequest_EthCallQueryRequest); ok { +func (x *PerChainQueryRequest) GetEthCallQueryRequest() *EthCallQueryRequest { + if x, ok := x.GetMessage().(*PerChainQueryRequest_EthCallQueryRequest); ok { return x.EthCallQueryRequest } return nil } -type isQueryRequest_Message interface { - isQueryRequest_Message() +type isPerChainQueryRequest_Message interface { + isPerChainQueryRequest_Message() } -type QueryRequest_EthCallQueryRequest struct { +type PerChainQueryRequest_EthCallQueryRequest struct { EthCallQueryRequest *EthCallQueryRequest `protobuf:"bytes,3,opt,name=eth_call_query_request,json=ethCallQueryRequest,proto3,oneof"` } -func (*QueryRequest_EthCallQueryRequest) isQueryRequest_Message() {} +func (*PerChainQueryRequest_EthCallQueryRequest) isPerChainQueryRequest_Message() {} type EthCallQueryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - To []byte `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - Block string `protobuf:"bytes,3,opt,name=block,proto3" json:"block,omitempty"` + Block string `protobuf:"bytes,1,opt,name=block,proto3" json:"block,omitempty"` + CallData []*EthCallQueryRequest_EthCallData `protobuf:"bytes,2,rep,name=call_data,json=callData,proto3" json:"call_data,omitempty"` } func (x *EthCallQueryRequest) Reset() { *x = EthCallQueryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1321,7 +1367,7 @@ func (x *EthCallQueryRequest) String() string { func (*EthCallQueryRequest) ProtoMessage() {} func (x *EthCallQueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1334,30 +1380,23 @@ func (x *EthCallQueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EthCallQueryRequest.ProtoReflect.Descriptor instead. func (*EthCallQueryRequest) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{15} + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16} } -func (x *EthCallQueryRequest) GetTo() []byte { +func (x *EthCallQueryRequest) GetBlock() string { if x != nil { - return x.To + return x.Block } - return nil + return "" } -func (x *EthCallQueryRequest) GetData() []byte { +func (x *EthCallQueryRequest) GetCallData() []*EthCallQueryRequest_EthCallData { if x != nil { - return x.Data + return x.CallData } return nil } -func (x *EthCallQueryRequest) GetBlock() string { - if x != nil { - return x.Block - } - return "" -} - type SignedQueryResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1372,7 +1411,7 @@ type SignedQueryResponse struct { func (x *SignedQueryResponse) Reset() { *x = SignedQueryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1385,7 +1424,7 @@ func (x *SignedQueryResponse) String() string { func (*SignedQueryResponse) ProtoMessage() {} func (x *SignedQueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1398,7 +1437,7 @@ func (x *SignedQueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedQueryResponse.ProtoReflect.Descriptor instead. func (*SignedQueryResponse) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16} + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{17} } func (x *SignedQueryResponse) GetQueryResponse() []byte { @@ -1433,7 +1472,7 @@ type Heartbeat_Network struct { func (x *Heartbeat_Network) Reset() { *x = Heartbeat_Network{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1446,7 +1485,7 @@ func (x *Heartbeat_Network) String() string { func (*Heartbeat_Network) ProtoMessage() {} func (x *Heartbeat_Network) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1503,7 +1542,7 @@ type ChainGovernorConfig_Chain struct { func (x *ChainGovernorConfig_Chain) Reset() { *x = ChainGovernorConfig_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1516,7 +1555,7 @@ func (x *ChainGovernorConfig_Chain) String() string { func (*ChainGovernorConfig_Chain) ProtoMessage() {} func (x *ChainGovernorConfig_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1566,7 +1605,7 @@ type ChainGovernorConfig_Token struct { func (x *ChainGovernorConfig_Token) Reset() { *x = ChainGovernorConfig_Token{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1579,7 +1618,7 @@ func (x *ChainGovernorConfig_Token) String() string { func (*ChainGovernorConfig_Token) ProtoMessage() {} func (x *ChainGovernorConfig_Token) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1630,7 +1669,7 @@ type ChainGovernorStatus_EnqueuedVAA struct { func (x *ChainGovernorStatus_EnqueuedVAA) Reset() { *x = ChainGovernorStatus_EnqueuedVAA{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1643,7 +1682,7 @@ func (x *ChainGovernorStatus_EnqueuedVAA) String() string { func (*ChainGovernorStatus_EnqueuedVAA) ProtoMessage() {} func (x *ChainGovernorStatus_EnqueuedVAA) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1700,7 +1739,7 @@ type ChainGovernorStatus_Emitter struct { func (x *ChainGovernorStatus_Emitter) Reset() { *x = ChainGovernorStatus_Emitter{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1713,7 +1752,7 @@ func (x *ChainGovernorStatus_Emitter) String() string { func (*ChainGovernorStatus_Emitter) ProtoMessage() {} func (x *ChainGovernorStatus_Emitter) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1763,7 +1802,7 @@ type ChainGovernorStatus_Chain struct { func (x *ChainGovernorStatus_Chain) Reset() { *x = ChainGovernorStatus_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[22] + mi := &file_gossip_v1_gossip_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1776,7 +1815,7 @@ func (x *ChainGovernorStatus_Chain) String() string { func (*ChainGovernorStatus_Chain) ProtoMessage() {} func (x *ChainGovernorStatus_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[22] + mi := &file_gossip_v1_gossip_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1813,6 +1852,61 @@ func (x *ChainGovernorStatus_Chain) GetEmitters() []*ChainGovernorStatus_Emitter return nil } +type EthCallQueryRequest_EthCallData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + To []byte `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *EthCallQueryRequest_EthCallData) Reset() { + *x = EthCallQueryRequest_EthCallData{} + if protoimpl.UnsafeEnabled { + mi := &file_gossip_v1_gossip_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EthCallQueryRequest_EthCallData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EthCallQueryRequest_EthCallData) ProtoMessage() {} + +func (x *EthCallQueryRequest_EthCallData) ProtoReflect() protoreflect.Message { + mi := &file_gossip_v1_gossip_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EthCallQueryRequest_EthCallData.ProtoReflect.Descriptor instead. +func (*EthCallQueryRequest_EthCallData) Descriptor() ([]byte, []int) { + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16, 0} +} + +func (x *EthCallQueryRequest_EthCallData) GetTo() []byte { + if x != nil { + return x.To + } + return nil +} + +func (x *EthCallQueryRequest_EthCallData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + var File_gossip_v1_gossip_proto protoreflect.FileDescriptor var file_gossip_v1_gossip_proto_rawDesc = []byte{ @@ -2051,32 +2145,44 @@ var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x65, 0x74, 0x68, 0x5f, 0x63, 0x61, - 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, - 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, 0x74, 0x68, 0x43, 0x61, 0x6c, - 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, - 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x74, 0x6f, 0x12, - 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x5a, 0x0a, 0x13, 0x53, 0x69, 0x67, - 0x6e, 0x65, 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x25, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, - 0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, - 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x75, 0x72, 0x65, 0x22, 0x71, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x70, 0x65, + 0x72, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x50, 0x65, 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x51, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x14, 0x50, 0x65, 0x72, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x55, 0x0a, 0x16, 0x65, + 0x74, 0x68, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, + 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, + 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xa7, 0x01, + 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x47, 0x0a, 0x09, 0x63, + 0x61, 0x6c, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, + 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, + 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x63, 0x61, 0x6c, 0x6c, + 0x44, 0x61, 0x74, 0x61, 0x1a, 0x31, 0x0a, 0x0b, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x02, 0x74, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5a, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, + 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, + 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2091,7 +2197,7 @@ func file_gossip_v1_gossip_proto_rawDescGZIP() []byte { return file_gossip_v1_gossip_proto_rawDescData } -var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*GossipMessage)(nil), // 0: gossip.v1.GossipMessage (*SignedHeartbeat)(nil), // 1: gossip.v1.SignedHeartbeat @@ -2108,14 +2214,16 @@ var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*ChainGovernorStatus)(nil), // 12: gossip.v1.ChainGovernorStatus (*SignedQueryRequest)(nil), // 13: gossip.v1.SignedQueryRequest (*QueryRequest)(nil), // 14: gossip.v1.QueryRequest - (*EthCallQueryRequest)(nil), // 15: gossip.v1.EthCallQueryRequest - (*SignedQueryResponse)(nil), // 16: gossip.v1.SignedQueryResponse - (*Heartbeat_Network)(nil), // 17: gossip.v1.Heartbeat.Network - (*ChainGovernorConfig_Chain)(nil), // 18: gossip.v1.ChainGovernorConfig.Chain - (*ChainGovernorConfig_Token)(nil), // 19: gossip.v1.ChainGovernorConfig.Token - (*ChainGovernorStatus_EnqueuedVAA)(nil), // 20: gossip.v1.ChainGovernorStatus.EnqueuedVAA - (*ChainGovernorStatus_Emitter)(nil), // 21: gossip.v1.ChainGovernorStatus.Emitter - (*ChainGovernorStatus_Chain)(nil), // 22: gossip.v1.ChainGovernorStatus.Chain + (*PerChainQueryRequest)(nil), // 15: gossip.v1.PerChainQueryRequest + (*EthCallQueryRequest)(nil), // 16: gossip.v1.EthCallQueryRequest + (*SignedQueryResponse)(nil), // 17: gossip.v1.SignedQueryResponse + (*Heartbeat_Network)(nil), // 18: gossip.v1.Heartbeat.Network + (*ChainGovernorConfig_Chain)(nil), // 19: gossip.v1.ChainGovernorConfig.Chain + (*ChainGovernorConfig_Token)(nil), // 20: gossip.v1.ChainGovernorConfig.Token + (*ChainGovernorStatus_EnqueuedVAA)(nil), // 21: gossip.v1.ChainGovernorStatus.EnqueuedVAA + (*ChainGovernorStatus_Emitter)(nil), // 22: gossip.v1.ChainGovernorStatus.Emitter + (*ChainGovernorStatus_Chain)(nil), // 23: gossip.v1.ChainGovernorStatus.Chain + (*EthCallQueryRequest_EthCallData)(nil), // 24: gossip.v1.EthCallQueryRequest.EthCallData } var file_gossip_v1_gossip_proto_depIdxs = []int32{ 3, // 0: gossip.v1.GossipMessage.signed_observation:type_name -> gossip.v1.SignedObservation @@ -2127,19 +2235,21 @@ var file_gossip_v1_gossip_proto_depIdxs = []int32{ 9, // 6: gossip.v1.GossipMessage.signed_chain_governor_config:type_name -> gossip.v1.SignedChainGovernorConfig 11, // 7: gossip.v1.GossipMessage.signed_chain_governor_status:type_name -> gossip.v1.SignedChainGovernorStatus 13, // 8: gossip.v1.GossipMessage.signed_query_request:type_name -> gossip.v1.SignedQueryRequest - 16, // 9: gossip.v1.GossipMessage.signed_query_response:type_name -> gossip.v1.SignedQueryResponse - 17, // 10: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network - 18, // 11: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain - 19, // 12: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token - 22, // 13: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain - 15, // 14: gossip.v1.QueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest - 20, // 15: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA - 21, // 16: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter - 17, // [17:17] is the sub-list for method output_type - 17, // [17:17] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 17, // 9: gossip.v1.GossipMessage.signed_query_response:type_name -> gossip.v1.SignedQueryResponse + 18, // 10: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network + 19, // 11: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain + 20, // 12: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token + 23, // 13: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain + 15, // 14: gossip.v1.QueryRequest.per_chain_queries:type_name -> gossip.v1.PerChainQueryRequest + 16, // 15: gossip.v1.PerChainQueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest + 24, // 16: gossip.v1.EthCallQueryRequest.call_data:type_name -> gossip.v1.EthCallQueryRequest.EthCallData + 21, // 17: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA + 22, // 18: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter + 19, // [19:19] is the sub-list for method output_type + 19, // [19:19] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_gossip_v1_gossip_proto_init() } @@ -2329,7 +2439,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EthCallQueryRequest); i { + switch v := v.(*PerChainQueryRequest); i { case 0: return &v.state case 1: @@ -2341,7 +2451,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SignedQueryResponse); i { + switch v := v.(*EthCallQueryRequest); i { case 0: return &v.state case 1: @@ -2353,7 +2463,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Heartbeat_Network); i { + switch v := v.(*SignedQueryResponse); i { case 0: return &v.state case 1: @@ -2365,7 +2475,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Chain); i { + switch v := v.(*Heartbeat_Network); i { case 0: return &v.state case 1: @@ -2377,7 +2487,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorConfig_Token); i { + switch v := v.(*ChainGovernorConfig_Chain); i { case 0: return &v.state case 1: @@ -2389,7 +2499,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { + switch v := v.(*ChainGovernorConfig_Token); i { case 0: return &v.state case 1: @@ -2401,7 +2511,7 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChainGovernorStatus_Emitter); i { + switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { case 0: return &v.state case 1: @@ -2413,6 +2523,18 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChainGovernorStatus_Emitter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gossip_v1_gossip_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_Chain); i { case 0: return &v.state @@ -2424,6 +2546,18 @@ func file_gossip_v1_gossip_proto_init() { return nil } } + file_gossip_v1_gossip_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EthCallQueryRequest_EthCallData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_gossip_v1_gossip_proto_msgTypes[0].OneofWrappers = []interface{}{ (*GossipMessage_SignedObservation)(nil), @@ -2437,8 +2571,8 @@ func file_gossip_v1_gossip_proto_init() { (*GossipMessage_SignedQueryRequest)(nil), (*GossipMessage_SignedQueryResponse)(nil), } - file_gossip_v1_gossip_proto_msgTypes[14].OneofWrappers = []interface{}{ - (*QueryRequest_EthCallQueryRequest)(nil), + file_gossip_v1_gossip_proto_msgTypes[15].OneofWrappers = []interface{}{ + (*PerChainQueryRequest_EthCallQueryRequest)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2446,7 +2580,7 @@ func file_gossip_v1_gossip_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gossip_v1_gossip_proto_rawDesc, NumEnums: 0, - NumMessages: 23, + NumMessages: 25, NumExtensions: 0, NumServices: 0, }, diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 4e43b4c009..49bc8be892 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -96,10 +96,10 @@ type ( // Incoming query requests from the network. Pre-filtered to only // include requests for our chainID. - queryReqC <-chan *common.QueryRequest + queryReqC <-chan *common.PerChainQueryInternal // Outbound query responses to query requests - queryResponseC chan<- *common.QueryResponse + queryResponseC chan<- *common.PerChainQueryResponseInternal pending map[pendingKey]*pendingMessage pendingMu sync.Mutex @@ -150,8 +150,8 @@ func NewEthWatcher( msgC chan<- *common.MessagePublication, setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, - queryReqC <-chan *common.QueryRequest, - queryResponseC chan<- *common.QueryResponse, + queryReqC <-chan *common.PerChainQueryInternal, + queryResponseC chan<- *common.PerChainQueryResponseInternal, unsafeDevMode bool, ) *Watcher { @@ -539,24 +539,18 @@ func (w *Watcher) Run(parentCtx context.Context) error { } switch req := queryRequest.Request.Message.(type) { - case *gossipv1.QueryRequest_EthCallQueryRequest: - to := eth_common.BytesToAddress(req.EthCallQueryRequest.To) - data := eth_hexutil.Encode(req.EthCallQueryRequest.Data) + case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: block := req.EthCallQueryRequest.Block logger.Info("received query request", zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), zap.String("block", block), + zap.Int("numRequests", len(req.EthCallQueryRequest.CallData)), zap.String("component", "ccqevm"), ) timeout, cancel := context.WithTimeout(ctx, 5*time.Second) // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 - callTransactionArg := map[string]interface{}{ - "to": to, - "data": data, - } + var blockMethod string var callBlockArg interface{} // TODO: try making these error and see what happens @@ -582,38 +576,70 @@ func (w *Watcher) Run(parentCtx context.Context) error { blockMethod = "eth_getBlockByNumber" callBlockArg = block } - var blockResult connectors.BlockMarshaller - var blockError error - var callResult eth_hexutil.Bytes - var callErr error - err := w.ethConn.RawBatchCallContext(timeout, []rpc.BatchElem{ - { - Method: blockMethod, - Args: []interface{}{ - block, - false, // no full transaction details + + // EvmCallData contains the details of a single query in the batch. + type EvmCallData struct { + to eth_common.Address + data string + callTransactionArg map[string]interface{} + callResult *eth_hexutil.Bytes + callErr error + } + + // We build two slices. The first is the batch submitted to the RPC call. It contains one entry for each query plus one to query the block. + // The second is the data associated with each request (but not the block request). The index into both is the index into the request call data. + batch := []rpc.BatchElem{} + evmCallData := []EvmCallData{} + + // Add each requested query to the batch. + for _, callData := range req.EthCallQueryRequest.CallData { + // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 + to := eth_common.BytesToAddress(callData.To) + data := eth_hexutil.Encode(callData.Data) + ecd := EvmCallData{ + to: to, + data: data, + callTransactionArg: map[string]interface{}{ + "to": to, + "data": data, }, - Result: &blockResult, - Error: blockError, - }, - { + callResult: ð_hexutil.Bytes{}, + } + evmCallData = append(evmCallData, ecd) + + batch = append(batch, rpc.BatchElem{ Method: "eth_call", Args: []interface{}{ - callTransactionArg, + ecd.callTransactionArg, callBlockArg, }, - Result: &callResult, - Error: callErr, + Result: ecd.callResult, + Error: ecd.callErr, + }) + } + + // Add the block query to the batch. + var blockResult connectors.BlockMarshaller + var blockError error + batch = append(batch, rpc.BatchElem{ + Method: blockMethod, + Args: []interface{}{ + block, + false, // no full transaction details }, + Result: &blockResult, + Error: blockError, }) + + // Query the RPC. + err := w.ethConn.RawBatchCallContext(timeout, batch) cancel() if err != nil { logger.Error("failed to process query request", zap.Error(err), zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), zap.String("block", block), + zap.Any("batch", batch), zap.String("component", "ccqevm"), ) w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) @@ -623,9 +649,8 @@ func (w *Watcher) Run(parentCtx context.Context) error { if blockError != nil { logger.Error("failed to process query block request", zap.Error(blockError), zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), zap.String("block", block), + zap.Any("batch", batch), zap.String("component", "ccqevm"), ) w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) @@ -635,61 +660,70 @@ func (w *Watcher) Run(parentCtx context.Context) error { if blockResult.Number == nil { logger.Error("invalid query block result", zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), zap.String("block", block), + zap.Any("batch", batch), zap.String("component", "ccqevm"), ) w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) continue } - if callErr != nil { - logger.Error("failed to process query call request", - zap.Error(callErr), zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), - zap.String("block", block), - zap.String("component", "ccqevm"), - ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) - continue - } + resp := []common.EthCallQueryResponse{} + + errFound := false + for idx := range req.EthCallQueryRequest.CallData { + if evmCallData[idx].callErr != nil { + logger.Error("failed to process query call request", + zap.Error(evmCallData[idx].callErr), zap.String("eth_network", w.networkName), + zap.String("block", block), + zap.Int("errorIdx", idx), + zap.Any("batch", batch), + zap.String("component", "ccqevm"), + ) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + errFound = true + break + } + + // Nil or Empty results are not valid + // eth_call will return empty when the state doesn't exist for a block + if len(*evmCallData[idx].callResult) == 0 { + logger.Error("invalid call result", + zap.String("eth_network", w.networkName), + zap.String("block", block), + zap.Int("errorIdx", idx), + zap.Any("batch", batch), + zap.String("component", "ccqevm"), + ) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + errFound = true + break + } - // Nil or Empty results are not valid - // eth_call will return empty when the state doesn't exist for a block - if len(callResult) == 0 { - logger.Error("invalid call result", + logger.Info("query result", zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), zap.String("block", block), + zap.String("blockNumber", blockResult.Number.String()), + zap.String("blockHash", blockResult.Hash.Hex()), + zap.String("blockTime", blockResult.Time.String()), + zap.Int("idx", idx), + zap.String("to", evmCallData[idx].to.Hex()), + zap.Any("data", evmCallData[idx].data), + zap.String("result", evmCallData[idx].callResult.String()), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) - continue - } - logger.Info("query result", - zap.String("eth_network", w.networkName), - zap.String("to", to.Hex()), - zap.Any("data", data), - zap.String("block", block), - zap.String("blockNumber", blockResult.Number.String()), - zap.String("blockHash", blockResult.Hash.Hex()), - zap.String("blockTime", blockResult.Time.String()), - zap.String("result", callResult.String()), - zap.String("component", "ccqevm"), - ) - - resp := &common.EthCallQueryResponse{ - Number: blockResult.Number.ToInt(), - Hash: blockResult.Hash, - Time: time.Unix(int64(blockResult.Time), 0), - Result: callResult, + resp = append(resp, common.EthCallQueryResponse{ + Number: blockResult.Number.ToInt(), + Hash: blockResult.Hash, + Time: time.Unix(int64(blockResult.Time), 0), + Result: *evmCallData[idx].callResult, + }) } - w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, resp) + if !errFound { + w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, resp) + } default: logger.Warn("received unsupported request type", @@ -1119,8 +1153,8 @@ func (w *Watcher) SetMaxWaitConfirmations(maxWaitConfirmations uint64) { } // ccqSendQueryResponse sends an error response back to the query handler. -func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.QueryRequest, status common.QueryStatus, result *common.EthCallQueryResponse) { - queryResponse := common.CreateQueryResponse(req, status, result) +func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.PerChainQueryInternal, status common.QueryStatus, results []common.EthCallQueryResponse) { + queryResponse := common.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.ChainID, status, results) select { case w.queryResponseC <- queryResponse: logger.Debug("published query response error to handler", zap.String("component", "ccqevm")) diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index b68135cd8e..aae54677ce 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -243,17 +243,25 @@ message SignedQueryRequest { } message QueryRequest { + uint32 nonce = 1; + repeated PerChainQueryRequest per_chain_queries = 2; +} + +message PerChainQueryRequest { uint32 chain_id = 1; - uint32 nonce = 2; oneof message { EthCallQueryRequest eth_call_query_request = 3; } } message EthCallQueryRequest { - bytes to = 1; - bytes data = 2; - string block = 3; + string block = 1; + repeated EthCallData call_data = 2; + + message EthCallData { + bytes to = 1; + bytes data = 2; + } } message SignedQueryResponse { From 0f3634ab3e3c6d0c3fa4c457a9ed9a7a0557cce3 Mon Sep 17 00:00:00 2001 From: Paul Noel <35237584+panoel@users.noreply.github.com> Date: Thu, 15 Jun 2023 11:53:31 -0500 Subject: [PATCH 14/37] tilt: deploy core and token bridge to wormchain (#3084) * wormchain: deploy more contracts in tilt * wormchain: change contract ordering * wormchain: rearrange instantiation --- wormchain/contracts/tools/deploy_wormchain.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/wormchain/contracts/tools/deploy_wormchain.ts b/wormchain/contracts/tools/deploy_wormchain.ts index a73a2d1f37..3a93c732d8 100644 --- a/wormchain/contracts/tools/deploy_wormchain.ts +++ b/wormchain/contracts/tools/deploy_wormchain.ts @@ -43,6 +43,9 @@ type ContractName = string; const artifacts: ContractName[] = [ "global_accountant.wasm", "wormchain_ibc_receiver.wasm", + "cw_wormhole.wasm", + "cw20_wrapped_2.wasm", + "cw_token_bridge.wasm", ]; const ARTIFACTS_PATH = "../artifacts/"; @@ -333,6 +336,80 @@ async function main() { updateIbcWhitelistRes.transactionHash, updateIbcWhitelistRes.code ); + + const init_guardians = JSON.parse(process.env.INIT_SIGNERS); + if (!init_guardians || init_guardians.length === 0) { + throw "failed to get initial guardians from .env file."; + } + + addresses["cw_wormhole.wasm"] = await instantiate( + codeIds["cw_wormhole.wasm"], + { + gov_chain: GOVERNANCE_CHAIN, + gov_address: Buffer.from(GOVERNANCE_EMITTER, "hex").toString("base64"), + guardian_set_expirity: 86400, + initial_guardian_set: { + addresses: init_guardians.map((hex) => { + return { + bytes: Buffer.from(hex, "hex").toString("base64"), + }; + }), + expiration_time: 0, + }, + chain_id: 3104, + fee_denom: "uworm", + }, + "wormhole" + ); + + console.log( + "instantiated wormhole contract: ", + addresses["cw_wormhole.wasm"] + ); + + addresses["cw_token_bridge.wasm"] = await instantiate( + codeIds["cw_token_bridge.wasm"], + { + gov_chain: GOVERNANCE_CHAIN, + gov_address: Buffer.from(GOVERNANCE_EMITTER, "hex").toString("base64"), + wormhole_contract: addresses["cw_wormhole.wasm"], + wrapped_asset_code_id: codeIds["cw20_wrapped_2.wasm"], + chain_id: 3104, + native_denom: "uworm", + native_symbol: "WORM", + native_decimals: 6, + }, + "tokenBridge" + ); + + console.log( + "instantiated token bridge contract: ", + addresses["cw_token_bridge.wasm"] + ); + + for (let vaa of accountingRegistrations) { + const tbRegMsg = client.wasm.msgExecuteContract({ + sender: signer, + contract: addresses["cw_token_bridge.wasm"], + msg: toUtf8( + JSON.stringify({ + submit_vaa: { + data: vaa, + }, + }) + ), + funds: [], + }); + const tbRes = await client.signAndBroadcast(signer, [tbRegMsg], { + ...ZERO_FEE, + gas: "10000000", + }); + console.log( + `sent chain registration to token bridge, tx: `, + tbRes.transactionHash, + vaa + ); + } } try { From 9ddee1a95d120d26de333b9f3684f824cb20aca8 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 21 Jun 2023 12:07:02 -0500 Subject: [PATCH 15/37] CCQ: Messaging changes (#3093) * CCQ: Messaging changes * Add tests for marshaling query request * Response changes * Rework response structs, add tests * Code review rework * Make block number uint64 internally --- node/cmd/guardiand/query.go | 26 +- node/cmd/guardiand/query_test.go | 142 +++---- node/hack/query/send_req.go | 73 ++-- node/hack/query/test/query_test.go | 37 +- node/pkg/common/queryRequest.go | 467 +++++++++++++++-------- node/pkg/common/queryResponse.go | 469 +++++++++++++++-------- node/pkg/common/query_test.go | 517 ++++++++++++++++++++++---- node/pkg/p2p/p2p.go | 2 +- node/pkg/proto/gossip/v1/gossip.pb.go | 431 +++------------------ node/pkg/watchers/evm/watcher.go | 50 ++- proto/gossip/v1/gossip.proto | 22 -- 11 files changed, 1266 insertions(+), 970 deletions(-) diff --git a/node/cmd/guardiand/query.go b/node/cmd/guardiand/query.go index 1fd86be287..b5c61d51f1 100644 --- a/node/cmd/guardiand/query.go +++ b/node/cmd/guardiand/query.go @@ -15,7 +15,6 @@ import ( ethCrypto "github.com/ethereum/go-ethereum/crypto" "go.uber.org/zap" - "google.golang.org/protobuf/proto" ) const ( @@ -30,7 +29,7 @@ type ( // pendingQuery is the cache entry for a given query. pendingQuery struct { signedRequest *gossipv1.SignedQueryRequest - request *gossipv1.QueryRequest + request *common.QueryRequest requestID string receiveTime time.Time queries []*perChainQuery @@ -141,14 +140,14 @@ func handleQueryRequestsImpl( continue } - var queryRequest gossipv1.QueryRequest - err = proto.Unmarshal(signedRequest.QueryRequest, &queryRequest) + var queryRequest common.QueryRequest + err = queryRequest.Unmarshal(signedRequest.QueryRequest) if err != nil { qLogger.Error("failed to unmarshal query request", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID), zap.Error(err)) continue } - if err := common.ValidateQueryRequest(&queryRequest); err != nil { + if err := queryRequest.Validate(); err != nil { qLogger.Error("received invalid message", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID), zap.Error(err)) continue } @@ -178,7 +177,6 @@ func handleQueryRequestsImpl( req: &common.PerChainQueryInternal{ RequestID: requestID, RequestIdx: requestIdx, - ChainID: chainID, Request: pcq, }, channel: channel, @@ -207,7 +205,7 @@ func handleQueryRequestsImpl( case resp := <-queryResponseReadC: // Response from a watcher. if resp.Status == common.QuerySuccess { - if len(resp.Results) == 0 { + if resp.Response == nil { qLogger.Error("received a successful query response with no results, dropping it!", zap.String("requestID", resp.RequestID)) continue } @@ -236,16 +234,16 @@ func handleQueryRequestsImpl( } // Build the list of per chain response publications and the overall query response publication. - responses := []common.PerChainQueryResponse{} + responses := []*common.PerChainQueryResponse{} for _, resp := range pq.responses { if resp == nil { qLogger.Error("unexpected null response in pending query!", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) continue } - responses = append(responses, common.PerChainQueryResponse{ - ChainID: uint32(resp.ChainID), - Responses: resp.Results, + responses = append(responses, &common.PerChainQueryResponse{ + ChainId: resp.ChainId, + Response: resp.Response, }) } @@ -325,7 +323,7 @@ func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Addre result[addr] = struct{}{} } - if len(result) == 0 { + if len(result) <= 0 { return nil, fmt.Errorf("no allowed requestors specified, ccqAllowedRequesters: `%s`", ccqAllowedRequesters) } @@ -338,11 +336,11 @@ func (pcq *perChainQuery) ccqForwardToWatcher(qLogger *zap.Logger, receiveTime t select { // TODO: only send the query request itself and reassemble in this module case pcq.channel <- pcq.req: - qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chainID", pcq.req.ChainID)) + qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chainID", pcq.req.Request.ChainId)) pcq.lastUpdateTime = receiveTime default: // By leaving lastUpdateTime unset, we will retry next interval. - qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chain_id", pcq.req.ChainID)) + qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chain_id", pcq.req.Request.ChainId)) } } diff --git a/node/cmd/guardiand/query_test.go b/node/cmd/guardiand/query_test.go index bd4d0cb8d3..92a8ed14bc 100644 --- a/node/cmd/guardiand/query_test.go +++ b/node/cmd/guardiand/query_test.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "fmt" "math" - "math/big" "strconv" "strings" "sync" @@ -25,7 +24,6 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "google.golang.org/protobuf/proto" ) const ( @@ -52,40 +50,38 @@ func createPerChainQueryForTesting( chainId vaa.ChainID, block string, numCalls int, -) *gossipv1.PerChainQueryRequest { - callData := []*gossipv1.EthCallQueryRequest_EthCallData{} +) *common.PerChainQueryRequest { + callData := []*common.EthCallData{} for count := 0; count < numCalls; count++ { - callData = append(callData, &gossipv1.EthCallQueryRequest_EthCallData{ + callData = append(callData, &common.EthCallData{ To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d:%d", chainId, count))), Data: []byte(fmt.Sprintf("CallData for %d:%d", chainId, count)), }) } - callRequest := &gossipv1.EthCallQueryRequest{ - Block: block, + callRequest := &common.EthCallQueryRequest{ + BlockId: block, CallData: callData, } - return &gossipv1.PerChainQueryRequest{ - ChainId: uint32(chainId), - Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest, - }, + return &common.PerChainQueryRequest{ + ChainId: chainId, + Query: callRequest, } } // createSignedQueryRequestForTesting creates a query request object and signs it using the specified key. func createSignedQueryRequestForTesting( sk *ecdsa.PrivateKey, - perChainQueries []*gossipv1.PerChainQueryRequest, -) (*gossipv1.SignedQueryRequest, *gossipv1.QueryRequest) { + perChainQueries []*common.PerChainQueryRequest, +) (*gossipv1.SignedQueryRequest, *common.QueryRequest) { nonce += 1 - queryRequest := &gossipv1.QueryRequest{ + queryRequest := &common.QueryRequest{ Nonce: nonce, PerChainQueries: perChainQueries, } - queryRequestBytes, err := proto.Marshal(queryRequest) + queryRequestBytes, err := queryRequest.Marshal() if err != nil { panic(err) } @@ -105,28 +101,28 @@ func createSignedQueryRequestForTesting( } // createExpectedResultsForTest generates an array of the results expected for a request. These results are returned by the watcher, and used to validate the response. -func createExpectedResultsForTest(perChainQueries []*gossipv1.PerChainQueryRequest) []common.PerChainQueryResponse { +func createExpectedResultsForTest(perChainQueries []*common.PerChainQueryRequest) []common.PerChainQueryResponse { expectedResults := []common.PerChainQueryResponse{} for _, pcq := range perChainQueries { - switch req := pcq.Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: + switch req := pcq.Query.(type) { + case *common.EthCallQueryRequest: now := time.Now() - blockNum, err := strconv.ParseInt(strings.TrimPrefix(req.EthCallQueryRequest.Block, "0x"), 16, 64) + blockNum, err := strconv.ParseUint(strings.TrimPrefix(req.BlockId, "0x"), 16, 64) if err != nil { panic("invalid blockNum!") } - resp := []common.EthCallQueryResponse{} - for _, cd := range req.EthCallQueryRequest.CallData { - resp = append(resp, common.EthCallQueryResponse{ - Number: big.NewInt(blockNum), - Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), - Time: timeForTest(timeForTest(now)), - Result: []byte(hex.EncodeToString(cd.To) + ":" + hex.EncodeToString(cd.Data)), - }) + resp := &common.EthCallQueryResponse{ + BlockNumber: blockNum, + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), + Time: timeForTest(timeForTest(now)), + Results: [][]byte{}, + } + for _, cd := range req.CallData { + resp.Results = append(resp.Results, []byte(hex.EncodeToString(cd.To)+":"+hex.EncodeToString(cd.Data))) } expectedResults = append(expectedResults, common.PerChainQueryResponse{ - ChainID: pcq.ChainId, - Responses: resp, + ChainId: pcq.ChainId, + Response: resp, }) default: @@ -142,7 +138,7 @@ func validateResponseForTest( t *testing.T, response *common.QueryResponsePublication, signedRequest *gossipv1.SignedQueryRequest, - queryRequest *gossipv1.QueryRequest, + queryRequest *common.QueryRequest, expectedResults []common.PerChainQueryResponse, ) bool { require.NotNil(t, response) @@ -352,16 +348,16 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context case <-ctx.Done(): return case pcqr := <-chainQueryReqC: - require.Equal(t, chainId, pcqr.ChainID) + require.Equal(t, chainId, pcqr.Request.ChainId) md.mutex.Lock() md.incrementRequestsPerChainAlreadyLocked(chainId) if md.shouldIgnoreAlreadyLocked(chainId) { logger.Info("watcher ignoring query", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx)) } else { - results := md.expectedResults[pcqr.RequestIdx].Responses - result := md.getStatusAlreadyLocked(chainId) - logger.Info("watcher returning", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx), zap.Int("result", int(result))) - queryResponse := common.CreatePerChainQueryResponseInternal(pcqr.RequestID, pcqr.RequestIdx, pcqr.ChainID, result, results) + results := md.expectedResults[pcqr.RequestIdx].Response + status := md.getStatusAlreadyLocked(chainId) + logger.Info("watcher returning", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx), zap.Int("status", int(status))) + queryResponse := common.CreatePerChainQueryResponseInternal(pcqr.RequestID, pcqr.RequestIdx, pcqr.Request.ChainId, status, results) md.queryResponseWriteC <- queryResponse } md.mutex.Unlock() @@ -410,12 +406,12 @@ func TestInvalidQueries(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) - var perChainQueries []*gossipv1.PerChainQueryRequest + var perChainQueries []*common.PerChainQueryRequest var signedQueryRequest *gossipv1.SignedQueryRequest // Query with a bad signature should fail. md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) signedQueryRequest.Signature[0] += 1 // Corrupt the signature. md.signedQueryReqWriteC <- signedQueryRequest @@ -423,64 +419,14 @@ func TestInvalidQueries(t *testing.T) { // Query for an unsupported chain should fail. The supported chains are defined in supportedChains in query.go md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDAlgorand, "0x28d9630", 2)} - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // Query with no per-chain queries should fail. - md.resetState() - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, []*gossipv1.PerChainQueryRequest{}) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // Query for an invalid chain should fail. - md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} - perChainQueries[0].ChainId = uint32(math.MaxUint16) + 1 // Corrupt the chain ID. + perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDAlgorand, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) md.signedQueryReqWriteC <- signedQueryRequest require.Nil(t, md.waitForResponse()) // Query for a chain that supports queries but that is not in the watcher channel map should fail. md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDSepolia, "0x28d9630", 2)} - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // Query for "latest" should fail. - md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} - switch req := perChainQueries[0].Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - req.EthCallQueryRequest.Block = "latest" - } - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // A per-chain query with no call data should fail. - md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 0)} - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // Wrong length "To" contract should fail. - md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} - switch req := perChainQueries[0].Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - req.EthCallQueryRequest.CallData[0].To = req.EthCallQueryRequest.CallData[0].To[2:] - } - signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) - md.signedQueryReqWriteC <- signedQueryRequest - require.Nil(t, md.waitForResponse()) - - // Invalid type of per-chain query should fail. - md.resetState() - perChainQueries = []*gossipv1.PerChainQueryRequest{{ChainId: uint32(vaa.ChainIDPolygon)}} + perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDSepolia, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) md.signedQueryReqWriteC <- signedQueryRequest require.Nil(t, md.waitForResponse()) @@ -494,7 +440,7 @@ func TestSingleQueryShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -518,7 +464,7 @@ func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{ + perChainQueries := []*common.PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -546,7 +492,7 @@ func TestQueryWithLimitedRetriesShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -574,7 +520,7 @@ func TestQueryWithRetryDueToTimeoutShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -601,7 +547,7 @@ func TestQueryWithTooManyRetriesShouldFail(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{ + perChainQueries := []*common.PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -634,7 +580,7 @@ func TestQueryWithLimitedRetriesOnMultipleChainsShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{ + perChainQueries := []*common.PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -669,7 +615,7 @@ func TestFatalErrorOnPerChainQueryShouldCauseRequestToFail(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{ + perChainQueries := []*common.PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -699,7 +645,7 @@ func TestPublishRetrySucceeds(t *testing.T) { md := createQueryHandlerForTestWithoutPublisher(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*gossipv1.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index d01bf24874..fe4bb254b3 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -181,7 +181,7 @@ func main() { } methods := []string{"name", "totalSupply"} - callData := []*gossipv1.EthCallQueryRequest_EthCallData{} + callData := []*common.EthCallData{} to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") for _, method := range methods { @@ -190,7 +190,7 @@ func main() { panic(err) } - callData = append(callData, &gossipv1.EthCallQueryRequest_EthCallData{ + callData = append(callData, &common.EthCallData{ To: to, Data: data, }) @@ -211,8 +211,8 @@ func main() { // block := "0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2" // Start of query creation... - callRequest := &gossipv1.EthCallQueryRequest{ - Block: hexutil.EncodeBig(blockNum), + callRequest := &common.EthCallQueryRequest{ + BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } @@ -228,8 +228,8 @@ func main() { // Second request... blockNum = blockNum.Sub(blockNum, big.NewInt(5)) - callRequest2 := &gossipv1.EthCallQueryRequest{ - Block: hexutil.EncodeBig(blockNum), + callRequest2 := &common.EthCallQueryRequest{ + BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } queryRequest2 := createQueryRequest(callRequest2) @@ -239,7 +239,7 @@ func main() { // Now, want to send a single query with multiple requests... logger.Info("Starting multiquery test in 5...") time.Sleep(time.Second * 5) - multiCallRequest := []*gossipv1.EthCallQueryRequest{callRequest, callRequest2} + multiCallRequest := []*common.EthCallQueryRequest{callRequest, callRequest2} multQueryRequest := createQueryRequestWithMultipleRequests(multiCallRequest) sendQueryAndGetRsp(multQueryRequest, sk, th, ctx, logger, sub, wethAbi, methods) @@ -264,41 +264,37 @@ const ( GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" ) -func createQueryRequest(callRequest *gossipv1.EthCallQueryRequest) *gossipv1.QueryRequest { - queryRequest := &gossipv1.QueryRequest{ +func createQueryRequest(callRequest *common.EthCallQueryRequest) *common.QueryRequest { + queryRequest := &common.QueryRequest{ Nonce: rand.Uint32(), - PerChainQueries: []*gossipv1.PerChainQueryRequest{ + PerChainQueries: []*common.PerChainQueryRequest{ { ChainId: 5, - Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest, - }, + Query: callRequest, }, }, } return queryRequest } -func createQueryRequestWithMultipleRequests(callRequests []*gossipv1.EthCallQueryRequest) *gossipv1.QueryRequest { - perChainQueries := []*gossipv1.PerChainQueryRequest{} +func createQueryRequestWithMultipleRequests(callRequests []*common.EthCallQueryRequest) *common.QueryRequest { + perChainQueries := []*common.PerChainQueryRequest{} for _, req := range callRequests { - perChainQueries = append(perChainQueries, &gossipv1.PerChainQueryRequest{ + perChainQueries = append(perChainQueries, &common.PerChainQueryRequest{ ChainId: 5, - Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: req, - }, + Query: req, }) } - queryRequest := &gossipv1.QueryRequest{ + queryRequest := &common.QueryRequest{ Nonce: rand.Uint32(), PerChainQueries: perChainQueries, } return queryRequest } -func sendQueryAndGetRsp(queryRequest *gossipv1.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription, wethAbi abi.ABI, methods []string) { - queryRequestBytes, err := proto.Marshal(queryRequest) +func sendQueryAndGetRsp(queryRequest *common.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription, wethAbi abi.ABI, methods []string) { + queryRequestBytes, err := queryRequest.Marshal() if err != nil { panic(err) } @@ -352,7 +348,8 @@ func sendQueryAndGetRsp(queryRequest *gossipv1.QueryRequest, sk *ecdsa.PrivateKe switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) - response, err := common.UnmarshalQueryResponsePublication(m.SignedQueryResponse.QueryResponse) + var response common.QueryResponsePublication + err := response.Unmarshal(m.SignedQueryResponse.QueryResponse) if err != nil { logger.Warn("failed to unmarshal response", zap.Error(err)) break @@ -366,25 +363,39 @@ func sendQueryAndGetRsp(queryRequest *gossipv1.QueryRequest, sk *ecdsa.PrivateKe break } // Do double loop over responses - for index, pcq := range response.PerChainResponses { + for index := range response.PerChainResponses { logger.Info("per chain query response index", zap.Int("index", index)) - localCallData := queryRequest.PerChainQueries[index].GetEthCallQueryRequest().GetCallData() + var localCallData []*common.EthCallData + switch ecq := queryRequest.PerChainQueries[index].Query.(type) { + case *common.EthCallQueryRequest: + localCallData = ecq.CallData + default: + panic("unsupported query type") + } + + var localResp *common.EthCallQueryResponse + switch ecq := response.PerChainResponses[index].Response.(type) { + case *common.EthCallQueryResponse: + localResp = ecq + default: + panic("unsupported query type") + } - if len(pcq.Responses) != len(localCallData) { - logger.Warn("unexpected number of results", zap.Int("expectedNum", len(localCallData)), zap.Int("expectedNum", len(pcq.Responses))) + if len(localResp.Results) != len(localCallData) { + logger.Warn("unexpected number of results", zap.Int("expectedNum", len(localCallData)), zap.Int("expectedNum", len(localResp.Results))) break } - for idx, resp := range pcq.Responses { - result, err := wethAbi.Methods[methods[idx]].Outputs.Unpack(resp.Result) + for idx, resp := range localResp.Results { + result, err := wethAbi.Methods[methods[idx]].Outputs.Unpack(resp) if err != nil { logger.Warn("failed to unpack result", zap.Error(err)) break } - resultStr := hexutil.Encode(resp.Result) - logger.Info("found matching response", zap.Int("idx", idx), zap.String("number", resp.Number.String()), zap.String("hash", resp.Hash.String()), zap.String("time", resp.Time.String()), zap.String("method", methods[idx]), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) + resultStr := hexutil.Encode(resp) + logger.Info("found matching response", zap.Int("idx", idx), zap.Uint64("number", localResp.BlockNumber), zap.String("hash", localResp.Hash.String()), zap.String("time", localResp.Time.String()), zap.String("method", methods[idx]), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) } } } diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index a8a0c7559f..4ed7707e55 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -185,31 +185,29 @@ func TestCrossChainQuery(t *testing.T) { } to, _ := hex.DecodeString("DDb64fE46a91D46ee29420539FC25FD07c5FEa3E") // WETH - callData := []*gossipv1.EthCallQueryRequest_EthCallData{ + callData := []*common.EthCallData{ { To: to, Data: data, }, } - callRequest := &gossipv1.EthCallQueryRequest{ - Block: hexutil.EncodeBig(blockNum), + callRequest := &common.EthCallQueryRequest{ + BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } - queryRequest := &gossipv1.QueryRequest{ + queryRequest := &common.QueryRequest{ Nonce: 1, - PerChainQueries: []*gossipv1.PerChainQueryRequest{ + PerChainQueries: []*common.PerChainQueryRequest{ { ChainId: 2, - Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest, - }, + Query: callRequest, }, }, } - queryRequestBytes, err := proto.Marshal(queryRequest) + queryRequestBytes, err := queryRequest.Marshal() if err != nil { panic(err) } @@ -262,7 +260,8 @@ func TestCrossChainQuery(t *testing.T) { switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) - response, err := common.UnmarshalQueryResponsePublication(m.SignedQueryResponse.QueryResponse) + var response common.QueryResponsePublication + err := response.Unmarshal(m.SignedQueryResponse.QueryResponse) if err != nil { logger.Fatal("failed to unmarshal response", zap.Error(err)) } @@ -297,22 +296,28 @@ func TestCrossChainQuery(t *testing.T) { break } - pcq := response.PerChainResponses[0] + var pcq *common.EthCallQueryResponse + switch ecq := response.PerChainResponses[0].Response.(type) { + case *common.EthCallQueryResponse: + pcq = ecq + default: + panic("unsupported query type") + } - if len(pcq.Responses) == 0 { + if len(pcq.Results) == 0 { logger.Warn("response did not contain any results", zap.Error(err)) break } - for idx, resp := range pcq.Responses { - result, err := wethAbi.Methods[methodName].Outputs.Unpack(resp.Result) + for idx, resp := range pcq.Results { + result, err := wethAbi.Methods[methodName].Outputs.Unpack(resp) if err != nil { logger.Warn("failed to unpack result", zap.Error(err)) break } - resultStr := hexutil.Encode(resp.Result) - logger.Info("found matching response", zap.Int("idx", idx), zap.String("number", resp.Number.String()), zap.String("hash", resp.Hash.String()), zap.String("time", resp.Time.String()), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) + resultStr := hexutil.Encode(resp) + logger.Info("found matching response", zap.Int("idx", idx), zap.Uint64("number", pcq.BlockNumber), zap.String("hash", pcq.Hash.String()), zap.String("time", pcq.Time.String()), zap.Any("resultDecoded", result), zap.String("resultStr", resultStr)) } success = true diff --git a/node/pkg/common/queryRequest.go b/node/pkg/common/queryRequest.go index d3e0045458..9f26415d46 100644 --- a/node/pkg/common/queryRequest.go +++ b/node/pkg/common/queryRequest.go @@ -14,6 +14,55 @@ import ( ethCrypto "github.com/ethereum/go-ethereum/crypto" ) +// QueryRequest defines a cross chain query request to be submitted to the guardians. +// It is the payload of the SignedQueryRequest gossip message. +type QueryRequest struct { + Nonce uint32 + PerChainQueries []*PerChainQueryRequest +} + +// PerChainQueryRequest represents a query request for a single chain. +type PerChainQueryRequest struct { + // ChainId indicates which chain this query is destine for. + ChainId vaa.ChainID + + // Query is the chain specific query data. + Query ChainSpecificQuery +} + +// ChainSpecificQuery is the interface that must be implemented by a chain specific query. +type ChainSpecificQuery interface { + Type() ChainSpecificQueryType + Marshal() ([]byte, error) + Unmarshal(data []byte) error + UnmarshalFromReader(reader *bytes.Reader) error + Validate() error +} + +// ChainSpecificQueryType is used to interpret the data in a per chain query request. +type ChainSpecificQueryType uint8 + +// EthCallQueryRequestType is the type of an EVM eth_call query request. +const EthCallQueryRequestType ChainSpecificQueryType = 1 + +// EthCallQueryRequest implements ChainSpecificQuery for an EVM eth_call query request. +type EthCallQueryRequest struct { + // BlockId identifies the block to be queried. It mus be a hex string starting with 0x. It may be a block number or a block hash. + BlockId string + + // CallData is an array of specific queries to be performed on the specified block, in a single RPC call. + CallData []*EthCallData +} + +// EthCallData specifies the parameters to a single EVM eth_call request. +type EthCallData struct { + // To specifies the contract address to be queried. + To []byte + + // Data is the ABI encoded parameters to the query. + Data []byte +} + const SignedQueryRequestChannelSize = 50 const EvmContractAddressLength = 20 @@ -21,8 +70,7 @@ const EvmContractAddressLength = 20 type PerChainQueryInternal struct { RequestID string RequestIdx int - ChainID vaa.ChainID - Request *gossipv1.PerChainQueryRequest + Request *PerChainQueryRequest } // QueryRequestDigest returns the query signing prefix based on the environment. @@ -50,17 +98,26 @@ func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequ } } -// MarshalQueryRequest serializes the binary representation of a query request -func MarshalQueryRequest(queryRequest *gossipv1.QueryRequest) ([]byte, error) { +// +// Implementation of QueryRequest. +// + +// Marshal serializes the binary representation of a query request. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (queryRequest *QueryRequest) Marshal() ([]byte, error) { + if err := queryRequest.Validate(); err != nil { + return nil, err + } + buf := new(bytes.Buffer) vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 vaa.MustWrite(buf, binary.BigEndian, uint8(len(queryRequest.PerChainQueries))) for _, perChainQuery := range queryRequest.PerChainQueries { - pcqBuf, err := MarshalPerChainQueryRequest(perChainQuery) + pcqBuf, err := perChainQuery.Marshal() if err != nil { - return nil, fmt.Errorf("failed to marshal per chain query") + return nil, fmt.Errorf("failed to marshal per chain query: %w", err) } buf.Write(pcqBuf) } @@ -68,215 +125,323 @@ func MarshalQueryRequest(queryRequest *gossipv1.QueryRequest) ([]byte, error) { return buf.Bytes(), nil } -// MarshalQueryRequest serializes the binary representation of a per chain query request -func MarshalPerChainQueryRequest(perChainQuery *gossipv1.PerChainQueryRequest) ([]byte, error) { - buf := new(bytes.Buffer) - switch req := perChainQuery.Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - vaa.MustWrite(buf, binary.BigEndian, QUERY_REQUEST_TYPE_ETH_CALL) - vaa.MustWrite(buf, binary.BigEndian, uint16(perChainQuery.ChainId)) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(req.EthCallQueryRequest.Block))) - buf.Write([]byte(req.EthCallQueryRequest.Block)) - vaa.MustWrite(buf, binary.BigEndian, uint8(len(req.EthCallQueryRequest.CallData))) - for _, callData := range req.EthCallQueryRequest.CallData { - buf.Write(callData.To) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) - buf.Write(callData.Data) +// Unmarshal deserializes the binary representation of a query request from a byte array +func (queryRequest *QueryRequest) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return queryRequest.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes the binary representation of a query request from an existing reader +func (queryRequest *QueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { + if err := binary.Read(reader, binary.BigEndian, &queryRequest.Nonce); err != nil { + return fmt.Errorf("failed to read request nonce: %w", err) + } + + numPerChainQueries := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numPerChainQueries); err != nil { + return fmt.Errorf("failed to read number of per chain queries: %w", err) + } + + for count := 0; count < int(numPerChainQueries); count++ { + perChainQuery := PerChainQueryRequest{} + err := perChainQuery.UnmarshalFromReader(reader) + if err != nil { + return fmt.Errorf("failed to Unmarshal per chain query: %w", err) + } + queryRequest.PerChainQueries = append(queryRequest.PerChainQueries, &perChainQuery) + } + + return nil +} + +// Validate does basic validation on a received query request. +func (queryRequest *QueryRequest) Validate() error { + // Nothing to validate on the Nonce. + if len(queryRequest.PerChainQueries) <= 0 { + return fmt.Errorf("request does not contain any per chain queries") + } + if len(queryRequest.PerChainQueries) > math.MaxUint8 { + return fmt.Errorf("too many per chain queries") + } + for idx, perChainQuery := range queryRequest.PerChainQueries { + if err := perChainQuery.Validate(); err != nil { + return fmt.Errorf("failed to validate per chain query %d: %w", idx, err) + } + } + return nil +} + +// Equal verifies that two query requests are equal. +func (left *QueryRequest) Equal(right *QueryRequest) bool { + if left.Nonce != right.Nonce { + return false + } + if len(left.PerChainQueries) != len(right.PerChainQueries) { + return false + } + + for idx := range left.PerChainQueries { + if !left.PerChainQueries[idx].Equal(right.PerChainQueries[idx]) { + return false } - default: - return nil, fmt.Errorf("invalid request type") } + return true +} + +// +// Implementation of PerChainQueryRequest. +// + +// Marshal serializes the binary representation of a per chain query request. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (perChainQuery *PerChainQueryRequest) Marshal() ([]byte, error) { + if err := perChainQuery.Validate(); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, perChainQuery.ChainId) + vaa.MustWrite(buf, binary.BigEndian, perChainQuery.Query.Type()) + queryBuf, err := perChainQuery.Query.Marshal() + if err != nil { + return nil, err + } + buf.Write(queryBuf) return buf.Bytes(), nil } -// UnmarshalQueryRequest deserializes the binary representation of a query request from a byte array -func UnmarshalQueryRequest(data []byte) (*gossipv1.QueryRequest, error) { +// Unmarshal deserializes the binary representation of a per chain query request from a byte array +func (perChainQuery *PerChainQueryRequest) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) - return UnmarshalQueryRequestFromReader(reader) + return perChainQuery.UnmarshalFromReader(reader) } -// UnmarshalQueryRequestFromReader deserializes the binary representation of a query request from an existing reader -func UnmarshalQueryRequestFromReader(reader *bytes.Reader) (*gossipv1.QueryRequest, error) { - queryRequest := &gossipv1.QueryRequest{} +// UnmarshalFromReader deserializes the binary representation of a per chain query request from an existing reader +func (perChainQuery *PerChainQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { + if err := binary.Read(reader, binary.BigEndian, &perChainQuery.ChainId); err != nil { + return fmt.Errorf("failed to read request chain: %w", err) + } - queryNonce := uint32(0) - if err := binary.Read(reader, binary.BigEndian, &queryNonce); err != nil { - return nil, fmt.Errorf("failed to read request nonce: %w", err) + qt := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &qt); err != nil { + return fmt.Errorf("failed to read request type: %w", err) } - queryRequest.Nonce = queryNonce + queryType := ChainSpecificQueryType(qt) - numPerChainQueries := uint8(0) - if err := binary.Read(reader, binary.BigEndian, &numPerChainQueries); err != nil { - return nil, fmt.Errorf("failed to read number of per chain queries: %w", err) + if err := ValidatePerChainQueryRequestType(queryType); err != nil { + return err } - for count := 0; count < int(numPerChainQueries); count++ { - perChainQuery, err := UnmarshalPerChainQueryRequestFromReader(reader) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal per chain query: %w", err) + switch queryType { + case EthCallQueryRequestType: + q := EthCallQueryRequest{} + if err := q.UnmarshalFromReader(reader); err != nil { + return fmt.Errorf("failed to unmarshal eth call request: %w", err) } - queryRequest.PerChainQueries = append(queryRequest.PerChainQueries, perChainQuery) + perChainQuery.Query = &q + default: + return fmt.Errorf("unsupported query type: %d", queryType) } - return queryRequest, nil + return nil } -// UnmarshalPerChainQueryRequest deserializes the binary representation of a per chain query request from a byte array -func UnmarshalPerChainQueryRequest(data []byte) (*gossipv1.PerChainQueryRequest, error) { - reader := bytes.NewReader(data[:]) - return UnmarshalPerChainQueryRequestFromReader(reader) +// Validate does basic validation on a per chain query request. +func (perChainQuery *PerChainQueryRequest) Validate() error { + str := perChainQuery.ChainId.String() + if _, err := vaa.ChainIDFromString(str); err != nil { + return fmt.Errorf("invalid chainID: %d", uint16(perChainQuery.ChainId)) + } + + if perChainQuery.Query == nil { + return fmt.Errorf("query is nil") + } + + if err := ValidatePerChainQueryRequestType(perChainQuery.Query.Type()); err != nil { + return err + } + + if err := perChainQuery.Query.Validate(); err != nil { + return fmt.Errorf("chain specific query is invalid: %w", err) + } + + return nil } -// UnmarshalPerChainQueryRequestFromReader deserializes the binary representation of a per chain query request from an existing reader -func UnmarshalPerChainQueryRequestFromReader(reader *bytes.Reader) (*gossipv1.PerChainQueryRequest, error) { - perChainQuery := &gossipv1.PerChainQueryRequest{} +// Equal verifies that two query requests are equal. +func (left *PerChainQueryRequest) Equal(right *PerChainQueryRequest) bool { + if left.ChainId != right.ChainId { + return false + } - 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 left.Query == nil && right.Query == nil { + return true } - if requestType != QUERY_REQUEST_TYPE_ETH_CALL { - // TODO: support reading different types of request/response pairs - return nil, fmt.Errorf("unsupported request type: %d", requestType) + + if left.Query == nil || right.Query == nil { + return false } - 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) + if left.Query.Type() != right.Query.Type() { + return false } - perChainQuery.ChainId = uint32(queryChain) - ethCallQueryRequest := &gossipv1.EthCallQueryRequest{} + switch leftEcq := left.Query.(type) { + case *EthCallQueryRequest: + switch rightEcd := right.Query.(type) { + case *EthCallQueryRequest: + return leftEcq.Equal(rightEcd) + default: + panic("unsupported query type on right") // We checked this above! + } + default: + panic("unsupported query type on left") // We checked this above! + } +} - 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) +// +// Implementation of EthCallQueryRequest, which implements the ChainSpecificQuery interface. +// + +func (e *EthCallQueryRequest) Type() ChainSpecificQueryType { + return EthCallQueryRequestType +} + +// Marshal serializes the binary representation of an EVM eth_call request. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (ecd *EthCallQueryRequest) Marshal() ([]byte, error) { + if err := ecd.Validate(); err != nil { + return nil, 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) + + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.BlockId))) + buf.Write([]byte(ecd.BlockId)) + + vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecd.CallData))) + for _, callData := range ecd.CallData { + buf.Write(callData.To) + vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data))) + buf.Write(callData.Data) } - ethCallQueryRequest.Block = string(queryEthCallBlockBytes[:]) + return buf.Bytes(), nil +} + +// Unmarshal deserializes an EVM eth_call query from a byte array +func (ecd *EthCallQueryRequest) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return ecd.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes an EVM eth_call query from a byte array +func (ecd *EthCallQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { + blockIdLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &blockIdLen); err != nil { + return fmt.Errorf("failed to read call Data len: %w", err) + } + + blockId := make([]byte, blockIdLen) + if n, err := reader.Read(blockId[:]); err != nil || n != int(blockIdLen) { + return fmt.Errorf("failed to read call To [%d]: %w", n, err) + } + ecd.BlockId = string(blockId[:]) numCallData := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil { - return nil, fmt.Errorf("failed to read number of call data entries: %w", err) + return fmt.Errorf("failed to read number of call data entries: %w", err) } for count := 0; count < int(numCallData); count++ { - queryEthCallTo := [EvmContractAddressLength]byte{} - if n, err := reader.Read(queryEthCallTo[:]); err != nil || n != EvmContractAddressLength { - return nil, fmt.Errorf("failed to read call To [%d]: %w", n, err) + to := [EvmContractAddressLength]byte{} + if n, err := reader.Read(to[:]); err != nil || n != EvmContractAddressLength { + return fmt.Errorf("failed to read call To [%d]: %w", n, err) } - 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) + dataLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &dataLen); err != nil { + return 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) + data := make([]byte, dataLen) + if n, err := reader.Read(data[:]); err != nil || n != int(dataLen) { + return fmt.Errorf("failed to read call To [%d]: %w", n, err) } - callData := &gossipv1.EthCallQueryRequest_EthCallData{ - To: queryEthCallTo[:], - Data: queryEthCallData[:], + callData := &EthCallData{ + To: to[:], + Data: data[:], } - ethCallQueryRequest.CallData = append(ethCallQueryRequest.CallData, callData) - } - - perChainQuery.Message = &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: ethCallQueryRequest, + ecd.CallData = append(ecd.CallData, callData) } - return perChainQuery, nil + return nil } -// ValidateQueryRequest does basic validation on a received query request. -func ValidateQueryRequest(queryRequest *gossipv1.QueryRequest) error { - if len(queryRequest.PerChainQueries) == 0 { - return fmt.Errorf("request does not contain any queries") +// Validate does basic validation on an EVM eth_call query. +func (ecd *EthCallQueryRequest) Validate() error { + if len(ecd.BlockId) > math.MaxUint32 { + return fmt.Errorf("block id too long") } - for _, perChainQuery := range queryRequest.PerChainQueries { - if perChainQuery.ChainId > math.MaxUint16 { - return fmt.Errorf("invalid chain id: %d is out of bounds", perChainQuery.ChainId) + if !strings.HasPrefix(ecd.BlockId, "0x") { + return fmt.Errorf("block id must be a hex number or hash starting with 0x") + } + if len(ecd.CallData) <= 0 { + return fmt.Errorf("does not contain any call data") + } + if len(ecd.CallData) > math.MaxUint8 { + return fmt.Errorf("too many call data entries") + } + for _, callData := range ecd.CallData { + if callData.To == nil || len(callData.To) <= 0 { + return fmt.Errorf("no call data to") } - switch req := perChainQuery.Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - if len(req.EthCallQueryRequest.Block) > math.MaxUint32 { - return fmt.Errorf("request block too long") - } - if !strings.HasPrefix(req.EthCallQueryRequest.Block, "0x") { - return fmt.Errorf("request block must be a hex number or hash starting with 0x") - } - if len(req.EthCallQueryRequest.CallData) == 0 { - return fmt.Errorf("per chain query does not contain any requests") - } - for _, callData := range req.EthCallQueryRequest.CallData { - if len(callData.To) != EvmContractAddressLength { - return fmt.Errorf("invalid length for To contract") - } - if len(callData.Data) > math.MaxUint32 { - return fmt.Errorf("request data too long") - } - } - default: - return fmt.Errorf("received invalid message from query module") + if len(callData.To) != EvmContractAddressLength { + return fmt.Errorf("invalid length for To contract") + } + if callData.Data == nil || len(callData.Data) <= 0 { + return fmt.Errorf("no call data data") + } + if len(callData.Data) > math.MaxUint32 { + return fmt.Errorf("call data data too long") } } return nil } -func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { - if !bytes.Equal(left.QueryRequest, right.QueryRequest) { +// Equal verifies that two EVM eth_call queries are equal. +func (left *EthCallQueryRequest) Equal(right *EthCallQueryRequest) bool { + if left.BlockId != right.BlockId { return false } - if !bytes.Equal(left.Signature, right.Signature) { + if len(left.CallData) != len(right.CallData) { return false } + for idx := range left.CallData { + if !bytes.Equal(left.CallData[idx].To, right.CallData[idx].To) { + return false + } + if !bytes.Equal(left.CallData[idx].Data, right.CallData[idx].Data) { + return false + } + } + return true } -func QueryRequestEqual(left *gossipv1.QueryRequest, right *gossipv1.QueryRequest) bool { - if left.Nonce != right.Nonce { - return false +func ValidatePerChainQueryRequestType(qt ChainSpecificQueryType) error { + if qt != EthCallQueryRequestType { + return fmt.Errorf("invalid query request type: %d", qt) } - if len(left.PerChainQueries) != len(right.PerChainQueries) { + return nil +} + +func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { + if !bytes.Equal(left.QueryRequest, right.QueryRequest) { return false } - - for idx := range left.PerChainQueries { - if left.PerChainQueries[idx].ChainId != right.PerChainQueries[idx].ChainId { - return false - } - - switch reqLeft := left.PerChainQueries[idx].Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - switch reqRight := right.PerChainQueries[idx].Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - if reqLeft.EthCallQueryRequest.Block != reqRight.EthCallQueryRequest.Block { - return false - } - if len(reqLeft.EthCallQueryRequest.CallData) != len(reqRight.EthCallQueryRequest.CallData) { - return false - } - for idx := range reqLeft.EthCallQueryRequest.CallData { - if !bytes.Equal(reqLeft.EthCallQueryRequest.CallData[idx].To, reqRight.EthCallQueryRequest.CallData[idx].To) { - return false - } - if !bytes.Equal(reqLeft.EthCallQueryRequest.CallData[idx].Data, reqRight.EthCallQueryRequest.CallData[idx].Data) { - return false - } - } - default: - return false - } - default: - return false - } + if !bytes.Equal(left.Signature, right.Signature) { + return false } - return true } diff --git a/node/pkg/common/queryResponse.go b/node/pkg/common/queryResponse.go index c8b2c3ac22..f82ff3f371 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/common/queryResponse.go @@ -6,14 +6,12 @@ import ( "encoding/hex" "fmt" "math" - "math/big" "time" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/wormhole-foundation/wormhole/sdk/vaa" - "google.golang.org/protobuf/proto" ) // QueryStatus is the status returned from the watcher to the query handler. @@ -34,72 +32,67 @@ const ( type PerChainQueryResponseInternal struct { RequestID string RequestIdx int - ChainID vaa.ChainID + ChainId vaa.ChainID Status QueryStatus - Results []EthCallQueryResponse + Response ChainSpecificResponse } // CreatePerChainQueryResponseInternal creates a PerChainQueryResponseInternal and returns a pointer to it. -func CreatePerChainQueryResponseInternal(reqId string, reqIdx int, chainID vaa.ChainID, status QueryStatus, results []EthCallQueryResponse) *PerChainQueryResponseInternal { +func CreatePerChainQueryResponseInternal(reqId string, reqIdx int, chainId vaa.ChainID, status QueryStatus, response ChainSpecificResponse) *PerChainQueryResponseInternal { return &PerChainQueryResponseInternal{ RequestID: reqId, RequestIdx: reqIdx, - ChainID: chainID, + ChainId: chainId, Status: status, - Results: results, + Response: response, } } var queryResponsePrefix = []byte("query_response_0000000000000000000|") +// QueryResponsePublication is the response to a QueryRequest. type QueryResponsePublication struct { Request *gossipv1.SignedQueryRequest - PerChainResponses []PerChainQueryResponse + PerChainResponses []*PerChainQueryResponse } +// PerChainQueryResponse represents a query response for a single chain. type PerChainQueryResponse struct { - ChainID uint32 - Responses []EthCallQueryResponse + // ChainId indicates which chain this query was destine for. + ChainId vaa.ChainID + + // Response is the chain specific query data. + Response ChainSpecificResponse } -type EthCallQueryResponse struct { - Number *big.Int - Hash common.Hash - Time time.Time - Result []byte - // NOTE: If you modify this struct, please update the Equal() method for QueryResponsePublication. +// ChainSpecificResponse is the interface that must be implemented by a chain specific response. +type ChainSpecificResponse interface { + Type() ChainSpecificQueryType + Marshal() ([]byte, error) + Unmarshal(data []byte) error + UnmarshalFromReader(reader *bytes.Reader) error + Validate() error } -const ( - QUERY_REQUEST_TYPE_ETH_CALL = uint8(1) -) +// EthCallQueryResponse implements ChainSpecificResponse for an EVM eth_call query response. +type EthCallQueryResponse struct { + BlockNumber uint64 + Hash common.Hash + Time time.Time -func (resp *QueryResponsePublication) RequestID() string { - if resp == nil || resp.Request == nil { - return "nil" - } - return hex.EncodeToString(resp.Request.Signature) + // Results is the array of responses matching CallData in EthCallQueryRequest + Results [][]byte } -// MarshalQueryResponsePublication serializes the binary representation of a query response -func MarshalQueryResponsePublication(msg *QueryResponsePublication) ([]byte, error) { - // TODO: copy request write checks to query module request handling - // TODO: only receive the unmarshalled query request (see note in query.go) - var queryRequest gossipv1.QueryRequest - err := proto.Unmarshal(msg.Request.QueryRequest, &queryRequest) - if err != nil { - return nil, fmt.Errorf("received invalid message from query module") - } - - // Validate things before we start marshalling. - if err := ValidateQueryRequest(&queryRequest); err != nil { - return nil, fmt.Errorf("queryRequest is invalid: %w", err) - } +// +// Implementation of QueryResponsePublication. +// - for idx := range msg.PerChainResponses { - if err := ValidatePerChainResponse(&msg.PerChainResponses[idx]); err != nil { - return nil, fmt.Errorf("invalid per chain response: %w", err) - } +// Marshal serializes the binary representation of a query response. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (msg *QueryResponsePublication) Marshal() ([]byte, error) { + if err := msg.Validate(); err != nil { + return nil, err } buf := new(bytes.Buffer) @@ -108,19 +101,14 @@ func MarshalQueryResponsePublication(msg *QueryResponsePublication) ([]byte, err // TODO: support writing off-chain and on-chain requests // Here, unset represents an off-chain request vaa.MustWrite(buf, binary.BigEndian, vaa.ChainIDUnset) - buf.Write(msg.Request.Signature[:]) - // Request - qrBuf, err := MarshalQueryRequest(&queryRequest) - if err != nil { - return nil, fmt.Errorf("failed to marshal query request") - } - buf.Write(qrBuf) + buf.Write(msg.Request.Signature[:]) + buf.Write(msg.Request.QueryRequest) // Per chain responses vaa.MustWrite(buf, binary.BigEndian, uint8(len(msg.PerChainResponses))) for idx := range msg.PerChainResponses { - pcrBuf, err := MarshalPerChainResponse(&msg.PerChainResponses[idx]) + pcrBuf, err := msg.PerChainResponses[idx].Marshal() if err != nil { return nil, fmt.Errorf("failed to marshal per chain response: %w", err) } @@ -130,74 +118,36 @@ func MarshalQueryResponsePublication(msg *QueryResponsePublication) ([]byte, err return buf.Bytes(), nil } -// MarshalPerChainResponse marshalls a per chain query response. -func MarshalPerChainResponse(pcr *PerChainQueryResponse) ([]byte, error) { - buf := new(bytes.Buffer) - vaa.MustWrite(buf, binary.BigEndian, pcr.ChainID) - vaa.MustWrite(buf, binary.BigEndian, uint8(len(pcr.Responses))) - for _, resp := range pcr.Responses { - vaa.MustWrite(buf, binary.BigEndian, resp.Number.Uint64()) - buf.Write(resp.Hash[:]) - vaa.MustWrite(buf, binary.BigEndian, resp.Time.UnixMicro()) - vaa.MustWrite(buf, binary.BigEndian, uint32(len(resp.Result))) - buf.Write(resp.Result) - } - return buf.Bytes(), nil -} - -// ValidatePerChainResponse performs basic validation on a per chain query response. -func ValidatePerChainResponse(pcr *PerChainQueryResponse) error { - if pcr.ChainID > math.MaxUint16 { - return fmt.Errorf("invalid chain ID") - } - - for _, resp := range pcr.Responses { - if len(resp.Hash) != 32 { - return fmt.Errorf("invalid length for block hash") - } - if len(resp.Result) > math.MaxUint32 { - return fmt.Errorf("response data too long") - } - } - - return 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{} - +func (msg *QueryResponsePublication) Unmarshal(data []byte) error { 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) + return 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) + return 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) + return fmt.Errorf("failed to read signature [%d]: %w", n, err) } signedQueryRequest.Signature = signature[:] - queryRequest, err := UnmarshalQueryRequestFromReader(reader) + queryRequest := QueryRequest{} + err := queryRequest.UnmarshalFromReader(reader) if err != nil { - return nil, fmt.Errorf("failed to unmarshal query request: %w", err) + return fmt.Errorf("failed to unmarshal query request: %w", err) } - queryRequestBytes, err := proto.Marshal(queryRequest) + queryRequestBytes, err := queryRequest.Marshal() if err != nil { - return nil, err + return err } signedQueryRequest.QueryRequest = queryRequestBytes @@ -206,70 +156,74 @@ func UnmarshalQueryResponsePublication(data []byte) (*QueryResponsePublication, // Responses numPerChainResponses := uint8(0) if err := binary.Read(reader, binary.BigEndian, &numPerChainResponses); err != nil { - return nil, fmt.Errorf("failed to read number of per chain responses: %w", err) + return fmt.Errorf("failed to read number of per chain responses: %w", err) } for count := 0; count < int(numPerChainResponses); count++ { - pcr, err := UnmarshalQueryPerChainResponseFromReader(reader) + var pcr PerChainQueryResponse + err := pcr.UnmarshalFromReader(reader) if err != nil { - return nil, fmt.Errorf("failed to unmarshal per chain response: %w", err) + return fmt.Errorf("failed to unmarshal per chain response: %w", err) } - msg.PerChainResponses = append(msg.PerChainResponses, *pcr) + msg.PerChainResponses = append(msg.PerChainResponses, &pcr) } - return msg, nil + return nil } -func UnmarshalQueryPerChainResponseFromReader(reader *bytes.Reader) (*PerChainQueryResponse, error) { - pcr := PerChainQueryResponse{} - - chainID := uint32(0) - if err := binary.Read(reader, binary.BigEndian, &chainID); err != nil { - return nil, fmt.Errorf("failed to read chain ID: %w", err) +// Validate does basic validation on a received query request. +func (msg *QueryResponsePublication) Validate() error { + // Unmarshal and validate the contained query request. + var queryRequest QueryRequest + err := queryRequest.Unmarshal(msg.Request.QueryRequest) + if err != nil { + return fmt.Errorf("failed to unmarshal query request") } - pcr.ChainID = chainID - - numResponses := uint8(0) - if err := binary.Read(reader, binary.BigEndian, &numResponses); err != nil { - return nil, fmt.Errorf("failed to read number of responses: %w", err) + if err := queryRequest.Validate(); err != nil { + return fmt.Errorf("query request is invalid: %w", err) } - for count := 0; count < int(numResponses); count++ { - 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) + if len(msg.PerChainResponses) <= 0 { + return fmt.Errorf("response does not contain any per chain responses") + } + if len(msg.PerChainResponses) > math.MaxUint8 { + return fmt.Errorf("too many per chain responses") + } + if len(msg.PerChainResponses) != len(queryRequest.PerChainQueries) { + return fmt.Errorf("number of responses does not match number of queries") + } + for idx, pcr := range msg.PerChainResponses { + if err := pcr.Validate(); err != nil { + return fmt.Errorf("failed to validate per chain query %d: %w", idx, 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) + if pcr.Response.Type() != queryRequest.PerChainQueries[idx].Query.Type() { + return fmt.Errorf("type of response %d does not match the query", idx) } - queryResponse.Hash = responseHash - - unixMicros := int64(0) - if err := binary.Read(reader, binary.BigEndian, &unixMicros); err != nil { - return nil, fmt.Errorf("failed to read response timestamp: %w", err) - } - queryResponse.Time = time.UnixMicro(unixMicros) + } + return nil +} - 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) +// Equal checks for equality on two query response publications. +func (left *QueryResponsePublication) Equal(right *QueryResponsePublication) bool { + if !bytes.Equal(left.Request.QueryRequest, right.Request.QueryRequest) || !bytes.Equal(left.Request.Signature, right.Request.Signature) { + return false + } + if len(left.PerChainResponses) != len(right.PerChainResponses) { + return false + } + for idx := range left.PerChainResponses { + if !left.PerChainResponses[idx].Equal(right.PerChainResponses[idx]) { + return false } - queryResponse.Result = responseResult[:] - - pcr.Responses = append(pcr.Responses, queryResponse) } + return true +} - return &pcr, nil +func (resp *QueryResponsePublication) RequestID() string { + if resp == nil || resp.Request == nil { + return "nil" + } + return hex.EncodeToString(resp.Request.Signature) } // Similar to sdk/vaa/structs.go, @@ -277,7 +231,7 @@ func UnmarshalQueryPerChainResponseFromReader(reader *bytes.Reader) (*PerChainQu // the first hash (32 bytes) vs the full body data. // TODO: confirm if this works / is worthwhile. func (msg *QueryResponsePublication) SigningDigest() (common.Hash, error) { - msgBytes, err := MarshalQueryResponsePublication(msg) + msgBytes, err := msg.Marshal() if err != nil { return common.Hash{}, err } @@ -289,43 +243,230 @@ func GetQueryResponseDigestFromBytes(b []byte) common.Hash { return crypto.Keccak256Hash(append(queryResponsePrefix, crypto.Keccak256Hash(b).Bytes()...)) } -// Equal checks for equality on two query response publications. -func (left *QueryResponsePublication) Equal(right *QueryResponsePublication) bool { - if !bytes.Equal(left.Request.QueryRequest, right.Request.QueryRequest) || !bytes.Equal(left.Request.Signature, right.Request.Signature) { - return false +// +// Implementation of PerChainQueryResponse. +// + +// Marshal marshalls a per chain query response. +func (perChainResponse *PerChainQueryResponse) Marshal() ([]byte, error) { + if err := perChainResponse.Validate(); err != nil { + return nil, err } - if len(left.PerChainResponses) != len(right.PerChainResponses) { - return false + + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, perChainResponse.ChainId) + vaa.MustWrite(buf, binary.BigEndian, perChainResponse.Response.Type()) + respBuf, err := perChainResponse.Response.Marshal() + if err != nil { + return nil, err } - for idx := range left.PerChainResponses { - if !left.PerChainResponses[idx].Equal(&right.PerChainResponses[idx]) { - return false + buf.Write(respBuf) + return buf.Bytes(), nil +} + +// Unmarshal deserializes the binary representation of a per chain query response from a byte array +func (perChainResponse *PerChainQueryResponse) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return perChainResponse.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes the binary representation of a per chain query response from an existing reader +func (perChainResponse *PerChainQueryResponse) UnmarshalFromReader(reader *bytes.Reader) error { + if err := binary.Read(reader, binary.BigEndian, &perChainResponse.ChainId); err != nil { + return fmt.Errorf("failed to read response chain: %w", err) + } + + qt := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &qt); err != nil { + return fmt.Errorf("failed to read response type: %w", err) + } + queryType := ChainSpecificQueryType(qt) + + if err := ValidatePerChainQueryRequestType(queryType); err != nil { + return err + } + + switch queryType { + case EthCallQueryRequestType: + r := EthCallQueryResponse{} + if err := r.UnmarshalFromReader(reader); err != nil { + return fmt.Errorf("failed to unmarshal eth call response: %w", err) } + perChainResponse.Response = &r + default: + return fmt.Errorf("unsupported query type: %d", queryType) } - return true + + return nil +} + +// ValidatePerChainResponse performs basic validation on a per chain query response. +func (perChainResponse *PerChainQueryResponse) Validate() error { + str := perChainResponse.ChainId.String() + if _, err := vaa.ChainIDFromString(str); err != nil { + return fmt.Errorf("invalid chainID: %d", uint16(perChainResponse.ChainId)) + } + + if perChainResponse.Response == nil { + return fmt.Errorf("response is nil") + } + + if err := ValidatePerChainQueryRequestType(perChainResponse.Response.Type()); err != nil { + return err + } + + if err := perChainResponse.Response.Validate(); err != nil { + return fmt.Errorf("chain specific response is invalid: %w", err) + } + + return nil } // Equal checks for equality on two per chain query responses. func (left *PerChainQueryResponse) Equal(right *PerChainQueryResponse) bool { - if left.ChainID != right.ChainID { + if left.ChainId != right.ChainId { return false } - if len(left.Responses) != len(right.Responses) { + + if left.Response == nil && right.Response == nil { + return true + } + + if left.Response == nil || right.Response == nil { return false } - for idx := range left.Responses { - if left.Responses[idx].Number.Cmp(right.Responses[idx].Number) != 0 { - return false + + if left.Response.Type() != right.Response.Type() { + return false + } + + switch leftEcq := left.Response.(type) { + case *EthCallQueryResponse: + switch rightEcd := right.Response.(type) { + case *EthCallQueryResponse: + return leftEcq.Equal(rightEcd) + default: + panic("unsupported query type on right") // We checked this above! } - if !bytes.Equal(left.Responses[idx].Hash.Bytes(), right.Responses[idx].Hash.Bytes()) { - return false + default: + panic("unsupported query type on left") // We checked this above! + } +} + +// +// Implementation of EthCallQueryResponse, which implements the ChainSpecificResponse for an EVM eth_call query response. +// + +func (e *EthCallQueryResponse) Type() ChainSpecificQueryType { + return EthCallQueryRequestType +} + +// Marshal serializes the binary representation of an EVM eth_call response. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (ecr *EthCallQueryResponse) Marshal() ([]byte, error) { + if err := ecr.Validate(); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, ecr.BlockNumber) + buf.Write(ecr.Hash[:]) + vaa.MustWrite(buf, binary.BigEndian, ecr.Time.UnixMicro()) + + vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecr.Results))) + for idx := range ecr.Results { + vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecr.Results[idx]))) + buf.Write(ecr.Results[idx]) + } + + return buf.Bytes(), nil +} + +// Unmarshal deserializes an EVM eth_call response from a byte array +func (ecr *EthCallQueryResponse) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return ecr.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes an EVM eth_call response from a byte array +func (ecr *EthCallQueryResponse) UnmarshalFromReader(reader *bytes.Reader) error { + if err := binary.Read(reader, binary.BigEndian, &ecr.BlockNumber); err != nil { + return fmt.Errorf("failed to read response number: %w", err) + } + + responseHash := common.Hash{} + if n, err := reader.Read(responseHash[:]); err != nil || n != 32 { + return fmt.Errorf("failed to read response hash [%d]: %w", n, err) + } + ecr.Hash = responseHash + + unixMicros := int64(0) + if err := binary.Read(reader, binary.BigEndian, &unixMicros); err != nil { + return fmt.Errorf("failed to read response timestamp: %w", err) + } + ecr.Time = time.UnixMicro(unixMicros) + + numResults := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numResults); err != nil { + return fmt.Errorf("failed to read number of results: %w", err) + } + + for count := 0; count < int(numResults); count++ { + resultLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &resultLen); err != nil { + return fmt.Errorf("failed to read result len: %w", err) } - if left.Responses[idx].Time != right.Responses[idx].Time { - return false + result := make([]byte, resultLen) + if n, err := reader.Read(result[:]); err != nil || n != int(resultLen) { + return fmt.Errorf("failed to read result [%d]: %w", n, err) } - if !bytes.Equal(left.Responses[idx].Result, right.Responses[idx].Result) { + + ecr.Results = append(ecr.Results, result) + } + + return nil +} + +// Validate does basic validation on an EVM eth_call response. +func (ecr *EthCallQueryResponse) Validate() error { + // Not checking for BlockNumber == 0, because maybe that could happen?? + + if len(ecr.Hash) != 32 { + return fmt.Errorf("invalid length for block hash") + } + + if len(ecr.Results) <= 0 { + return fmt.Errorf("does not contain any results") + } + if len(ecr.Results) > math.MaxUint8 { + return fmt.Errorf("too many results") + } + for _, result := range ecr.Results { + if len(result) > math.MaxUint32 { + return fmt.Errorf("result too long") + } + } + return nil +} + +// Equal verifies that two EVM eth_call responses are equal. +func (left *EthCallQueryResponse) Equal(right *EthCallQueryResponse) bool { + if left.BlockNumber != right.BlockNumber { + return false + } + + if !bytes.Equal(left.Hash.Bytes(), right.Hash.Bytes()) { + return false + } + + if len(left.Results) != len(right.Results) { + return false + } + for idx := range left.Results { + if !bytes.Equal(left.Results[idx], right.Results[idx]) { return false } } + return true } diff --git a/node/pkg/common/query_test.go b/node/pkg/common/query_test.go index 9004ff9ee4..618f60b290 100644 --- a/node/pkg/common/query_test.go +++ b/node/pkg/common/query_test.go @@ -2,23 +2,22 @@ package common import ( "encoding/hex" - "math/big" + "fmt" "strings" "testing" "time" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/wormhole-foundation/wormhole/sdk/vaa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" - - "google.golang.org/protobuf/proto" ) -func createQueryRequestForTesting() *gossipv1.QueryRequest { +func createQueryRequestForTesting(chainId vaa.ChainID) *QueryRequest { // Create a query request. 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 { @@ -36,7 +35,7 @@ func createQueryRequestForTesting() *gossipv1.QueryRequest { to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") block := "0x28d9630" - callData := []*gossipv1.EthCallQueryRequest_EthCallData{ + callData := []*EthCallData{ { To: to, Data: data1, @@ -46,21 +45,19 @@ func createQueryRequestForTesting() *gossipv1.QueryRequest { Data: data2, }, } - callRequest := &gossipv1.EthCallQueryRequest{ - Block: block, + callRequest := &EthCallQueryRequest{ + BlockId: block, CallData: callData, } - perChainQuery := &gossipv1.PerChainQueryRequest{ - ChainId: 5, - Message: &gossipv1.PerChainQueryRequest_EthCallQueryRequest{ - EthCallQueryRequest: callRequest, - }, + perChainQuery := &PerChainQueryRequest{ + ChainId: chainId, + Query: callRequest, } - queryRequest := &gossipv1.QueryRequest{ + queryRequest := &QueryRequest{ Nonce: 1, - PerChainQueries: []*gossipv1.PerChainQueryRequest{perChainQuery}, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, } return queryRequest @@ -71,32 +68,282 @@ func timeForTest(t time.Time) time.Time { return time.UnixMicro(t.UnixMicro()) } -func TestQueryRequestProtoMarshalUnMarshal(t *testing.T) { - queryRequest := createQueryRequestForTesting() - queryRequestBytes, err := proto.Marshal(queryRequest) +func TestQueryRequestMarshalUnmarshal(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + queryRequestBytes, err := queryRequest.Marshal() require.NoError(t, err) - var queryRequest2 gossipv1.QueryRequest - err = proto.Unmarshal(queryRequestBytes, &queryRequest2) + var queryRequest2 QueryRequest + err = queryRequest2.Unmarshal(queryRequestBytes) require.NoError(t, err) - assert.True(t, QueryRequestEqual(queryRequest, &queryRequest2)) + assert.True(t, queryRequest.Equal(&queryRequest2)) } -func TestQueryRequestMarshalUnMarshal(t *testing.T) { - queryRequest := createQueryRequestForTesting() - queryRequestBytes, err := MarshalQueryRequest(queryRequest) - require.NoError(t, err) +func TestMarshalOfQueryRequestWithNoPerChainQueriesShouldFail(t *testing.T) { + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{ + { + ChainId: vaa.ChainIDPolygon, + // Leave Query nil. + }, + }, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} - queryRequest2, err := UnmarshalQueryRequest(queryRequestBytes) - require.NoError(t, err) +func TestMarshalOfQueryRequestWithTooManyPerChainQueriesShouldFail(t *testing.T) { + perChainQueries := []*PerChainQueryRequest{} + for count := 0; count < 300; count++ { + callData := []*EthCallData{{ + + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", count))), + Data: []byte(fmt.Sprintf("CallData for %d", count)), + }, + } - assert.True(t, QueryRequestEqual(queryRequest, queryRequest2)) + perChainQueries = append(perChainQueries, &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: callData, + }, + }) + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: perChainQueries, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfQueryRequestForInvalidChainIdShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDUnset) + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfQueryRequestWithInvalidBlockIdShouldFail(t *testing.T) { + callData := []*EthCallData{{ + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", 0))), + Data: []byte(fmt.Sprintf("CallData for %d", 0)), + }} + + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "latest", + CallData: callData, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfQueryRequestWithNoCallDataEntriesShouldFail(t *testing.T) { + callData := []*EthCallData{} + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: callData, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfQueryRequestWithNilCallDataEntriesShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: nil, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfQueryRequestWithTooManyCallDataEntriesShouldFail(t *testing.T) { + callData := []*EthCallData{} + for count := 0; count < 300; count++ { + callData = append(callData, &EthCallData{ + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", count))), + Data: []byte(fmt.Sprintf("CallData for %d", count)), + }) + } + + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: callData, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfEthCallQueryWithNilToShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: nil, + Data: []byte("This can't be zero length"), + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfEthCallQueryWithEmptyToShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: []byte{}, + Data: []byte("This can't be zero length"), + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfEthCallQueryWithWrongLengthToShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: []byte("TooShort"), + Data: []byte("This can't be zero length"), + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfEthCallQueryWithNilDataShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", 0))), + Data: nil, + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) } -func TestQueryResponseMarshalUnMarshal(t *testing.T) { - queryRequest := createQueryRequestForTesting() - queryRequestBytes, err := proto.Marshal(queryRequest) +func TestMarshalOfEthCallQueryWithEmptyDataShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", 0))), + Data: []byte{}, + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestMarshalOfEthCallQueryWithWrongToLengthShouldFail(t *testing.T) { + perChainQuery := &PerChainQueryRequest{ + ChainId: vaa.ChainIDPolygon, + Query: &EthCallQueryRequest{ + BlockId: "0x28d9630", + CallData: []*EthCallData{ + { + To: []byte("This is too short!"), + Data: []byte("This can't be zero length"), + }, + }, + }, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery}, + } + _, err := queryRequest.Marshal() + require.Error(t, err) +} + +func TestPostSignedQueryRequestShouldFailIfNoOneIsListening(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + queryRequestBytes, err := queryRequest.Marshal() require.NoError(t, err) sig := [65]byte{} @@ -105,57 +352,68 @@ func TestQueryResponseMarshalUnMarshal(t *testing.T) { Signature: sig[:], } - results, err := hex.DecodeString("010203040506070809") + var signedQueryReqSendC chan<- *gossipv1.SignedQueryRequest + assert.Error(t, PostSignedQueryRequest(signedQueryReqSendC, signedQueryRequest)) +} + +func createQueryResponseFromRequest(t *testing.T, queryRequest *QueryRequest) *QueryResponsePublication { + queryRequestBytes, err := queryRequest.Marshal() require.NoError(t, err) - respPub := &QueryResponsePublication{ - Request: signedQueryRequest, - PerChainResponses: []PerChainQueryResponse{ - { - ChainID: 5, - Responses: []EthCallQueryResponse{ - { - Number: big.NewInt(42), - Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), - Time: timeForTest(time.Now()), - Result: results, - }, - { - Number: big.NewInt(43), - Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef9deadbeef"), - Time: timeForTest(time.Now()), - Result: results, - }, - }, - }, - { - ChainID: 11, - Responses: []EthCallQueryResponse{ - { - Number: big.NewInt(44), - Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e3"), - Time: timeForTest(time.Now()), - Result: results, - }, + sig := [65]byte{} + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig[:], + } + + perChainResponses := []*PerChainQueryResponse{} + for idx, pcr := range queryRequest.PerChainQueries { + switch req := pcr.Query.(type) { + case *EthCallQueryRequest: + results := [][]byte{} + for idx := range req.CallData { + result := []byte([]byte(fmt.Sprintf("Result %d", idx))) + results = append(results, result[:]) + } + perChainResponses = append(perChainResponses, &PerChainQueryResponse{ + ChainId: pcr.ChainId, + Response: &EthCallQueryResponse{ + BlockNumber: uint64(1000 + idx), + Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), + Time: timeForTest(time.Now()), + Results: results, }, - }, - }, + }) + default: + panic("invalid query type!") + } + } - respPubBytes, err := MarshalQueryResponsePublication(respPub) + return &QueryResponsePublication{ + Request: signedQueryRequest, + PerChainResponses: perChainResponses, + } +} + +func TestQueryResponseMarshalUnmarshal(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + respPubBytes, err := respPub.Marshal() require.NoError(t, err) - respPub2, err := UnmarshalQueryResponsePublication(respPubBytes) + var respPub2 QueryResponsePublication + err = respPub2.Unmarshal(respPubBytes) require.NoError(t, err) require.NotNil(t, respPub2) - assert.True(t, respPub.Equal(respPub2)) + assert.True(t, respPub.Equal(&respPub2)) } -/* -func TesMarshalUnMarshalQueryResponseWithNoResults(t *testing.T) { - queryRequest := createQueryRequestForTesting() - queryRequestBytes, err := proto.Marshal(queryRequest) +func TestMarshalUnmarshalQueryResponseWithNoPerChainResponsesShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + queryRequestBytes, err := queryRequest.Marshal() require.NoError(t, err) sig := [65]byte{} @@ -165,17 +423,122 @@ func TesMarshalUnMarshalQueryResponseWithNoResults(t *testing.T) { } respPub := &QueryResponsePublication{ - Request: signedQueryRequest, - Responses: nil, + Request: signedQueryRequest, + PerChainResponses: []*PerChainQueryResponse{}, } - respPubBytes, err := MarshalQueryResponsePublication(respPub) - require.NoError(t, err) + _, err = respPub.Marshal() + require.Error(t, err) +} - respPub2, err := UnmarshalQueryResponsePublication(respPubBytes) +func TestMarshalUnmarshalQueryResponseWithNilPerChainResponsesShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + queryRequestBytes, err := queryRequest.Marshal() require.NoError(t, err) - require.NotNil(t, respPub2) - assert.True(t, respPub.Equal(respPub2)) + sig := [65]byte{} + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig[:], + } + + respPub := &QueryResponsePublication{ + Request: signedQueryRequest, + PerChainResponses: nil, + } + + _, err = respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithTooManyPerChainResponsesShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + for count := 0; count < 300; count++ { + respPub.PerChainResponses = append(respPub.PerChainResponses, respPub.PerChainResponses[0]) + } + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithWrongNumberOfPerChainResponsesShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + respPub.PerChainResponses = append(respPub.PerChainResponses, respPub.PerChainResponses[0]) + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithInvalidChainIDShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + respPub.PerChainResponses[0].ChainId = vaa.ChainIDUnset + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithNilResponseShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + respPub.PerChainResponses[0].Response = nil + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithNoResultsShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + switch resp := respPub.PerChainResponses[0].Response.(type) { + case *EthCallQueryResponse: + resp.Results = [][]byte{} + default: + panic("invalid query type!") + } + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithNilResultsShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + switch resp := respPub.PerChainResponses[0].Response.(type) { + case *EthCallQueryResponse: + resp.Results = nil + default: + panic("invalid query type!") + } + + _, err := respPub.Marshal() + require.Error(t, err) +} + +func TestMarshalUnmarshalQueryResponseWithTooManyResultsShouldFail(t *testing.T) { + queryRequest := createQueryRequestForTesting(vaa.ChainIDPolygon) + respPub := createQueryResponseFromRequest(t, queryRequest) + + results := [][]byte{} + for count := 0; count < 300; count++ { + results = append(results, []byte{}) + } + + switch resp := respPub.PerChainResponses[0].Response.(type) { + case *EthCallQueryResponse: + resp.Results = results + default: + panic("invalid query type!") + } + + _, err := respPub.Marshal() + require.Error(t, err) } -*/ diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 55d0412943..6d18899457 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -507,7 +507,7 @@ func Run( logger.Error("received a cross chain query response when the feature is disabled, dropping it", zap.String("component", "ccqp2p")) continue } - msgBytes, err := node_common.MarshalQueryResponsePublication(msg) + msgBytes, err := msg.Marshal() if err != nil { logger.Error("failed to marshal query response", zap.Error(err), zap.String("component", "ccqp2p")) continue diff --git a/node/pkg/proto/gossip/v1/gossip.pb.go b/node/pkg/proto/gossip/v1/gossip.pb.go index 04691f6769..2165fe345d 100644 --- a/node/pkg/proto/gossip/v1/gossip.pb.go +++ b/node/pkg/proto/gossip/v1/gossip.pb.go @@ -1212,191 +1212,6 @@ func (x *SignedQueryRequest) GetSignature() []byte { return nil } -type QueryRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Nonce uint32 `protobuf:"varint,1,opt,name=nonce,proto3" json:"nonce,omitempty"` - PerChainQueries []*PerChainQueryRequest `protobuf:"bytes,2,rep,name=per_chain_queries,json=perChainQueries,proto3" json:"per_chain_queries,omitempty"` -} - -func (x *QueryRequest) Reset() { - *x = QueryRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *QueryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryRequest) ProtoMessage() {} - -func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. -func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{14} -} - -func (x *QueryRequest) GetNonce() uint32 { - if x != nil { - return x.Nonce - } - return 0 -} - -func (x *QueryRequest) GetPerChainQueries() []*PerChainQueryRequest { - if x != nil { - return x.PerChainQueries - } - return nil -} - -type PerChainQueryRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ChainId uint32 `protobuf:"varint,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` - // Types that are assignable to Message: - // - // *PerChainQueryRequest_EthCallQueryRequest - Message isPerChainQueryRequest_Message `protobuf_oneof:"message"` -} - -func (x *PerChainQueryRequest) Reset() { - *x = PerChainQueryRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *PerChainQueryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PerChainQueryRequest) ProtoMessage() {} - -func (x *PerChainQueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PerChainQueryRequest.ProtoReflect.Descriptor instead. -func (*PerChainQueryRequest) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{15} -} - -func (x *PerChainQueryRequest) GetChainId() uint32 { - if x != nil { - return x.ChainId - } - return 0 -} - -func (m *PerChainQueryRequest) GetMessage() isPerChainQueryRequest_Message { - if m != nil { - return m.Message - } - return nil -} - -func (x *PerChainQueryRequest) GetEthCallQueryRequest() *EthCallQueryRequest { - if x, ok := x.GetMessage().(*PerChainQueryRequest_EthCallQueryRequest); ok { - return x.EthCallQueryRequest - } - return nil -} - -type isPerChainQueryRequest_Message interface { - isPerChainQueryRequest_Message() -} - -type PerChainQueryRequest_EthCallQueryRequest struct { - EthCallQueryRequest *EthCallQueryRequest `protobuf:"bytes,3,opt,name=eth_call_query_request,json=ethCallQueryRequest,proto3,oneof"` -} - -func (*PerChainQueryRequest_EthCallQueryRequest) isPerChainQueryRequest_Message() {} - -type EthCallQueryRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Block string `protobuf:"bytes,1,opt,name=block,proto3" json:"block,omitempty"` - CallData []*EthCallQueryRequest_EthCallData `protobuf:"bytes,2,rep,name=call_data,json=callData,proto3" json:"call_data,omitempty"` -} - -func (x *EthCallQueryRequest) Reset() { - *x = EthCallQueryRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EthCallQueryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EthCallQueryRequest) ProtoMessage() {} - -func (x *EthCallQueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EthCallQueryRequest.ProtoReflect.Descriptor instead. -func (*EthCallQueryRequest) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16} -} - -func (x *EthCallQueryRequest) GetBlock() string { - if x != nil { - return x.Block - } - return "" -} - -func (x *EthCallQueryRequest) GetCallData() []*EthCallQueryRequest_EthCallData { - if x != nil { - return x.CallData - } - return nil -} - type SignedQueryResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1411,7 +1226,7 @@ type SignedQueryResponse struct { func (x *SignedQueryResponse) Reset() { *x = SignedQueryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1424,7 +1239,7 @@ func (x *SignedQueryResponse) String() string { func (*SignedQueryResponse) ProtoMessage() {} func (x *SignedQueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[17] + mi := &file_gossip_v1_gossip_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1437,7 +1252,7 @@ func (x *SignedQueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedQueryResponse.ProtoReflect.Descriptor instead. func (*SignedQueryResponse) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{17} + return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{14} } func (x *SignedQueryResponse) GetQueryResponse() []byte { @@ -1472,7 +1287,7 @@ type Heartbeat_Network struct { func (x *Heartbeat_Network) Reset() { *x = Heartbeat_Network{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1485,7 +1300,7 @@ func (x *Heartbeat_Network) String() string { func (*Heartbeat_Network) ProtoMessage() {} func (x *Heartbeat_Network) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[18] + mi := &file_gossip_v1_gossip_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1542,7 +1357,7 @@ type ChainGovernorConfig_Chain struct { func (x *ChainGovernorConfig_Chain) Reset() { *x = ChainGovernorConfig_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1555,7 +1370,7 @@ func (x *ChainGovernorConfig_Chain) String() string { func (*ChainGovernorConfig_Chain) ProtoMessage() {} func (x *ChainGovernorConfig_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[19] + mi := &file_gossip_v1_gossip_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1605,7 +1420,7 @@ type ChainGovernorConfig_Token struct { func (x *ChainGovernorConfig_Token) Reset() { *x = ChainGovernorConfig_Token{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1618,7 +1433,7 @@ func (x *ChainGovernorConfig_Token) String() string { func (*ChainGovernorConfig_Token) ProtoMessage() {} func (x *ChainGovernorConfig_Token) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[20] + mi := &file_gossip_v1_gossip_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1669,7 +1484,7 @@ type ChainGovernorStatus_EnqueuedVAA struct { func (x *ChainGovernorStatus_EnqueuedVAA) Reset() { *x = ChainGovernorStatus_EnqueuedVAA{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1682,7 +1497,7 @@ func (x *ChainGovernorStatus_EnqueuedVAA) String() string { func (*ChainGovernorStatus_EnqueuedVAA) ProtoMessage() {} func (x *ChainGovernorStatus_EnqueuedVAA) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[21] + mi := &file_gossip_v1_gossip_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1739,7 +1554,7 @@ type ChainGovernorStatus_Emitter struct { func (x *ChainGovernorStatus_Emitter) Reset() { *x = ChainGovernorStatus_Emitter{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[22] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1752,7 +1567,7 @@ func (x *ChainGovernorStatus_Emitter) String() string { func (*ChainGovernorStatus_Emitter) ProtoMessage() {} func (x *ChainGovernorStatus_Emitter) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[22] + mi := &file_gossip_v1_gossip_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1802,7 +1617,7 @@ type ChainGovernorStatus_Chain struct { func (x *ChainGovernorStatus_Chain) Reset() { *x = ChainGovernorStatus_Chain{} if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[23] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1815,7 +1630,7 @@ func (x *ChainGovernorStatus_Chain) String() string { func (*ChainGovernorStatus_Chain) ProtoMessage() {} func (x *ChainGovernorStatus_Chain) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[23] + mi := &file_gossip_v1_gossip_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1852,61 +1667,6 @@ func (x *ChainGovernorStatus_Chain) GetEmitters() []*ChainGovernorStatus_Emitter return nil } -type EthCallQueryRequest_EthCallData struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - To []byte `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` -} - -func (x *EthCallQueryRequest_EthCallData) Reset() { - *x = EthCallQueryRequest_EthCallData{} - if protoimpl.UnsafeEnabled { - mi := &file_gossip_v1_gossip_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EthCallQueryRequest_EthCallData) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EthCallQueryRequest_EthCallData) ProtoMessage() {} - -func (x *EthCallQueryRequest_EthCallData) ProtoReflect() protoreflect.Message { - mi := &file_gossip_v1_gossip_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EthCallQueryRequest_EthCallData.ProtoReflect.Descriptor instead. -func (*EthCallQueryRequest_EthCallData) Descriptor() ([]byte, []int) { - return file_gossip_v1_gossip_proto_rawDescGZIP(), []int{16, 0} -} - -func (x *EthCallQueryRequest_EthCallData) GetTo() []byte { - if x != nil { - return x.To - } - return nil -} - -func (x *EthCallQueryRequest_EthCallData) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - var File_gossip_v1_gossip_proto protoreflect.FileDescriptor var file_gossip_v1_gossip_proto_rawDesc = []byte{ @@ -2145,44 +1905,17 @@ var file_gossip_v1_gossip_proto_rawDesc = []byte{ 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x22, 0x71, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x70, 0x65, - 0x72, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x65, 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x51, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x14, 0x50, 0x65, 0x72, 0x43, - 0x68, 0x61, 0x69, 0x6e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x55, 0x0a, 0x16, 0x65, - 0x74, 0x68, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, - 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, - 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x65, - 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xa7, 0x01, - 0x0a, 0x13, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x47, 0x0a, 0x09, 0x63, - 0x61, 0x6c, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, - 0x2e, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x74, 0x68, 0x43, 0x61, - 0x6c, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, - 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x63, 0x61, 0x6c, 0x6c, - 0x44, 0x61, 0x74, 0x61, 0x1a, 0x31, 0x0a, 0x0b, 0x45, 0x74, 0x68, 0x43, 0x61, 0x6c, 0x6c, 0x44, - 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x02, 0x74, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5a, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, - 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, - 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, - 0x73, 0x73, 0x69, 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x75, 0x72, 0x65, 0x22, 0x5a, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, + 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6d, 0x68, 0x6f, 0x6c, + 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, 0x73, 0x73, 0x69, + 0x70, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2197,7 +1930,7 @@ func file_gossip_v1_gossip_proto_rawDescGZIP() []byte { return file_gossip_v1_gossip_proto_rawDescData } -var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_gossip_v1_gossip_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*GossipMessage)(nil), // 0: gossip.v1.GossipMessage (*SignedHeartbeat)(nil), // 1: gossip.v1.SignedHeartbeat @@ -2213,17 +1946,13 @@ var file_gossip_v1_gossip_proto_goTypes = []interface{}{ (*SignedChainGovernorStatus)(nil), // 11: gossip.v1.SignedChainGovernorStatus (*ChainGovernorStatus)(nil), // 12: gossip.v1.ChainGovernorStatus (*SignedQueryRequest)(nil), // 13: gossip.v1.SignedQueryRequest - (*QueryRequest)(nil), // 14: gossip.v1.QueryRequest - (*PerChainQueryRequest)(nil), // 15: gossip.v1.PerChainQueryRequest - (*EthCallQueryRequest)(nil), // 16: gossip.v1.EthCallQueryRequest - (*SignedQueryResponse)(nil), // 17: gossip.v1.SignedQueryResponse - (*Heartbeat_Network)(nil), // 18: gossip.v1.Heartbeat.Network - (*ChainGovernorConfig_Chain)(nil), // 19: gossip.v1.ChainGovernorConfig.Chain - (*ChainGovernorConfig_Token)(nil), // 20: gossip.v1.ChainGovernorConfig.Token - (*ChainGovernorStatus_EnqueuedVAA)(nil), // 21: gossip.v1.ChainGovernorStatus.EnqueuedVAA - (*ChainGovernorStatus_Emitter)(nil), // 22: gossip.v1.ChainGovernorStatus.Emitter - (*ChainGovernorStatus_Chain)(nil), // 23: gossip.v1.ChainGovernorStatus.Chain - (*EthCallQueryRequest_EthCallData)(nil), // 24: gossip.v1.EthCallQueryRequest.EthCallData + (*SignedQueryResponse)(nil), // 14: gossip.v1.SignedQueryResponse + (*Heartbeat_Network)(nil), // 15: gossip.v1.Heartbeat.Network + (*ChainGovernorConfig_Chain)(nil), // 16: gossip.v1.ChainGovernorConfig.Chain + (*ChainGovernorConfig_Token)(nil), // 17: gossip.v1.ChainGovernorConfig.Token + (*ChainGovernorStatus_EnqueuedVAA)(nil), // 18: gossip.v1.ChainGovernorStatus.EnqueuedVAA + (*ChainGovernorStatus_Emitter)(nil), // 19: gossip.v1.ChainGovernorStatus.Emitter + (*ChainGovernorStatus_Chain)(nil), // 20: gossip.v1.ChainGovernorStatus.Chain } var file_gossip_v1_gossip_proto_depIdxs = []int32{ 3, // 0: gossip.v1.GossipMessage.signed_observation:type_name -> gossip.v1.SignedObservation @@ -2235,21 +1964,18 @@ var file_gossip_v1_gossip_proto_depIdxs = []int32{ 9, // 6: gossip.v1.GossipMessage.signed_chain_governor_config:type_name -> gossip.v1.SignedChainGovernorConfig 11, // 7: gossip.v1.GossipMessage.signed_chain_governor_status:type_name -> gossip.v1.SignedChainGovernorStatus 13, // 8: gossip.v1.GossipMessage.signed_query_request:type_name -> gossip.v1.SignedQueryRequest - 17, // 9: gossip.v1.GossipMessage.signed_query_response:type_name -> gossip.v1.SignedQueryResponse - 18, // 10: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network - 19, // 11: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain - 20, // 12: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token - 23, // 13: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain - 15, // 14: gossip.v1.QueryRequest.per_chain_queries:type_name -> gossip.v1.PerChainQueryRequest - 16, // 15: gossip.v1.PerChainQueryRequest.eth_call_query_request:type_name -> gossip.v1.EthCallQueryRequest - 24, // 16: gossip.v1.EthCallQueryRequest.call_data:type_name -> gossip.v1.EthCallQueryRequest.EthCallData - 21, // 17: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA - 22, // 18: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter - 19, // [19:19] is the sub-list for method output_type - 19, // [19:19] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 14, // 9: gossip.v1.GossipMessage.signed_query_response:type_name -> gossip.v1.SignedQueryResponse + 15, // 10: gossip.v1.Heartbeat.networks:type_name -> gossip.v1.Heartbeat.Network + 16, // 11: gossip.v1.ChainGovernorConfig.chains:type_name -> gossip.v1.ChainGovernorConfig.Chain + 17, // 12: gossip.v1.ChainGovernorConfig.tokens:type_name -> gossip.v1.ChainGovernorConfig.Token + 20, // 13: gossip.v1.ChainGovernorStatus.chains:type_name -> gossip.v1.ChainGovernorStatus.Chain + 18, // 14: gossip.v1.ChainGovernorStatus.Emitter.enqueued_vaas:type_name -> gossip.v1.ChainGovernorStatus.EnqueuedVAA + 19, // 15: gossip.v1.ChainGovernorStatus.Chain.emitters:type_name -> gossip.v1.ChainGovernorStatus.Emitter + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_gossip_v1_gossip_proto_init() } @@ -2427,42 +2153,6 @@ func file_gossip_v1_gossip_proto_init() { } } file_gossip_v1_gossip_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*QueryRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_gossip_v1_gossip_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PerChainQueryRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_gossip_v1_gossip_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EthCallQueryRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_gossip_v1_gossip_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SignedQueryResponse); i { case 0: return &v.state @@ -2474,7 +2164,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Heartbeat_Network); i { case 0: return &v.state @@ -2486,7 +2176,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorConfig_Chain); i { case 0: return &v.state @@ -2498,7 +2188,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorConfig_Token); i { case 0: return &v.state @@ -2510,7 +2200,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_EnqueuedVAA); i { case 0: return &v.state @@ -2522,7 +2212,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_Emitter); i { case 0: return &v.state @@ -2534,7 +2224,7 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + file_gossip_v1_gossip_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChainGovernorStatus_Chain); i { case 0: return &v.state @@ -2546,18 +2236,6 @@ func file_gossip_v1_gossip_proto_init() { return nil } } - file_gossip_v1_gossip_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EthCallQueryRequest_EthCallData); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } } file_gossip_v1_gossip_proto_msgTypes[0].OneofWrappers = []interface{}{ (*GossipMessage_SignedObservation)(nil), @@ -2571,16 +2249,13 @@ func file_gossip_v1_gossip_proto_init() { (*GossipMessage_SignedQueryRequest)(nil), (*GossipMessage_SignedQueryResponse)(nil), } - file_gossip_v1_gossip_proto_msgTypes[15].OneofWrappers = []interface{}{ - (*PerChainQueryRequest_EthCallQueryRequest)(nil), - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gossip_v1_gossip_proto_rawDesc, NumEnums: 0, - NumMessages: 25, + NumMessages: 21, NumExtensions: 0, NumServices: 0, }, diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 49bc8be892..b7609943dc 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -3,6 +3,8 @@ package evm import ( "context" "fmt" + "math" + "math/big" "sync" "sync/atomic" "time" @@ -527,6 +529,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { }) common.RunWithScissors(ctx, errC, "evm_fetch_query_req", func(ctx context.Context) error { + ccqMaxBlockNumber := big.NewInt(0).SetUint64(math.MaxUint64) for { select { case <-ctx.Done(): @@ -534,17 +537,17 @@ func (w *Watcher) Run(parentCtx context.Context) error { case queryRequest := <-w.queryReqC: // This can't happen unless there is a programming error - the caller // is expected to send us only requests for our chainID. - if queryRequest.ChainID != w.chainID { + if queryRequest.Request.ChainId != w.chainID { panic("ccqevm: invalid chain ID") } - switch req := queryRequest.Request.Message.(type) { - case *gossipv1.PerChainQueryRequest_EthCallQueryRequest: - block := req.EthCallQueryRequest.Block + switch req := queryRequest.Request.Query.(type) { + case *common.EthCallQueryRequest: + block := req.BlockId logger.Info("received query request", zap.String("eth_network", w.networkName), zap.String("block", block), - zap.Int("numRequests", len(req.EthCallQueryRequest.CallData)), + zap.Int("numRequests", len(req.CallData)), zap.String("component", "ccqevm"), ) @@ -592,7 +595,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { evmCallData := []EvmCallData{} // Add each requested query to the batch. - for _, callData := range req.EthCallQueryRequest.CallData { + for _, callData := range req.CallData { // like https://github.com/ethereum/go-ethereum/blob/master/ethclient/ethclient.go#L610 to := eth_common.BytesToAddress(callData.To) data := eth_hexutil.Encode(callData.Data) @@ -668,10 +671,26 @@ func (w *Watcher) Run(parentCtx context.Context) error { continue } - resp := []common.EthCallQueryResponse{} + if blockResult.Number.ToInt().Cmp(ccqMaxBlockNumber) > 0 { + logger.Error("block number too large", + zap.String("eth_network", w.networkName), + zap.String("block", block), + zap.Any("batch", batch), + zap.String("component", "ccqevm"), + ) + w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + continue + } + + resp := common.EthCallQueryResponse{ + BlockNumber: blockResult.Number.ToInt().Uint64(), + Hash: blockResult.Hash, + Time: time.Unix(int64(blockResult.Time), 0), + Results: [][]byte{}, + } errFound := false - for idx := range req.EthCallQueryRequest.CallData { + for idx := range req.CallData { if evmCallData[idx].callErr != nil { logger.Error("failed to process query call request", zap.Error(evmCallData[idx].callErr), zap.String("eth_network", w.networkName), @@ -713,21 +732,16 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.String("component", "ccqevm"), ) - resp = append(resp, common.EthCallQueryResponse{ - Number: blockResult.Number.ToInt(), - Hash: blockResult.Hash, - Time: time.Unix(int64(blockResult.Time), 0), - Result: *evmCallData[idx].callResult, - }) + resp.Results = append(resp.Results, *evmCallData[idx].callResult) } if !errFound { - w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, resp) + w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, &resp) } default: logger.Warn("received unsupported request type", - zap.Any("payload", queryRequest.Request.Message), + zap.Uint8("payload", uint8(queryRequest.Request.Query.Type())), zap.String("component", "ccqevm"), ) w.ccqSendQueryResponse(logger, queryRequest, common.QueryFatalError, nil) @@ -1153,8 +1167,8 @@ func (w *Watcher) SetMaxWaitConfirmations(maxWaitConfirmations uint64) { } // ccqSendQueryResponse sends an error response back to the query handler. -func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.PerChainQueryInternal, status common.QueryStatus, results []common.EthCallQueryResponse) { - queryResponse := common.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.ChainID, status, results) +func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.PerChainQueryInternal, status common.QueryStatus, resp *common.EthCallQueryResponse) { + queryResponse := common.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, resp) select { case w.queryResponseC <- queryResponse: logger.Debug("published query response error to handler", zap.String("component", "ccqevm")) diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index aae54677ce..04685a831d 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -242,28 +242,6 @@ message SignedQueryRequest { bytes signature = 2; } -message QueryRequest { - uint32 nonce = 1; - repeated PerChainQueryRequest per_chain_queries = 2; -} - -message PerChainQueryRequest { - uint32 chain_id = 1; - oneof message { - EthCallQueryRequest eth_call_query_request = 3; - } -} - -message EthCallQueryRequest { - string block = 1; - repeated EthCallData call_data = 2; - - message EthCallData { - bytes to = 1; - bytes data = 2; - } -} - message SignedQueryResponse { // Serialized QueryResponse message. bytes query_response = 1; From f2a06110ac1fb535cbd30577254ddbf0f10de976 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:23:11 -0500 Subject: [PATCH 16/37] CCQ: Reorg code into a query package (#3112) * CCQ: Reorg code into a query package * Make requestTimeout public --- node/hack/query/send_req.go | 39 +++--- node/hack/query/test/query_test.go | 19 +-- node/pkg/p2p/p2p.go | 7 +- node/pkg/query/helpers_test.go | 58 ++++++++ .../query_test.go => query/msg_test.go} | 2 +- node/{cmd/guardiand => pkg/query}/query.go | 62 ++++----- .../guardiand => pkg/query}/query_test.go | 130 +++++++++--------- .../queryRequest.go => query/request.go} | 11 +- .../queryResponse.go => query/response.go} | 2 +- node/pkg/watchers/evm/watcher.go | 33 ++--- 10 files changed, 210 insertions(+), 153 deletions(-) create mode 100644 node/pkg/query/helpers_test.go rename node/pkg/{common/query_test.go => query/msg_test.go} (99%) rename node/{cmd/guardiand => pkg/query}/query.go (87%) rename node/{cmd/guardiand => pkg/query}/query_test.go (83%) rename node/pkg/{common/queryRequest.go => query/request.go} (98%) rename node/pkg/{common/queryResponse.go => query/response.go} (99%) diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index fe4bb254b3..6dd95de0d2 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -20,6 +20,7 @@ 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/certusone/wormhole/node/pkg/query" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common/hexutil" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -181,7 +182,7 @@ func main() { } methods := []string{"name", "totalSupply"} - callData := []*common.EthCallData{} + callData := []*query.EthCallData{} to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") for _, method := range methods { @@ -190,7 +191,7 @@ func main() { panic(err) } - callData = append(callData, &common.EthCallData{ + callData = append(callData, &query.EthCallData{ To: to, Data: data, }) @@ -211,7 +212,7 @@ func main() { // block := "0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2" // Start of query creation... - callRequest := &common.EthCallQueryRequest{ + callRequest := &query.EthCallQueryRequest{ BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } @@ -228,7 +229,7 @@ func main() { // Second request... blockNum = blockNum.Sub(blockNum, big.NewInt(5)) - callRequest2 := &common.EthCallQueryRequest{ + callRequest2 := &query.EthCallQueryRequest{ BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } @@ -239,7 +240,7 @@ func main() { // Now, want to send a single query with multiple requests... logger.Info("Starting multiquery test in 5...") time.Sleep(time.Second * 5) - multiCallRequest := []*common.EthCallQueryRequest{callRequest, callRequest2} + multiCallRequest := []*query.EthCallQueryRequest{callRequest, callRequest2} multQueryRequest := createQueryRequestWithMultipleRequests(multiCallRequest) sendQueryAndGetRsp(multQueryRequest, sk, th, ctx, logger, sub, wethAbi, methods) @@ -264,10 +265,10 @@ const ( GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" ) -func createQueryRequest(callRequest *common.EthCallQueryRequest) *common.QueryRequest { - queryRequest := &common.QueryRequest{ +func createQueryRequest(callRequest *query.EthCallQueryRequest) *query.QueryRequest { + queryRequest := &query.QueryRequest{ Nonce: rand.Uint32(), - PerChainQueries: []*common.PerChainQueryRequest{ + PerChainQueries: []*query.PerChainQueryRequest{ { ChainId: 5, Query: callRequest, @@ -277,23 +278,23 @@ func createQueryRequest(callRequest *common.EthCallQueryRequest) *common.QueryRe return queryRequest } -func createQueryRequestWithMultipleRequests(callRequests []*common.EthCallQueryRequest) *common.QueryRequest { - perChainQueries := []*common.PerChainQueryRequest{} +func createQueryRequestWithMultipleRequests(callRequests []*query.EthCallQueryRequest) *query.QueryRequest { + perChainQueries := []*query.PerChainQueryRequest{} for _, req := range callRequests { - perChainQueries = append(perChainQueries, &common.PerChainQueryRequest{ + perChainQueries = append(perChainQueries, &query.PerChainQueryRequest{ ChainId: 5, Query: req, }) } - queryRequest := &common.QueryRequest{ + queryRequest := &query.QueryRequest{ Nonce: rand.Uint32(), PerChainQueries: perChainQueries, } return queryRequest } -func sendQueryAndGetRsp(queryRequest *common.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription, wethAbi abi.ABI, methods []string) { +func sendQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription, wethAbi abi.ABI, methods []string) { queryRequestBytes, err := queryRequest.Marshal() if err != nil { panic(err) @@ -301,7 +302,7 @@ func sendQueryAndGetRsp(queryRequest *common.QueryRequest, sk *ecdsa.PrivateKey, numQueries := len(queryRequest.PerChainQueries) // Sign the query request using our private key. - digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) + digest := query.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) sig, err := ethCrypto.Sign(digest.Bytes(), sk) if err != nil { panic(err) @@ -348,7 +349,7 @@ func sendQueryAndGetRsp(queryRequest *common.QueryRequest, sk *ecdsa.PrivateKey, switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) - var response common.QueryResponsePublication + var response query.QueryResponsePublication err := response.Unmarshal(m.SignedQueryResponse.QueryResponse) if err != nil { logger.Warn("failed to unmarshal response", zap.Error(err)) @@ -366,17 +367,17 @@ func sendQueryAndGetRsp(queryRequest *common.QueryRequest, sk *ecdsa.PrivateKey, for index := range response.PerChainResponses { logger.Info("per chain query response index", zap.Int("index", index)) - var localCallData []*common.EthCallData + var localCallData []*query.EthCallData switch ecq := queryRequest.PerChainQueries[index].Query.(type) { - case *common.EthCallQueryRequest: + case *query.EthCallQueryRequest: localCallData = ecq.CallData default: panic("unsupported query type") } - var localResp *common.EthCallQueryResponse + var localResp *query.EthCallQueryResponse switch ecq := response.PerChainResponses[index].Response.(type) { - case *common.EthCallQueryResponse: + case *query.EthCallQueryResponse: localResp = ecq default: panic("unsupported query type") diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index 4ed7707e55..e115743ce6 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -20,6 +20,7 @@ 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/certusone/wormhole/node/pkg/query" "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -185,21 +186,21 @@ func TestCrossChainQuery(t *testing.T) { } to, _ := hex.DecodeString("DDb64fE46a91D46ee29420539FC25FD07c5FEa3E") // WETH - callData := []*common.EthCallData{ + callData := []*query.EthCallData{ { To: to, Data: data, }, } - callRequest := &common.EthCallQueryRequest{ + callRequest := &query.EthCallQueryRequest{ BlockId: hexutil.EncodeBig(blockNum), CallData: callData, } - queryRequest := &common.QueryRequest{ + queryRequest := &query.QueryRequest{ Nonce: 1, - PerChainQueries: []*common.PerChainQueryRequest{ + PerChainQueries: []*query.PerChainQueryRequest{ { ChainId: 2, Query: callRequest, @@ -213,7 +214,7 @@ func TestCrossChainQuery(t *testing.T) { } // Sign the query request using our private key. - digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) + digest := query.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) sig, err := ethCrypto.Sign(digest.Bytes(), sk) if err != nil { panic(err) @@ -260,13 +261,13 @@ func TestCrossChainQuery(t *testing.T) { switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) - var response common.QueryResponsePublication + var response query.QueryResponsePublication err := response.Unmarshal(m.SignedQueryResponse.QueryResponse) if err != nil { logger.Fatal("failed to unmarshal response", zap.Error(err)) } if bytes.Equal(response.Request.QueryRequest, queryRequestBytes) && bytes.Equal(response.Request.Signature, sig) { - digest := common.GetQueryResponseDigestFromBytes(m.SignedQueryResponse.QueryResponse) + digest := query.GetQueryResponseDigestFromBytes(m.SignedQueryResponse.QueryResponse) signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), m.SignedQueryResponse.Signature) if err != nil { logger.Fatal("failed to verify signature on response", @@ -296,9 +297,9 @@ func TestCrossChainQuery(t *testing.T) { break } - var pcq *common.EthCallQueryResponse + var pcq *query.EthCallQueryResponse switch ecq := response.PerChainResponses[0].Response.(type) { - case *common.EthCallQueryResponse: + case *query.EthCallQueryResponse: pcq = ecq default: panic("unsupported query type") diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 6d18899457..981e3f2411 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -13,6 +13,7 @@ import ( "github.com/certusone/wormhole/node/pkg/accountant" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/governor" + "github.com/certusone/wormhole/node/pkg/query" "github.com/certusone/wormhole/node/pkg/version" eth_common "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" @@ -212,7 +213,7 @@ func Run( gatewayRelayerEnabled bool, ccqEnabled bool, signedQueryReqC chan<- *gossipv1.SignedQueryRequest, - queryResponseReadC <-chan *node_common.QueryResponsePublication, + queryResponseReadC <-chan *query.QueryResponsePublication, ) func(ctx context.Context) error { if components == nil { components = DefaultComponents() @@ -512,7 +513,7 @@ func Run( logger.Error("failed to marshal query response", zap.Error(err), zap.String("component", "ccqp2p")) continue } - digest := node_common.GetQueryResponseDigestFromBytes(msgBytes) + digest := query.GetQueryResponseDigestFromBytes(msgBytes) sig, err := ethcrypto.Sign(digest.Bytes(), gk) if err != nil { panic(err) @@ -702,7 +703,7 @@ func Run( case *gossipv1.GossipMessage_SignedQueryRequest: if signedQueryReqC != nil { if ccqEnabled { - if err := node_common.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { + if err := query.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { logger.Warn("failed to handle query request", zap.Error(err), zap.String("component", "ccqp2p")) } } else { diff --git a/node/pkg/query/helpers_test.go b/node/pkg/query/helpers_test.go new file mode 100644 index 0000000000..16ebc9152c --- /dev/null +++ b/node/pkg/query/helpers_test.go @@ -0,0 +1,58 @@ +package query + +import ( + "crypto/ecdsa" + "fmt" + "io" + "os" + + nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1" + + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/openpgp/armor" //nolint + "google.golang.org/protobuf/proto" +) + +const ( + GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" +) + +// loadGuardianKey loads a serialized guardian key from disk. +func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { + f, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + p, err := armor.Decode(f) + if err != nil { + return nil, fmt.Errorf("failed to read armored file: %w", err) + } + + if p.Type != GuardianKeyArmoredBlock { + return nil, fmt.Errorf("invalid block type: %s", p.Type) + } + + b, err := io.ReadAll(p.Body) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var m nodev1.GuardianKey + err = proto.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) + } + + gk, err := ethCrypto.ToECDSA(m.Data) + if err != nil { + return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) + } + + return gk, nil +} + +func makeChannelPair[T any](cap int) (<-chan T, chan<- T) { + out := make(chan T, cap) + return out, out +} diff --git a/node/pkg/common/query_test.go b/node/pkg/query/msg_test.go similarity index 99% rename from node/pkg/common/query_test.go rename to node/pkg/query/msg_test.go index 618f60b290..ce461f40ae 100644 --- a/node/pkg/common/query_test.go +++ b/node/pkg/query/msg_test.go @@ -1,4 +1,4 @@ -package common +package query import ( "encoding/hex" diff --git a/node/cmd/guardiand/query.go b/node/pkg/query/query.go similarity index 87% rename from node/cmd/guardiand/query.go rename to node/pkg/query/query.go index b5c61d51f1..49f222ab14 100644 --- a/node/cmd/guardiand/query.go +++ b/node/pkg/query/query.go @@ -1,4 +1,4 @@ -package guardiand +package query import ( "context" @@ -18,47 +18,47 @@ import ( ) const ( - // requestTimeout indicates how long before a request is considered to have timed out. - requestTimeout = 1 * time.Minute + // RequestTimeout indicates how long before a request is considered to have timed out. + RequestTimeout = 1 * time.Minute - // retryInterval specifies how long we will wait between retry intervals. This is the interval of our ticker. - retryInterval = 10 * time.Second + // RetryInterval specifies how long we will wait between retry intervals. This is the interval of our ticker. + RetryInterval = 10 * time.Second ) type ( // pendingQuery is the cache entry for a given query. pendingQuery struct { signedRequest *gossipv1.SignedQueryRequest - request *common.QueryRequest + request *QueryRequest requestID string receiveTime time.Time queries []*perChainQuery - responses []*common.PerChainQueryResponseInternal + responses []*PerChainQueryResponseInternal // respPub is only populated when we need to retry sending the response to p2p. - respPub *common.QueryResponsePublication + respPub *QueryResponsePublication } // perChainQuery is the data associated with a single per chain query in a query request. perChainQuery struct { - req *common.PerChainQueryInternal - channel chan *common.PerChainQueryInternal + req *PerChainQueryInternal + channel chan *PerChainQueryInternal lastUpdateTime time.Time } ) -// handleQueryRequests multiplexes observation requests to the appropriate chain -func handleQueryRequests( +// HandleQueryRequests multiplexes observation requests to the appropriate chain +func HandleQueryRequests( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal, + chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal, allowedRequestors map[ethCommon.Address]struct{}, - queryResponseReadC <-chan *common.PerChainQueryResponseInternal, - queryResponseWriteC chan<- *common.QueryResponsePublication, + queryResponseReadC <-chan *PerChainQueryResponseInternal, + queryResponseWriteC chan<- *QueryResponsePublication, env common.Environment, ) { - handleQueryRequestsImpl(ctx, logger, signedQueryReqC, chainQueryReqC, allowedRequestors, queryResponseReadC, queryResponseWriteC, env, requestTimeout, retryInterval) + handleQueryRequestsImpl(ctx, logger, signedQueryReqC, chainQueryReqC, allowedRequestors, queryResponseReadC, queryResponseWriteC, env, RequestTimeout, RetryInterval) } // handleQueryRequestsImpl allows instantiating the handler in the test environment with shorter timeout and retry parameters. @@ -66,10 +66,10 @@ func handleQueryRequestsImpl( ctx context.Context, logger *zap.Logger, signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal, + chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal, allowedRequestors map[ethCommon.Address]struct{}, - queryResponseReadC <-chan *common.PerChainQueryResponseInternal, - queryResponseWriteC chan<- *common.QueryResponsePublication, + queryResponseReadC <-chan *PerChainQueryResponseInternal, + queryResponseWriteC chan<- *QueryResponsePublication, env common.Environment, requestTimeoutImpl time.Duration, retryIntervalImpl time.Duration, @@ -119,7 +119,7 @@ func handleQueryRequestsImpl( // - valid "block" strings requestID := hex.EncodeToString(signedRequest.Signature) - digest := common.QueryRequestDigest(env, signedRequest.QueryRequest) + digest := QueryRequestDigest(env, signedRequest.QueryRequest) signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedRequest.Signature) if err != nil { @@ -140,7 +140,7 @@ func handleQueryRequestsImpl( continue } - var queryRequest common.QueryRequest + var queryRequest QueryRequest err = queryRequest.Unmarshal(signedRequest.QueryRequest) if err != nil { qLogger.Error("failed to unmarshal query request", zap.String("requestor", signerAddress.Hex()), zap.String("requestID", requestID), zap.Error(err)) @@ -155,7 +155,7 @@ func handleQueryRequestsImpl( // Build the set of per chain queries and placeholders for the per chain responses. errorFound := false queries := []*perChainQuery{} - responses := make([]*common.PerChainQueryResponseInternal, len(queryRequest.PerChainQueries)) + responses := make([]*PerChainQueryResponseInternal, len(queryRequest.PerChainQueries)) receiveTime := time.Now() for requestIdx, pcq := range queryRequest.PerChainQueries { @@ -174,7 +174,7 @@ func handleQueryRequestsImpl( } queries = append(queries, &perChainQuery{ - req: &common.PerChainQueryInternal{ + req: &PerChainQueryInternal{ RequestID: requestID, RequestIdx: requestIdx, Request: pcq, @@ -204,7 +204,7 @@ func handleQueryRequestsImpl( } case resp := <-queryResponseReadC: // Response from a watcher. - if resp.Status == common.QuerySuccess { + if resp.Status == QuerySuccess { if resp.Response == nil { qLogger.Error("received a successful query response with no results, dropping it!", zap.String("requestID", resp.RequestID)) continue @@ -234,20 +234,20 @@ func handleQueryRequestsImpl( } // Build the list of per chain response publications and the overall query response publication. - responses := []*common.PerChainQueryResponse{} + responses := []*PerChainQueryResponse{} for _, resp := range pq.responses { if resp == nil { qLogger.Error("unexpected null response in pending query!", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) continue } - responses = append(responses, &common.PerChainQueryResponse{ + responses = append(responses, &PerChainQueryResponse{ ChainId: resp.ChainId, Response: resp.Response, }) } - respPub := &common.QueryResponsePublication{ + respPub := &QueryResponsePublication{ Request: pq.signedRequest, PerChainResponses: responses, } @@ -261,13 +261,13 @@ func handleQueryRequestsImpl( qLogger.Warn("failed to publish query response to p2p, will retry publishing next interval", zap.String("requestID", resp.RequestID)) pq.respPub = respPub } - } else if resp.Status == common.QueryRetryNeeded { + } else if resp.Status == QueryRetryNeeded { if _, exists := pendingQueries[resp.RequestID]; exists { qLogger.Warn("query failed, will retry next interval", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) } else { qLogger.Warn("received a retry needed response with no outstanding query, dropping it", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) } - } else if resp.Status == common.QueryFatalError { + } else if resp.Status == QueryFatalError { qLogger.Warn("received a fatal error response, dropping the whole request", zap.String("requestID", resp.RequestID), zap.Int("requestIdx", resp.RequestIdx)) delete(pendingQueries, resp.RequestID) } else { @@ -307,8 +307,8 @@ func handleQueryRequestsImpl( } } -// ccqParseAllowedRequesters parses a comma separated list of allowed requesters into a map to be used for look ups. -func ccqParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { +// ParseAllowedRequesters parses a comma separated list of allowed requesters into a map to be used for look ups. +func ParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { if ccqAllowedRequesters == "" { return nil, fmt.Errorf("if cross chain query is enabled `--ccqAllowedRequesters` must be specified") } diff --git a/node/cmd/guardiand/query_test.go b/node/pkg/query/query_test.go similarity index 83% rename from node/cmd/guardiand/query_test.go rename to node/pkg/query/query_test.go index 92a8ed14bc..d55e1c3c84 100644 --- a/node/cmd/guardiand/query_test.go +++ b/node/pkg/query/query_test.go @@ -1,4 +1,4 @@ -package guardiand +package query import ( "bytes" @@ -50,21 +50,21 @@ func createPerChainQueryForTesting( chainId vaa.ChainID, block string, numCalls int, -) *common.PerChainQueryRequest { - callData := []*common.EthCallData{} +) *PerChainQueryRequest { + callData := []*EthCallData{} for count := 0; count < numCalls; count++ { - callData = append(callData, &common.EthCallData{ + callData = append(callData, &EthCallData{ To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d:%d", chainId, count))), Data: []byte(fmt.Sprintf("CallData for %d:%d", chainId, count)), }) } - callRequest := &common.EthCallQueryRequest{ + callRequest := &EthCallQueryRequest{ BlockId: block, CallData: callData, } - return &common.PerChainQueryRequest{ + return &PerChainQueryRequest{ ChainId: chainId, Query: callRequest, } @@ -73,10 +73,10 @@ func createPerChainQueryForTesting( // createSignedQueryRequestForTesting creates a query request object and signs it using the specified key. func createSignedQueryRequestForTesting( sk *ecdsa.PrivateKey, - perChainQueries []*common.PerChainQueryRequest, -) (*gossipv1.SignedQueryRequest, *common.QueryRequest) { + perChainQueries []*PerChainQueryRequest, +) (*gossipv1.SignedQueryRequest, *QueryRequest) { nonce += 1 - queryRequest := &common.QueryRequest{ + queryRequest := &QueryRequest{ Nonce: nonce, PerChainQueries: perChainQueries, } @@ -86,7 +86,7 @@ func createSignedQueryRequestForTesting( panic(err) } - digest := common.QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) + digest := QueryRequestDigest(common.UnsafeDevNet, queryRequestBytes) sig, err := ethCrypto.Sign(digest.Bytes(), sk) if err != nil { panic(err) @@ -101,17 +101,17 @@ func createSignedQueryRequestForTesting( } // createExpectedResultsForTest generates an array of the results expected for a request. These results are returned by the watcher, and used to validate the response. -func createExpectedResultsForTest(perChainQueries []*common.PerChainQueryRequest) []common.PerChainQueryResponse { - expectedResults := []common.PerChainQueryResponse{} +func createExpectedResultsForTest(perChainQueries []*PerChainQueryRequest) []PerChainQueryResponse { + expectedResults := []PerChainQueryResponse{} for _, pcq := range perChainQueries { switch req := pcq.Query.(type) { - case *common.EthCallQueryRequest: + case *EthCallQueryRequest: now := time.Now() blockNum, err := strconv.ParseUint(strings.TrimPrefix(req.BlockId, "0x"), 16, 64) if err != nil { panic("invalid blockNum!") } - resp := &common.EthCallQueryResponse{ + resp := &EthCallQueryResponse{ BlockNumber: blockNum, Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), Time: timeForTest(timeForTest(now)), @@ -120,7 +120,7 @@ func createExpectedResultsForTest(perChainQueries []*common.PerChainQueryRequest for _, cd := range req.CallData { resp.Results = append(resp.Results, []byte(hex.EncodeToString(cd.To)+":"+hex.EncodeToString(cd.Data))) } - expectedResults = append(expectedResults, common.PerChainQueryResponse{ + expectedResults = append(expectedResults, PerChainQueryResponse{ ChainId: pcq.ChainId, Response: resp, }) @@ -136,13 +136,13 @@ func createExpectedResultsForTest(perChainQueries []*common.PerChainQueryRequest // validateResponseForTest performs validation on the responses generated by these tests. Note that it is not a generalized validate function. func validateResponseForTest( t *testing.T, - response *common.QueryResponsePublication, + response *QueryResponsePublication, signedRequest *gossipv1.SignedQueryRequest, - queryRequest *common.QueryRequest, - expectedResults []common.PerChainQueryResponse, + queryRequest *QueryRequest, + expectedResults []PerChainQueryResponse, ) bool { require.NotNil(t, response) - require.True(t, common.SignedQueryRequestEqual(signedRequest, response.Request)) + require.True(t, SignedQueryRequestEqual(signedRequest, response.Request)) require.Equal(t, len(queryRequest.PerChainQueries), len(response.PerChainResponses)) require.True(t, bytes.Equal(response.Request.Signature, signedRequest.Signature)) require.Equal(t, len(response.PerChainResponses), len(expectedResults)) @@ -153,13 +153,8 @@ func validateResponseForTest( return true } -// A timestamp has nanos, but we only marshal down to micros, so trim our time to micros for testing purposes. -func timeForTest(t time.Time) time.Time { - return time.UnixMicro(t.UnixMicro()) -} - -func TestCcqParseAllowedRequestersSuccess(t *testing.T) { - ccqAllowedRequestersList, err := ccqParseAllowedRequesters(testSigner) +func TestParseAllowedRequestersSuccess(t *testing.T) { + ccqAllowedRequestersList, err := ParseAllowedRequesters(testSigner) require.NoError(t, err) require.NotNil(t, ccqAllowedRequestersList) require.Equal(t, 1, len(ccqAllowedRequestersList)) @@ -169,7 +164,7 @@ func TestCcqParseAllowedRequestersSuccess(t *testing.T) { _, exists = ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf"))] require.False(t, exists) - ccqAllowedRequestersList, err = ccqParseAllowedRequesters("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf") + ccqAllowedRequestersList, err = ParseAllowedRequesters("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf") require.NoError(t, err) require.NotNil(t, ccqAllowedRequestersList) require.Equal(t, 2, len(ccqAllowedRequestersList)) @@ -180,18 +175,18 @@ func TestCcqParseAllowedRequestersSuccess(t *testing.T) { require.True(t, exists) } -func TestCcqParseAllowedRequestersFailsIfParameterEmpty(t *testing.T) { - ccqAllowedRequestersList, err := ccqParseAllowedRequesters("") +func TestParseAllowedRequestersFailsIfParameterEmpty(t *testing.T) { + ccqAllowedRequestersList, err := ParseAllowedRequesters("") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) - ccqAllowedRequestersList, err = ccqParseAllowedRequesters(",") + ccqAllowedRequestersList, err = ParseAllowedRequesters(",") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) } -func TestCcqParseAllowedRequestersFailsIfInvalidParameter(t *testing.T) { - ccqAllowedRequestersList, err := ccqParseAllowedRequesters("Hello") +func TestParseAllowedRequestersFailsIfInvalidParameter(t *testing.T) { + ccqAllowedRequestersList, err := ParseAllowedRequesters("Hello") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) } @@ -203,17 +198,17 @@ type mockData struct { signedQueryReqReadC <-chan *gossipv1.SignedQueryRequest signedQueryReqWriteC chan<- *gossipv1.SignedQueryRequest - chainQueryReqC map[vaa.ChainID]chan *common.PerChainQueryInternal + chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal - queryResponseReadC <-chan *common.PerChainQueryResponseInternal - queryResponseWriteC chan<- *common.PerChainQueryResponseInternal + queryResponseReadC <-chan *PerChainQueryResponseInternal + queryResponseWriteC chan<- *PerChainQueryResponseInternal - queryResponsePublicationReadC <-chan *common.QueryResponsePublication - queryResponsePublicationWriteC chan<- *common.QueryResponsePublication + queryResponsePublicationReadC <-chan *QueryResponsePublication + queryResponsePublicationWriteC chan<- *QueryResponsePublication mutex sync.Mutex - queryResponsePublication *common.QueryResponsePublication - expectedResults []common.PerChainQueryResponse + queryResponsePublication *QueryResponsePublication + expectedResults []PerChainQueryResponse requestsPerChain map[vaa.ChainID]int retriesPerChain map[vaa.ChainID]int } @@ -229,14 +224,14 @@ func (md *mockData) resetState() { } // setExpectedResults sets the results to be returned by the watchers. -func (md *mockData) setExpectedResults(expectedResults []common.PerChainQueryResponse) { +func (md *mockData) setExpectedResults(expectedResults []PerChainQueryResponse) { md.mutex.Lock() defer md.mutex.Unlock() md.expectedResults = expectedResults } // setRetries allows a test to specify how many times a given watcher should retry before returning success. -// If the count is the special value `fatalError`, the watcher will return common.QueryFatalError. +// If the count is the special value `fatalError`, the watcher will return QueryFatalError. func (md *mockData) setRetries(chainId vaa.ChainID, count int) { md.mutex.Lock() defer md.mutex.Unlock() @@ -253,7 +248,7 @@ func (md *mockData) incrementRequestsPerChainAlreadyLocked(chainId vaa.ChainID) } // getQueryResponsePublication returns the latest query response publication received by the mock. -func (md *mockData) getQueryResponsePublication() *common.QueryResponsePublication { +func (md *mockData) getQueryResponsePublication() *QueryResponsePublication { md.mutex.Lock() defer md.mutex.Unlock() return md.queryResponsePublication @@ -281,10 +276,10 @@ func (md *mockData) shouldIgnoreAlreadyLocked(chainId vaa.ChainID) bool { } // getStatusAlreadyLocked is used by the watchers to determine what query status they should return, based on the `retriesPerChain`. -func (md *mockData) getStatusAlreadyLocked(chainId vaa.ChainID) common.QueryStatus { +func (md *mockData) getStatusAlreadyLocked(chainId vaa.ChainID) QueryStatus { if val, exists := md.retriesPerChain[chainId]; exists { if val == fatalError { - return common.QueryFatalError + return QueryFatalError } val -= 1 if val > 0 { @@ -292,9 +287,9 @@ func (md *mockData) getStatusAlreadyLocked(chainId vaa.ChainID) common.QueryStat } else { delete(md.retriesPerChain, chainId) } - return common.QueryRetryNeeded + return QueryRetryNeeded } - return common.QuerySuccess + return QuerySuccess } // createQueryHandlerForTest creates the query handler mock environment, including the set of watchers and the response listener. @@ -311,28 +306,27 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context md := mockData{} var err error - *unsafeDevMode = true md.sk, err = loadGuardianKey("../../hack/query/dev.guardian.key") require.NoError(t, err) require.NotNil(t, md.sk) - ccqAllowedRequestersList, err := ccqParseAllowedRequesters(testSigner) + ccqAllowedRequestersList, err := ParseAllowedRequesters(testSigner) require.NoError(t, err) // Inbound observation requests from the p2p service (for all chains) - md.signedQueryReqReadC, md.signedQueryReqWriteC = makeChannelPair[*gossipv1.SignedQueryRequest](common.SignedQueryRequestChannelSize) + md.signedQueryReqReadC, md.signedQueryReqWriteC = makeChannelPair[*gossipv1.SignedQueryRequest](SignedQueryRequestChannelSize) // Per-chain query requests - md.chainQueryReqC = make(map[vaa.ChainID]chan *common.PerChainQueryInternal) + md.chainQueryReqC = make(map[vaa.ChainID]chan *PerChainQueryInternal) for _, chainId := range chains { - md.chainQueryReqC[chainId] = make(chan *common.PerChainQueryInternal) + md.chainQueryReqC[chainId] = make(chan *PerChainQueryInternal) } // Query responses from watchers to query handler aggregated across all chains - md.queryResponseReadC, md.queryResponseWriteC = makeChannelPair[*common.PerChainQueryResponseInternal](0) + md.queryResponseReadC, md.queryResponseWriteC = makeChannelPair[*PerChainQueryResponseInternal](0) // Query responses from query handler to p2p - md.queryResponsePublicationReadC, md.queryResponsePublicationWriteC = makeChannelPair[*common.QueryResponsePublication](0) + md.queryResponsePublicationReadC, md.queryResponsePublicationWriteC = makeChannelPair[*QueryResponsePublication](0) md.resetState() @@ -342,7 +336,7 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context // Create a routine for each configured watcher. It will take a per chain query and return the corresponding expected result. // It also pegs a counter of the number of requests the watcher received, for verification purposes. for chainId := range md.chainQueryReqC { - go func(chainId vaa.ChainID, chainQueryReqC <-chan *common.PerChainQueryInternal) { + go func(chainId vaa.ChainID, chainQueryReqC <-chan *PerChainQueryInternal) { for { select { case <-ctx.Done(): @@ -357,7 +351,7 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context results := md.expectedResults[pcqr.RequestIdx].Response status := md.getStatusAlreadyLocked(chainId) logger.Info("watcher returning", zap.String("chainId", chainId.String()), zap.Int("requestIdx", pcqr.RequestIdx), zap.Int("status", int(status))) - queryResponse := common.CreatePerChainQueryResponseInternal(pcqr.RequestID, pcqr.RequestIdx, pcqr.Request.ChainId, status, results) + queryResponse := CreatePerChainQueryResponseInternal(pcqr.RequestID, pcqr.RequestIdx, pcqr.Request.ChainId, status, results) md.queryResponseWriteC <- queryResponse } md.mutex.Unlock() @@ -387,7 +381,7 @@ func (md *mockData) startResponseListener(ctx context.Context) { } // waitForResponse is used by the tests to wait for a response publication. It will eventually timeout if the query fails. -func (md *mockData) waitForResponse() *common.QueryResponsePublication { +func (md *mockData) waitForResponse() *QueryResponsePublication { for count := 0; count < 50; count++ { time.Sleep(pollIntervalForTest) ret := md.getQueryResponsePublication() @@ -406,12 +400,12 @@ func TestInvalidQueries(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) - var perChainQueries []*common.PerChainQueryRequest + var perChainQueries []*PerChainQueryRequest var signedQueryRequest *gossipv1.SignedQueryRequest // Query with a bad signature should fail. md.resetState() - perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries = []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) signedQueryRequest.Signature[0] += 1 // Corrupt the signature. md.signedQueryReqWriteC <- signedQueryRequest @@ -419,14 +413,14 @@ func TestInvalidQueries(t *testing.T) { // Query for an unsupported chain should fail. The supported chains are defined in supportedChains in query.go md.resetState() - perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDAlgorand, "0x28d9630", 2)} + perChainQueries = []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDAlgorand, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) md.signedQueryReqWriteC <- signedQueryRequest require.Nil(t, md.waitForResponse()) // Query for a chain that supports queries but that is not in the watcher channel map should fail. md.resetState() - perChainQueries = []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDSepolia, "0x28d9630", 2)} + perChainQueries = []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDSepolia, "0x28d9630", 2)} signedQueryRequest, _ = createSignedQueryRequestForTesting(md.sk, perChainQueries) md.signedQueryReqWriteC <- signedQueryRequest require.Nil(t, md.waitForResponse()) @@ -440,7 +434,7 @@ func TestSingleQueryShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -464,7 +458,7 @@ func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{ + perChainQueries := []*PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -492,7 +486,7 @@ func TestQueryWithLimitedRetriesShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -520,7 +514,7 @@ func TestQueryWithRetryDueToTimeoutShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) @@ -547,7 +541,7 @@ func TestQueryWithTooManyRetriesShouldFail(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{ + perChainQueries := []*PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -580,7 +574,7 @@ func TestQueryWithLimitedRetriesOnMultipleChainsShouldSucceed(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{ + perChainQueries := []*PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -615,7 +609,7 @@ func TestFatalErrorOnPerChainQueryShouldCauseRequestToFail(t *testing.T) { md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{ + perChainQueries := []*PerChainQueryRequest{ createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2), createPerChainQueryForTesting(vaa.ChainIDBSC, "0x28d9123", 3), } @@ -645,7 +639,7 @@ func TestPublishRetrySucceeds(t *testing.T) { md := createQueryHandlerForTestWithoutPublisher(t, ctx, logger, watcherChainsForTest) // Create the request and the expected results. Give the expected results to the mock. - perChainQueries := []*common.PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} + perChainQueries := []*PerChainQueryRequest{createPerChainQueryForTesting(vaa.ChainIDPolygon, "0x28d9630", 2)} signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(md.sk, perChainQueries) expectedResults := createExpectedResultsForTest(queryRequest.PerChainQueries) md.setExpectedResults(expectedResults) diff --git a/node/pkg/common/queryRequest.go b/node/pkg/query/request.go similarity index 98% rename from node/pkg/common/queryRequest.go rename to node/pkg/query/request.go index 9f26415d46..79b25361a3 100644 --- a/node/pkg/common/queryRequest.go +++ b/node/pkg/query/request.go @@ -1,4 +1,4 @@ -package common +package query import ( "bytes" @@ -7,6 +7,7 @@ import ( "math" "strings" + "github.com/certusone/wormhole/node/pkg/common" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/wormhole-foundation/wormhole/sdk/vaa" @@ -74,12 +75,12 @@ type PerChainQueryInternal struct { } // QueryRequestDigest returns the query signing prefix based on the environment. -func QueryRequestDigest(env Environment, b []byte) ethCommon.Hash { +func QueryRequestDigest(env common.Environment, b []byte) ethCommon.Hash { // TODO: should this use a different standard of signing messages, like https://eips.ethereum.org/EIPS/eip-712 var queryRequestPrefix []byte - if env == MainNet { + if env == common.MainNet { queryRequestPrefix = []byte("mainnet_query_request_000000000000|") - } else if env == TestNet { + } else if env == common.TestNet { queryRequestPrefix = []byte("testnet_query_request_000000000000|") } else { queryRequestPrefix = []byte("devnet_query_request_0000000000000|") @@ -94,7 +95,7 @@ func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequ case signedQueryReqSendC <- req: return nil default: - return ErrChanFull + return common.ErrChanFull } } diff --git a/node/pkg/common/queryResponse.go b/node/pkg/query/response.go similarity index 99% rename from node/pkg/common/queryResponse.go rename to node/pkg/query/response.go index f82ff3f371..b644ea9633 100644 --- a/node/pkg/common/queryResponse.go +++ b/node/pkg/query/response.go @@ -1,4 +1,4 @@ -package common +package query import ( "bytes" diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index b7609943dc..f40ad62f66 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -26,6 +26,7 @@ import ( "go.uber.org/zap" "github.com/certusone/wormhole/node/pkg/common" + "github.com/certusone/wormhole/node/pkg/query" "github.com/certusone/wormhole/node/pkg/readiness" "github.com/certusone/wormhole/node/pkg/supervisor" "github.com/wormhole-foundation/wormhole/sdk/vaa" @@ -98,10 +99,10 @@ type ( // Incoming query requests from the network. Pre-filtered to only // include requests for our chainID. - queryReqC <-chan *common.PerChainQueryInternal + queryReqC <-chan *query.PerChainQueryInternal // Outbound query responses to query requests - queryResponseC chan<- *common.PerChainQueryResponseInternal + queryResponseC chan<- *query.PerChainQueryResponseInternal pending map[pendingKey]*pendingMessage pendingMu sync.Mutex @@ -152,8 +153,8 @@ func NewEthWatcher( msgC chan<- *common.MessagePublication, setC chan<- *common.GuardianSet, obsvReqC <-chan *gossipv1.ObservationRequest, - queryReqC <-chan *common.PerChainQueryInternal, - queryResponseC chan<- *common.PerChainQueryResponseInternal, + queryReqC <-chan *query.PerChainQueryInternal, + queryResponseC chan<- *query.PerChainQueryResponseInternal, unsafeDevMode bool, ) *Watcher { @@ -542,7 +543,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { } switch req := queryRequest.Request.Query.(type) { - case *common.EthCallQueryRequest: + case *query.EthCallQueryRequest: block := req.BlockId logger.Info("received query request", zap.String("eth_network", w.networkName), @@ -645,7 +646,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) continue } @@ -656,7 +657,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) continue } @@ -667,7 +668,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) continue } @@ -678,11 +679,11 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) continue } - resp := common.EthCallQueryResponse{ + resp := query.EthCallQueryResponse{ BlockNumber: blockResult.Number.ToInt().Uint64(), Hash: blockResult.Hash, Time: time.Unix(int64(blockResult.Time), 0), @@ -699,7 +700,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) errFound = true break } @@ -714,7 +715,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Any("batch", batch), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryRetryNeeded, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryRetryNeeded, nil) errFound = true break } @@ -736,7 +737,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { } if !errFound { - w.ccqSendQueryResponse(logger, queryRequest, common.QuerySuccess, &resp) + w.ccqSendQueryResponse(logger, queryRequest, query.QuerySuccess, &resp) } default: @@ -744,7 +745,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { zap.Uint8("payload", uint8(queryRequest.Request.Query.Type())), zap.String("component", "ccqevm"), ) - w.ccqSendQueryResponse(logger, queryRequest, common.QueryFatalError, nil) + w.ccqSendQueryResponse(logger, queryRequest, query.QueryFatalError, nil) } } } @@ -1167,8 +1168,8 @@ func (w *Watcher) SetMaxWaitConfirmations(maxWaitConfirmations uint64) { } // ccqSendQueryResponse sends an error response back to the query handler. -func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *common.PerChainQueryInternal, status common.QueryStatus, resp *common.EthCallQueryResponse) { - queryResponse := common.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, resp) +func (w *Watcher) ccqSendQueryResponse(logger *zap.Logger, req *query.PerChainQueryInternal, status query.QueryStatus, resp *query.EthCallQueryResponse) { + queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, resp) select { case w.queryResponseC <- queryResponse: logger.Debug("published query response error to handler", zap.String("component", "ccqevm")) From abecbb78b3e161633adfe577f488cd69f5aea0ed Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:51:30 -0500 Subject: [PATCH 17/37] CCQ: Message tweaks (#3147) * CCQ: Message tweaks Change-Id: I5d97fa0a32b42f1a763edf572b3d08365a7f5b20 * Add comments Change-Id: I35a9fac0fbaae7d175e2529b842c5cef4ac386f6 * Fix comment paste error Change-Id: I4dd23ea375e481d46e24a11d70b590221548c92e * Address review comments Change-Id: Ia516085fdcc182a5566b31342a5aa2480d37eedc --- node/hack/query/send_req.go | 4 +++- node/pkg/query/request.go | 23 +++++++++++++++++++++++ node/pkg/query/response.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 6dd95de0d2..08518005fb 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -348,7 +348,9 @@ func sendQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, var isMatchingResponse bool switch m := msg.Message.(type) { case *gossipv1.GossipMessage_SignedQueryResponse: - logger.Info("query response received", zap.Any("response", m.SignedQueryResponse)) + logger.Info("query response received", zap.Any("response", m.SignedQueryResponse), + zap.String("responseBytes", hexutil.Encode(m.SignedQueryResponse.QueryResponse)), + zap.String("sigBytes", hexutil.Encode(m.SignedQueryResponse.Signature))) var response query.QueryResponsePublication err := response.Unmarshal(m.SignedQueryResponse.QueryResponse) if err != nil { diff --git a/node/pkg/query/request.go b/node/pkg/query/request.go index 79b25361a3..b2c6f33279 100644 --- a/node/pkg/query/request.go +++ b/node/pkg/query/request.go @@ -112,6 +112,7 @@ func (queryRequest *QueryRequest) Marshal() ([]byte, error) { buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, uint8(1)) // version vaa.MustWrite(buf, binary.BigEndian, queryRequest.Nonce) // uint32 vaa.MustWrite(buf, binary.BigEndian, uint8(len(queryRequest.PerChainQueries))) @@ -134,6 +135,15 @@ func (queryRequest *QueryRequest) Unmarshal(data []byte) error { // UnmarshalFromReader deserializes the binary representation of a query request from an existing reader func (queryRequest *QueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { + var version uint8 + if err := binary.Read(reader, binary.BigEndian, &version); err != nil { + return fmt.Errorf("failed to read message version: %w", err) + } + + if version != 1 { + return fmt.Errorf("unsupported message version: %d", version) + } + if err := binary.Read(reader, binary.BigEndian, &queryRequest.Nonce); err != nil { return fmt.Errorf("failed to read request nonce: %w", err) } @@ -207,6 +217,13 @@ func (perChainQuery *PerChainQueryRequest) Marshal() ([]byte, error) { if err != nil { return nil, err } + + // Write the length of the query to facilitate on-chain parsing. + if len(queryBuf) > math.MaxUint32 { + return nil, fmt.Errorf("query too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(queryBuf))) + buf.Write(queryBuf) return buf.Bytes(), nil } @@ -233,6 +250,12 @@ func (perChainQuery *PerChainQueryRequest) UnmarshalFromReader(reader *bytes.Rea return err } + // Skip the query length. + var queryLength uint32 + if err := binary.Read(reader, binary.BigEndian, &queryLength); err != nil { + return fmt.Errorf("failed to read query length: %w", err) + } + switch queryType { case EthCallQueryRequestType: q := EthCallQueryRequest{} diff --git a/node/pkg/query/response.go b/node/pkg/query/response.go index b644ea9633..326195b981 100644 --- a/node/pkg/query/response.go +++ b/node/pkg/query/response.go @@ -97,12 +97,21 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, uint8(1)) // version + // Source // TODO: support writing off-chain and on-chain requests // Here, unset represents an off-chain request vaa.MustWrite(buf, binary.BigEndian, vaa.ChainIDUnset) buf.Write(msg.Request.Signature[:]) + + // Write the length of the request to facilitate on-chain parsing. + if len(msg.Request.QueryRequest) > math.MaxUint32 { + return nil, fmt.Errorf("request too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(msg.Request.QueryRequest))) + buf.Write(msg.Request.QueryRequest) // Per chain responses @@ -122,6 +131,15 @@ func (msg *QueryResponsePublication) Marshal() ([]byte, error) { func (msg *QueryResponsePublication) Unmarshal(data []byte) error { reader := bytes.NewReader(data[:]) + var version uint8 + if err := binary.Read(reader, binary.BigEndian, &version); err != nil { + return fmt.Errorf("failed to read message version: %w", err) + } + + if version != 1 { + return fmt.Errorf("unsupported message version: %d", version) + } + // Request requestChain := vaa.ChainID(0) if err := binary.Read(reader, binary.BigEndian, &requestChain); err != nil { @@ -139,6 +157,12 @@ func (msg *QueryResponsePublication) Unmarshal(data []byte) error { } signedQueryRequest.Signature = signature[:] + // Skip the query length. + queryRequestLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &queryRequestLen); err != nil { + return fmt.Errorf("failed to read length of query request: %w", err) + } + queryRequest := QueryRequest{} err := queryRequest.UnmarshalFromReader(reader) if err != nil { @@ -260,6 +284,12 @@ func (perChainResponse *PerChainQueryResponse) Marshal() ([]byte, error) { if err != nil { return nil, err } + + // Write the length of the response to facilitate on-chain parsing. + if len(respBuf) > math.MaxUint32 { + return nil, fmt.Errorf("response is too long") + } + vaa.MustWrite(buf, binary.BigEndian, uint32(len(respBuf))) buf.Write(respBuf) return buf.Bytes(), nil } @@ -286,6 +316,12 @@ func (perChainResponse *PerChainQueryResponse) UnmarshalFromReader(reader *bytes return err } + // Skip the response length. + var respLength uint32 + if err := binary.Read(reader, binary.BigEndian, &respLength); err != nil { + return fmt.Errorf("failed to read response length: %w", err) + } + switch queryType { case EthCallQueryRequestType: r := EthCallQueryResponse{} From e1fd3b4bfd00c1f532a53eb56d95dac4547f253f Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 30 Jun 2023 10:12:50 -0500 Subject: [PATCH 18/37] CCQ: Make work with node refactor (#3154) * CCQ: Make work with node refactor Change-Id: I416db0e01156787b6ed2420598a448d23c4581ca * Tweak log message Change-Id: Ibf109cfdccf01297e0755fc849b23d42cde2b772 --- Tiltfile | 2 +- node/cmd/guardiand/node.go | 3 +- node/pkg/node/node.go | 21 +++++++ node/pkg/node/options.go | 58 +++++++++++++++++- node/pkg/query/query.go | 89 ++++++++++++++++++++++------ node/pkg/query/query_test.go | 19 +++--- node/pkg/query/request.go | 1 - node/pkg/watchers/algorand/config.go | 3 + node/pkg/watchers/aptos/config.go | 3 + node/pkg/watchers/cosmwasm/config.go | 3 + node/pkg/watchers/evm/config.go | 5 +- node/pkg/watchers/mock/config.go | 3 + node/pkg/watchers/near/config.go | 3 + node/pkg/watchers/solana/config.go | 3 + node/pkg/watchers/sui/config.go | 3 + node/pkg/watchers/watchers.go | 3 + 16 files changed, 192 insertions(+), 30 deletions(-) diff --git a/Tiltfile b/Tiltfile index 159499c5f7..1662011536 100644 --- a/Tiltfile +++ b/Tiltfile @@ -302,7 +302,7 @@ def build_node_yaml(): "--gatewayLCD", "http://wormchain:1317" ] - + return encode_yaml_stream(node_yaml_with_replicas) k8s_yaml_with_ns(build_node_yaml()) diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index 6c811bd1fe..d610f58c78 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -479,7 +479,7 @@ func runNode(cmd *cobra.Command, args []string) { // Deterministic ganache ETH devnet address. *ethContract = unsafeDevModeEvmContractAddress(*ethContract) *bscContract = unsafeDevModeEvmContractAddress(*bscContract) - // *polygonContract = unsafeDevModeEvmContractAddress(*polygonContract) + *polygonContract = unsafeDevModeEvmContractAddress(*polygonContract) *avalancheContract = unsafeDevModeEvmContractAddress(*avalancheContract) *oasisContract = unsafeDevModeEvmContractAddress(*oasisContract) *auroraContract = unsafeDevModeEvmContractAddress(*auroraContract) @@ -1428,6 +1428,7 @@ func runNode(cmd *cobra.Command, args []string) { node.GuardianOptionAccountant(*accountantContract, *accountantWS, *accountantCheckEnabled, accountantWormchainConn), node.GuardianOptionGovernor(*chainGovernorEnabled), node.GuardianOptionGatewayRelayer(*gatewayRelayerContract, gatewayRelayerWormchainConn), + node.GuardianOptionQueryHandler(*ccqEnabled, *ccqAllowedRequesters), node.GuardianOptionAdminService(*adminSocketPath, ethRPC, ethContract, rpcMap), node.GuardianOptionP2P(p2pKey, *p2pNetworkID, *p2pBootstrap, *nodeName, *disableHeartbeatVerify, *p2pPort, ibc.GetFeatures), node.GuardianOptionStatusServer(*statusAddr), diff --git a/node/pkg/node/node.go b/node/pkg/node/node.go index 206fa143df..dd088beda9 100644 --- a/node/pkg/node/node.go +++ b/node/pkg/node/node.go @@ -11,7 +11,9 @@ import ( "github.com/certusone/wormhole/node/pkg/governor" "github.com/certusone/wormhole/node/pkg/gwrelayer" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/certusone/wormhole/node/pkg/query" "github.com/certusone/wormhole/node/pkg/supervisor" + "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" "google.golang.org/grpc" @@ -59,6 +61,7 @@ type G struct { acct *accountant.Accountant gov *governor.ChainGovernor gatewayRelayer *gwrelayer.GatewayRelayer + queryHandler *query.QueryHandler publicrpcServer *grpc.Server // runnables @@ -82,6 +85,12 @@ type G struct { obsvReqSendC channelPair[*gossipv1.ObservationRequest] // acctC is the channel where messages will be put after they reached quorum in the accountant. acctC channelPair[*common.MessagePublication] + + // Cross Chain Query Handler channels + chainQueryReqC map[vaa.ChainID]chan *query.PerChainQueryInternal + signedQueryReqC channelPair[*gossipv1.SignedQueryRequest] + queryResponseC channelPair[*query.PerChainQueryResponseInternal] + queryResponsePublicationC channelPair[*query.QueryResponsePublication] } func NewGuardianNode( @@ -108,6 +117,11 @@ func (g *G) initializeBasic(rootCtxCancel context.CancelFunc) { g.obsvReqC = makeChannelPair[*gossipv1.ObservationRequest](observationRequestInboundBufferSize) g.obsvReqSendC = makeChannelPair[*gossipv1.ObservationRequest](observationRequestOutboundBufferSize) g.acctC = makeChannelPair[*common.MessagePublication](accountant.MsgChannelCapacity) + // Cross Chain Query Handler channels + g.chainQueryReqC = make(map[vaa.ChainID]chan *query.PerChainQueryInternal) + g.signedQueryReqC = makeChannelPair[*gossipv1.SignedQueryRequest](query.SignedQueryRequestChannelSize) + g.queryResponseC = makeChannelPair[*query.PerChainQueryResponseInternal](0) + g.queryResponsePublicationC = makeChannelPair[*query.QueryResponsePublication](0) // Guardian set state managed by processor g.gst = common.NewGuardianSetState(nil) @@ -191,6 +205,13 @@ func (g *G) Run(rootCtxCancel context.CancelFunc, options ...*GuardianOption) su } } + if g.queryHandler != nil { + logger.Info("Starting query handler", zap.String("component", "ccq")) + if err := g.queryHandler.Start(ctx); err != nil { + logger.Fatal("failed to create chain governor", zap.Error(err), zap.String("component", "ccq")) + } + } + // Start any other runnables for name, runnable := range g.runnables { if err := supervisor.Run(ctx, name, runnable); err != nil { diff --git a/node/pkg/node/options.go b/node/pkg/node/options.go index e2b530cba7..8302a78889 100644 --- a/node/pkg/node/options.go +++ b/node/pkg/node/options.go @@ -16,6 +16,7 @@ import ( "github.com/certusone/wormhole/node/pkg/p2p" "github.com/certusone/wormhole/node/pkg/processor" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/certusone/wormhole/node/pkg/query" "github.com/certusone/wormhole/node/pkg/readiness" "github.com/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" @@ -72,6 +73,34 @@ func GuardianOptionP2P(p2pKey libp2p_crypto.PrivKey, networkId string, bootstrap components, ibcFeaturesFunc, (g.gatewayRelayer != nil), + (g.queryHandler != nil), + g.signedQueryReqC.writeC, + g.queryResponsePublicationC.readC, + ) + + return nil + }} +} + +// GuardianOptionQueryHandler configures the Cross Chain Query module. +func GuardianOptionQueryHandler(ccqEnabled bool, allowedRequesters string) *GuardianOption { + return &GuardianOption{ + name: "query", + f: func(ctx context.Context, logger *zap.Logger, g *G) error { + if !ccqEnabled { + logger.Info("ccq: cross chain query is disabled", zap.String("component", "ccq")) + return nil + } + + g.queryHandler = query.NewQueryHandler( + logger, + g.env, + allowedRequesters, + g.signedQueryReqC.readC, + g.chainQueryReqC, + g.queryResponseC.readC, + g.queryResponsePublicationC.writeC, + (g.gatewayRelayer != nil), ) return nil @@ -301,6 +330,32 @@ func GuardianOptionWatchers(watcherConfigs []watchers.WatcherConfig, ibcWatcherC }(chainMsgC[chainId], chainId) } + // Per-chain query response channel + chainQueryResponseC := make(map[vaa.ChainID]chan *query.PerChainQueryResponseInternal) + // aggregate per-chain msgC into msgC. + // SECURITY defense-in-depth: This way we enforce that a watcher must set the msg.EmitterChain to its chainId, which makes the code easier to audit + for _, chainId := range vaa.GetAllNetworkIDs() { + chainQueryResponseC[chainId] = make(chan *query.PerChainQueryResponseInternal) + go func(c <-chan *query.PerChainQueryResponseInternal, chainId vaa.ChainID) { + for { + select { + case <-ctx.Done(): + return + case response := <-c: + if response.ChainId != chainId { + // SECURITY: This should never happen. If it does, a watcher has been compromised. + logger.Fatal("SECURITY CRITICAL: Received query response from a chain that was not marked as originating from that chain", + zap.Uint16("responseChainId", uint16(response.ChainId)), + zap.Stringer("watcherChainId", chainId), + ) + } else { + g.queryResponseC.writeC <- response + } + } + } + }(chainQueryResponseC[chainId], chainId) + } + watchers := make(map[watchers.NetworkID]interfaces.L1Finalizer) for _, wc := range watcherConfigs { @@ -316,6 +371,7 @@ func GuardianOptionWatchers(watcherConfigs []watchers.WatcherConfig, ibcWatcherC } chainObsvReqC[wc.GetChainID()] = make(chan *gossipv1.ObservationRequest, observationRequestPerChainBufferSize) + g.chainQueryReqC[wc.GetChainID()] = make(chan *query.PerChainQueryInternal, query.QueryRequestBufferSize) if wc.RequiredL1Finalizer() != "" { l1watcher, ok := watchers[wc.RequiredL1Finalizer()] @@ -327,7 +383,7 @@ func GuardianOptionWatchers(watcherConfigs []watchers.WatcherConfig, ibcWatcherC wc.SetL1Finalizer(l1watcher) } - l1finalizer, runnable, err := wc.Create(chainMsgC[wc.GetChainID()], chainObsvReqC[wc.GetChainID()], g.setC.writeC, g.env) + l1finalizer, runnable, err := wc.Create(chainMsgC[wc.GetChainID()], chainObsvReqC[wc.GetChainID()], g.chainQueryReqC[wc.GetChainID()], chainQueryResponseC[wc.GetChainID()], g.setC.writeC, g.env) if err != nil { return fmt.Errorf("error creating watcher: %w", err) diff --git a/node/pkg/query/query.go b/node/pkg/query/query.go index 49f222ab14..94d894510f 100644 --- a/node/pkg/query/query.go +++ b/node/pkg/query/query.go @@ -9,6 +9,7 @@ import ( "github.com/certusone/wormhole/node/pkg/common" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/certusone/wormhole/node/pkg/supervisor" "github.com/wormhole-foundation/wormhole/sdk/vaa" ethCommon "github.com/ethereum/go-ethereum/common" @@ -23,9 +24,47 @@ const ( // RetryInterval specifies how long we will wait between retry intervals. This is the interval of our ticker. RetryInterval = 10 * time.Second + + // SignedQueryRequestChannelSize is the buffer size of the incoming query request channel. + SignedQueryRequestChannelSize = 50 + + // QueryRequestBufferSize is the buffer size of the per-network query request channel. + QueryRequestBufferSize = 25 ) +func NewQueryHandler( + logger *zap.Logger, + env common.Environment, + allowedRequestorsStr string, + signedQueryReqC <-chan *gossipv1.SignedQueryRequest, + chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal, + queryResponseReadC <-chan *PerChainQueryResponseInternal, + queryResponseWriteC chan<- *QueryResponsePublication, +) *QueryHandler { + return &QueryHandler{ + logger: logger.With(zap.String("component", "ccq")), + env: env, + allowedRequestorsStr: allowedRequestorsStr, + signedQueryReqC: signedQueryReqC, + chainQueryReqC: chainQueryReqC, + queryResponseReadC: queryResponseReadC, + queryResponseWriteC: queryResponseWriteC, + } +} + type ( + // QueryHandler defines the cross chain query handler. + QueryHandler struct { + logger *zap.Logger + env common.Environment + allowedRequestorsStr string + signedQueryReqC <-chan *gossipv1.SignedQueryRequest + chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal + queryResponseReadC <-chan *PerChainQueryResponseInternal + queryResponseWriteC chan<- *QueryResponsePublication + allowedRequestors map[ethCommon.Address]struct{} + } + // pendingQuery is the cache entry for a given query. pendingQuery struct { signedRequest *gossipv1.SignedQueryRequest @@ -47,18 +86,26 @@ type ( } ) -// HandleQueryRequests multiplexes observation requests to the appropriate chain -func HandleQueryRequests( - ctx context.Context, - logger *zap.Logger, - signedQueryReqC <-chan *gossipv1.SignedQueryRequest, - chainQueryReqC map[vaa.ChainID]chan *PerChainQueryInternal, - allowedRequestors map[ethCommon.Address]struct{}, - queryResponseReadC <-chan *PerChainQueryResponseInternal, - queryResponseWriteC chan<- *QueryResponsePublication, - env common.Environment, -) { - handleQueryRequestsImpl(ctx, logger, signedQueryReqC, chainQueryReqC, allowedRequestors, queryResponseReadC, queryResponseWriteC, env, RequestTimeout, RetryInterval) +// Start initializes the query handler and starts the runnable. +func (qh *QueryHandler) Start(ctx context.Context) error { + qh.logger.Debug("entering Start", zap.String("enforceFlag", qh.allowedRequestorsStr)) + + var err error + qh.allowedRequestors, err = parseAllowedRequesters(qh.allowedRequestorsStr) + if err != nil { + return fmt.Errorf("failed to parse allowed requesters: %w", err) + } + + if err := supervisor.Run(ctx, "query_handler", common.WrapWithScissors(qh.handleQueryRequests, "query_handler")); err != nil { + return fmt.Errorf("failed to start query handler routine: %w", err) + } + + return nil +} + +// handleQueryRequests multiplexes observation requests to the appropriate chain +func (qh *QueryHandler) handleQueryRequests(ctx context.Context) error { + return handleQueryRequestsImpl(ctx, qh.logger, qh.signedQueryReqC, qh.chainQueryReqC, qh.allowedRequestors, qh.queryResponseReadC, qh.queryResponseWriteC, qh.env, RequestTimeout, RetryInterval) } // handleQueryRequestsImpl allows instantiating the handler in the test environment with shorter timeout and retry parameters. @@ -73,7 +120,7 @@ func handleQueryRequestsImpl( env common.Environment, requestTimeoutImpl time.Duration, retryIntervalImpl time.Duration, -) { +) error { qLogger := logger.With(zap.String("component", "ccqhandler")) qLogger.Info("cross chain queries are enabled", zap.Any("allowedRequestors", allowedRequestors), zap.String("env", string(env))) @@ -106,7 +153,7 @@ func handleQueryRequestsImpl( for { select { case <-ctx.Done(): - return + return nil case signedRequest := <-signedQueryReqC: // Inbound query request. // requestor validation happens here @@ -121,6 +168,8 @@ func handleQueryRequestsImpl( requestID := hex.EncodeToString(signedRequest.Signature) digest := QueryRequestDigest(env, signedRequest.QueryRequest) + qLogger.Info("received a query request", zap.String("requestID", requestID)) + signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), signedRequest.Signature) if err != nil { qLogger.Error("failed to recover public key", zap.String("requestID", requestID)) @@ -296,7 +345,13 @@ func handleQueryRequestsImpl( } else { for requestIdx, pcq := range pq.queries { if pq.responses[requestIdx] == nil && pcq.lastUpdateTime.Add(retryIntervalImpl).Before(now) { - qLogger.Info("retrying query request", zap.String("requestId", reqId), zap.Int("requestIdx", requestIdx), zap.Stringer("receiveTime", pq.receiveTime), zap.Stringer("lastUpdateTime", pcq.lastUpdateTime)) + qLogger.Info("retrying query request", + zap.String("requestId", reqId), + zap.Int("requestIdx", requestIdx), + zap.Stringer("receiveTime", pq.receiveTime), + zap.Stringer("lastUpdateTime", pcq.lastUpdateTime), + zap.String("chainID", pq.queries[requestIdx].req.Request.ChainId.String()), + ) pcq.ccqForwardToWatcher(qLogger, pq.receiveTime) } } @@ -307,8 +362,8 @@ func handleQueryRequestsImpl( } } -// ParseAllowedRequesters parses a comma separated list of allowed requesters into a map to be used for look ups. -func ParseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { +// parseAllowedRequesters parses a comma separated list of allowed requesters into a map to be used for look ups. +func parseAllowedRequesters(ccqAllowedRequesters string) (map[ethCommon.Address]struct{}, error) { if ccqAllowedRequesters == "" { return nil, fmt.Errorf("if cross chain query is enabled `--ccqAllowedRequesters` must be specified") } diff --git a/node/pkg/query/query_test.go b/node/pkg/query/query_test.go index d55e1c3c84..2fed9c51f4 100644 --- a/node/pkg/query/query_test.go +++ b/node/pkg/query/query_test.go @@ -154,7 +154,7 @@ func validateResponseForTest( } func TestParseAllowedRequestersSuccess(t *testing.T) { - ccqAllowedRequestersList, err := ParseAllowedRequesters(testSigner) + ccqAllowedRequestersList, err := parseAllowedRequesters(testSigner) require.NoError(t, err) require.NotNil(t, ccqAllowedRequestersList) require.Equal(t, 1, len(ccqAllowedRequestersList)) @@ -164,7 +164,7 @@ func TestParseAllowedRequestersSuccess(t *testing.T) { _, exists = ccqAllowedRequestersList[ethCommon.BytesToAddress(ethCommon.Hex2Bytes("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf"))] require.False(t, exists) - ccqAllowedRequestersList, err = ParseAllowedRequesters("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf") + ccqAllowedRequestersList, err = parseAllowedRequesters("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBf") require.NoError(t, err) require.NotNil(t, ccqAllowedRequestersList) require.Equal(t, 2, len(ccqAllowedRequestersList)) @@ -176,17 +176,17 @@ func TestParseAllowedRequestersSuccess(t *testing.T) { } func TestParseAllowedRequestersFailsIfParameterEmpty(t *testing.T) { - ccqAllowedRequestersList, err := ParseAllowedRequesters("") + ccqAllowedRequestersList, err := parseAllowedRequesters("") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) - ccqAllowedRequestersList, err = ParseAllowedRequesters(",") + ccqAllowedRequestersList, err = parseAllowedRequesters(",") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) } func TestParseAllowedRequestersFailsIfInvalidParameter(t *testing.T) { - ccqAllowedRequestersList, err := ParseAllowedRequesters("Hello") + ccqAllowedRequestersList, err := parseAllowedRequesters("Hello") require.Error(t, err) require.Nil(t, ccqAllowedRequestersList) } @@ -310,7 +310,7 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context require.NoError(t, err) require.NotNil(t, md.sk) - ccqAllowedRequestersList, err := ParseAllowedRequesters(testSigner) + ccqAllowedRequestersList, err := parseAllowedRequesters(testSigner) require.NoError(t, err) // Inbound observation requests from the p2p service (for all chains) @@ -330,8 +330,11 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context md.resetState() - go handleQueryRequestsImpl(ctx, logger, md.signedQueryReqReadC, md.chainQueryReqC, ccqAllowedRequestersList, - md.queryResponseReadC, md.queryResponsePublicationWriteC, common.GoTest, requestTimeoutForTest, retryIntervalForTest) + go func() { + err := handleQueryRequestsImpl(ctx, logger, md.signedQueryReqReadC, md.chainQueryReqC, ccqAllowedRequestersList, + md.queryResponseReadC, md.queryResponsePublicationWriteC, common.GoTest, requestTimeoutForTest, retryIntervalForTest) + assert.NoError(t, err) + }() // Create a routine for each configured watcher. It will take a per chain query and return the corresponding expected result. // It also pegs a counter of the number of requests the watcher received, for verification purposes. diff --git a/node/pkg/query/request.go b/node/pkg/query/request.go index b2c6f33279..38ac24cbe9 100644 --- a/node/pkg/query/request.go +++ b/node/pkg/query/request.go @@ -64,7 +64,6 @@ type EthCallData struct { Data []byte } -const SignedQueryRequestChannelSize = 50 const EvmContractAddressLength = 20 // PerChainQueryInternal is an internal representation of a query request that is passed to the watcher. diff --git a/node/pkg/watchers/algorand/config.go b/node/pkg/watchers/algorand/config.go index fff2a818b9..aa1f3355a9 100644 --- a/node/pkg/watchers/algorand/config.go +++ b/node/pkg/watchers/algorand/config.go @@ -3,6 +3,7 @@ package algorand import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -38,6 +39,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/aptos/config.go b/node/pkg/watchers/aptos/config.go index b01a51a664..632b8a46e3 100644 --- a/node/pkg/watchers/aptos/config.go +++ b/node/pkg/watchers/aptos/config.go @@ -3,6 +3,7 @@ package aptos import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -36,6 +37,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/cosmwasm/config.go b/node/pkg/watchers/cosmwasm/config.go index 4a5cb40929..b9837730dc 100644 --- a/node/pkg/watchers/cosmwasm/config.go +++ b/node/pkg/watchers/cosmwasm/config.go @@ -3,6 +3,7 @@ package cosmwasm import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -36,6 +37,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/evm/config.go b/node/pkg/watchers/evm/config.go index 3e777e87a1..3891f1d6f3 100644 --- a/node/pkg/watchers/evm/config.go +++ b/node/pkg/watchers/evm/config.go @@ -3,6 +3,7 @@ package evm import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -42,6 +43,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + queryReqC <-chan *query.PerChainQueryInternal, + queryResponseC chan<- *query.PerChainQueryResponseInternal, setC chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { @@ -54,7 +57,7 @@ func (wc *WatcherConfig) Create( var devMode bool = (env == common.UnsafeDevNet) - watcher := NewEthWatcher(wc.Rpc, eth_common.HexToAddress(wc.Contract), string(wc.NetworkID), wc.ChainID, msgC, setWriteC, obsvReqC, devMode) + watcher := NewEthWatcher(wc.Rpc, eth_common.HexToAddress(wc.Contract), string(wc.NetworkID), wc.ChainID, msgC, setWriteC, obsvReqC, queryReqC, queryResponseC, devMode) watcher.SetWaitForConfirmations(wc.WaitForConfirmations) if err := watcher.SetRootChainParams(wc.RootChainRpc, wc.RootChainContract); err != nil { return nil, nil, err diff --git a/node/pkg/watchers/mock/config.go b/node/pkg/watchers/mock/config.go index 8d4a34897b..cc352f5686 100644 --- a/node/pkg/watchers/mock/config.go +++ b/node/pkg/watchers/mock/config.go @@ -3,6 +3,7 @@ package mock import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -42,6 +43,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, setC chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/near/config.go b/node/pkg/watchers/near/config.go index 879016294c..ce5d36a138 100644 --- a/node/pkg/watchers/near/config.go +++ b/node/pkg/watchers/near/config.go @@ -3,6 +3,7 @@ package near import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -35,6 +36,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/solana/config.go b/node/pkg/watchers/solana/config.go index c506a6a329..a2263874a4 100644 --- a/node/pkg/watchers/solana/config.go +++ b/node/pkg/watchers/solana/config.go @@ -3,6 +3,7 @@ package solana import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -40,6 +41,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/sui/config.go b/node/pkg/watchers/sui/config.go index fea2bf4108..eb99c6d84f 100644 --- a/node/pkg/watchers/sui/config.go +++ b/node/pkg/watchers/sui/config.go @@ -3,6 +3,7 @@ package sui import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" @@ -36,6 +37,8 @@ func (wc *WatcherConfig) SetL1Finalizer(l1finalizer interfaces.L1Finalizer) { func (wc *WatcherConfig) Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + _ <-chan *query.PerChainQueryInternal, + _ chan<- *query.PerChainQueryResponseInternal, _ chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) { diff --git a/node/pkg/watchers/watchers.go b/node/pkg/watchers/watchers.go index ac60038c59..047e228a9c 100644 --- a/node/pkg/watchers/watchers.go +++ b/node/pkg/watchers/watchers.go @@ -3,6 +3,7 @@ package watchers import ( "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/certusone/wormhole/node/pkg/supervisor" "github.com/certusone/wormhole/node/pkg/watchers/interfaces" "github.com/wormhole-foundation/wormhole/sdk/vaa" @@ -20,6 +21,8 @@ type WatcherConfig interface { Create( msgC chan<- *common.MessagePublication, obsvReqC <-chan *gossipv1.ObservationRequest, + queryReqC <-chan *query.PerChainQueryInternal, + queryResponseC chan<- *query.PerChainQueryResponseInternal, setC chan<- *common.GuardianSet, env common.Environment, ) (interfaces.L1Finalizer, supervisor.Runnable, error) From 064ffa0a20c2ad6dccc36f306b360dd2b5c385a2 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 3 Aug 2023 18:57:18 -0400 Subject: [PATCH 19/37] CCQ: revert polygonRPC --- devnet/node.yaml | 4 +--- node/pkg/node/options.go | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/devnet/node.yaml b/devnet/node.yaml index 2c47d84775..a2f797c8c3 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -73,9 +73,7 @@ spec: # - --bscRPC # - ws://eth-devnet2:8545 - --polygonRPC - - wss://ws-matic-mainnet.chainstacklabs.com - - --polygonContract - - "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7" + - ws://eth-devnet:8545 - --avalancheRPC - ws://eth-devnet:8545 - --auroraRPC diff --git a/node/pkg/node/options.go b/node/pkg/node/options.go index 8302a78889..db6c8c0d2d 100644 --- a/node/pkg/node/options.go +++ b/node/pkg/node/options.go @@ -100,7 +100,6 @@ func GuardianOptionQueryHandler(ccqEnabled bool, allowedRequesters string) *Guar g.chainQueryReqC, g.queryResponseC.readC, g.queryResponsePublicationC.writeC, - (g.gatewayRelayer != nil), ) return nil From 7f2ad4a3973b6d5e35ad54f29219c367cde1ac47 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 1 Jun 2023 18:06:17 +0000 Subject: [PATCH 20/37] CCQ: evm example --- ethereum/contracts/query/QueryResponse.sol | 153 +++++++++++++++++++++ ethereum/scripts/test_query.js | 58 ++++++++ ethereum/truffle-config.js | 2 +- 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 ethereum/contracts/query/QueryResponse.sol create mode 100644 ethereum/scripts/test_query.js diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol new file mode 100644 index 0000000000..e41552d07b --- /dev/null +++ b/ethereum/contracts/query/QueryResponse.sol @@ -0,0 +1,153 @@ +// contracts/query/QueryResponse.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../libraries/external/BytesLib.sol"; +import "../interfaces/IWormhole.sol"; + +// TODO: move functions to library +contract QueryResponse { + using BytesLib for bytes; + struct EthCallResponse { + // Sender + uint16 senderChainId; + bytes requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain + // Request + uint8 requestType; + uint16 requestChainId; + uint32 requestNonce; + address requestTo; + bytes requestData; + bytes requestBlock; + // Response + uint64 blockNumber; + bytes32 blockHash; + uint32 blockTime; + bytes result; + } + + bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); + IWormhole public immutable wormhole; + + constructor (address _wormhole) { + wormhole = IWormhole(_wormhole); + } + + function getResponseHash(bytes memory response) public pure returns (bytes32) { + return keccak256(response); + } + + function getResponseDigest(bytes memory response) public pure returns (bytes32) { + return keccak256(abi.encodePacked(responsePrefix,getResponseHash(response))); + } + + /** + * @dev verifyQueryResponse serves to + * IWormhole.Signature expects the last byte to be bumped by 27 + * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174 + */ + function verifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view { + // TODO: make a verifyCurrentQuorum call on the core bridge so that there is only 1 cross call instead of 4 + uint32 gsi = wormhole.getCurrentGuardianSetIndex(); + IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(gsi); + + bytes32 responseHash = getResponseDigest(response); + + /** + * @dev Checks whether the guardianSet has zero keys + * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure + * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet + * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and + * signature verification. + */ + if(guardianSet.keys.length == 0){ + revert("invalid guardian set"); + } + + /** + * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding. + * WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM + * if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and + * vm.signatures length is 0, this could compromise the integrity of both vm and signature verification. + */ + if (signatures.length < wormhole.quorum(guardianSet.keys.length)){ + revert("no quorum"); + } + + /// @dev Verify the proposed vm.signatures against the guardianSet + (bool signaturesValid, string memory invalidReason) = wormhole.verifySignatures(responseHash, signatures, guardianSet); + if(!signaturesValid){ + revert(invalidReason); + } + + /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet. + } + + function parseEthCallResponse(bytes memory response) internal pure returns (EthCallResponse memory r) { + uint index = 0; + + r.senderChainId = response.toUint16(index); + index += 2; + + if (r.senderChainId == 0) { + r.requestId = response.slice(index, 65); + index += 65; + } else { + r.requestId = response.slice(index, 32); + index += 32; + } + + r.requestType = response.toUint8(index); + index += 1; + + require(r.requestType == 1, "invalid request type"); + + r.requestChainId = response.toUint16(index); + index += 2; + + r.requestNonce = response.toUint32(index); + index += 4; + + r.requestTo = response.toAddress(index); + index += 20; + + uint32 len = response.toUint32(index); + index += 4; + r.requestData = response.slice(index, len); + index += len; + + len = response.toUint32(index); + index += 4; + r.requestBlock = response.slice(index, len); + index += len; + + r.blockNumber = response.toUint64(index); + index += 8; + + r.blockHash = response.toBytes32(index); + index += 32; + + r.blockTime = response.toUint32(index); + index += 4; + + len = response.toUint32(index); + index += 4; + r.result = response.slice(index, len); + index += len; + + require(response.length == index, "invalid response"); + } + + function processStringResult(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (string memory result) { + verifyQueryResponse(response, signatures); + EthCallResponse memory parsed = parseEthCallResponse(response); + // Polygon + require(parsed.requestChainId == 5, "invalid request chain"); + // WMATIC + require(parsed.requestTo == 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270, "invalid request to"); + // Name + require(parsed.requestData.equal(abi.encodeWithSignature("name()")), "invalid request data"); + (result) = abi.decode(parsed.result, (string)); + } +} diff --git a/ethereum/scripts/test_query.js b/ethereum/scripts/test_query.js new file mode 100644 index 0000000000..c478f662bb --- /dev/null +++ b/ethereum/scripts/test_query.js @@ -0,0 +1,58 @@ +// run this script with truffle exec + +const jsonfile = require("jsonfile"); +const QueryResponseABI = jsonfile.readFileSync( + "../build/contracts/QueryResponse.json" +).abi; +const responseBytes = + "0x00004d2fded93c872040330a7d4a60cb4431d6c929c720437ed345daaff928f786f45fa31c825bad2714a69ca3f7d1324f8f9d51dc452fdfacff65ff4c6ad7e7390301010005000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde03000000066c6174657374000000000295f40396c790fb8cb9407de03f61daa46ef15a3c20d301e09af14c850185294c07580c6477b4cf000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000"; +const sigs = [ + [ + "0xb8320f25a42b2b4a832e18e5b68dc12f269d7d8a8d9d43c76107d74d4d7202eb", + "0x6d8258da2a60de95ebce1283ba6ec124a2301a864e35979ebb48af5981e0fae8", + "0x1b", // zero add 27 + "0x00", + ], +]; +const expectedHash = + "0x3c4628ca459c0ee5344d91146776f46627bdbf189f4f045d9dedf480861c05f3"; +const expectedDigest = + "0x616674308c1ab1b468665f21fd3808a8fc5807a4ca9859b681d2e3f7ace97cc2"; + +module.exports = async function(callback) { + try { + const QueryResponse = await artifacts.require("QueryResponse"); + //Query deploy + const queryAddress = ( + await QueryResponse.new("0xC89Ce4735882C9F0f0FE26686c53074E09B0D550") + ).address; + + console.log("QueryResponse deployed at: " + queryAddress); + + const initialized = new web3.eth.Contract(QueryResponseABI, queryAddress); + + const hashResult = await initialized.methods + .getResponseHash(responseBytes) + .call(); + console.log(hashResult); + + const digestResult = await initialized.methods + .getResponseDigest(responseBytes) + .call(); + console.log(digestResult); + + const verify = await initialized.methods + .verifyQueryResponse(responseBytes, sigs) + .call(); + console.log(verify); + + const result = await initialized.methods + .processStringResult(responseBytes, sigs) + .call(); + console.log(result); + + callback(); + } catch (e) { + callback(e); + } +}; diff --git a/ethereum/truffle-config.js b/ethereum/truffle-config.js index 0534e9096a..8032896fcd 100644 --- a/ethereum/truffle-config.js +++ b/ethereum/truffle-config.js @@ -4,7 +4,7 @@ const KLAYHDWalletProvider = require("truffle-hdwallet-provider-klaytn"); module.exports = { contracts_directory: - "contracts/{*.sol,bridge/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol},interfaces/IWormhole.sol,mock/*.sol,nft/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol}}", + "contracts/{*.sol,bridge/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol},interfaces/IWormhole.sol,mock/*.sol,nft/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol},query/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol}}", networks: { development: { host: "127.0.0.1", From 46c81271e42bd0e4ed82b3f8fb0f15791022798c Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Wed, 28 Jun 2023 18:36:24 +0000 Subject: [PATCH 21/37] Rework to support batches --- ethereum/contracts/query/QueryResponse.sol | 232 +++++++++++++-------- ethereum/scripts/test_query.js | 44 +++- 2 files changed, 183 insertions(+), 93 deletions(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index e41552d07b..4df1c339d2 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -9,24 +9,40 @@ import "../interfaces/IWormhole.sol"; // TODO: move functions to library contract QueryResponse { using BytesLib for bytes; - struct EthCallResponse { - // Sender + + /// @dev ParsedQueryResponse is returned by parseAndVerifyQueryResponse(). + struct ParsedQueryResponse { + uint8 version; uint16 senderChainId; bytes requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain - // Request - uint8 requestType; - uint16 requestChainId; - uint32 requestNonce; - address requestTo; - bytes requestData; - bytes requestBlock; - // Response - uint64 blockNumber; + uint32 nonce; + ParsedPerChainQueryResponse [] responses; + } + + /// @dev ParsedPerChainQueryResponse describes a single per-chain response. + struct ParsedPerChainQueryResponse { + uint16 chainId; + uint8 queryType; + bytes request; + bytes response; + } + + /// @dev ParsedPerChainQueryResponse describes an ETH call per-chain query. + struct EthCallQueryResponse { + bytes requestBlockId; + uint64 blockNum; bytes32 blockHash; - uint32 blockTime; - bytes result; + uint64 blockTime; + EthCallData [] result; } + /// @dev ParsedPerChainQueryResponse describes a single ETH call query / response pair. + struct EthCallData { + address contractAddress; + bytes callData; + bytes result; + } + bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); IWormhole public immutable wormhole; @@ -41,13 +57,132 @@ contract QueryResponse { function getResponseDigest(bytes memory response) public pure returns (bytes32) { return keccak256(abi.encodePacked(responsePrefix,getResponseHash(response))); } + + /// @dev parseAndVerifyQueryResponse verifies the query response and returns the parsed response. + function parseAndVerifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) { + verifyQueryResponseSignatures(response, signatures); + + uint index = 0; + + r.version = response.toUint8(index); + require(r.version == 1, "invalid response version"); + index += 1; + + r.senderChainId = response.toUint16(index); + index += 2; + + if (r.senderChainId == 0) { + r.requestId = response.slice(index, 65); + index += 65; + } else { + r.requestId = response.slice(index, 32); + index += 32; + } + + uint32 len = response.toUint32(index); // query_request_len + index += 4; + uint reqIdx = index; + + require(response.toUint8(reqIdx) == r.version, "version mismatch between request and response"); + reqIdx += 1; + + r.nonce = response.toUint32(reqIdx); + reqIdx += 4; + + uint8 numPerChainQueries = response.toUint8(reqIdx); + reqIdx += 1; + + // The response starts after the request. + uint respIdx = index + len; + + require(response.toUint8(respIdx) == numPerChainQueries, "num_per_chain_responses does not match num_per_chain_queries"); + respIdx += 1; + + r.responses = new ParsedPerChainQueryResponse[](numPerChainQueries); + + // Walk through the requests and responses in lock step. + for (uint idx = 0; idx < numPerChainQueries; idx++) { + r.responses[idx].chainId = response.toUint16(reqIdx); + require(response.toUint16(respIdx) == r.responses[idx].chainId, "reqChainId does not match respChainId"); + reqIdx += 2; + respIdx += 2; + + r.responses[idx].queryType = response.toUint8(reqIdx); + require(response.toUint8(respIdx) == r.responses[idx].queryType, "reqQueryType does not match respQueryType"); + reqIdx += 1; + respIdx += 1; + + require(r.responses[idx].queryType == 1, "EthCall is the only supported query type"); + + len = response.toUint32(reqIdx); + reqIdx += 4; + r.responses[idx].request = response.slice(reqIdx, len); + reqIdx += len; + + len = response.toUint32(respIdx); + respIdx += 4; + r.responses[idx].response = response.slice(respIdx, len); + respIdx += len; + } + + return r; + } + + /// @dev parseEthCallQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query. + function parseEthCallQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallQueryResponse memory r) { + require(pcr.queryType == 1, "query type must be EthCall"); + + uint reqIdx = 0; + uint respIdx = 0; + + uint32 len = pcr.request.toUint32(reqIdx); // block_id_len + reqIdx += 4; + + r.requestBlockId = pcr.request.slice(reqIdx, len); + reqIdx += len; + + uint8 numBatchCallData = pcr.request.toUint8(reqIdx); + reqIdx += 1; + + r.blockNum = pcr.response.toUint64(respIdx); + respIdx += 8; + + r.blockHash = pcr.response.toBytes32(respIdx); + respIdx += 32; + + r.blockTime = pcr.response.toUint64(respIdx); + respIdx += 8; + + require(pcr.response.toUint8(respIdx) == numBatchCallData, "num results doesn't match num call datas"); + respIdx += 1; + + r.result = new EthCallData[](numBatchCallData); + + // Walk through the call data and results in lock step. + for (uint idx = 0; idx < numBatchCallData; idx++) { + r.result[idx].contractAddress = pcr.request.toAddress(reqIdx); + reqIdx += 20; + + len = pcr.request.toUint32(reqIdx); // call_data_len + reqIdx += 4; + r.result[idx].callData = pcr.request.slice(reqIdx, len); + reqIdx += len; + + len = pcr.response.toUint32(respIdx); // result_len + respIdx += 4; + r.result[idx].result = pcr.response.slice(respIdx, len); + respIdx += len; + } + + return r; + } /** - * @dev verifyQueryResponse serves to + * @dev verifyQueryResponseSignatures serves to * IWormhole.Signature expects the last byte to be bumped by 27 * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174 */ - function verifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view { + function verifyQueryResponseSignatures(bytes memory response, IWormhole.Signature[] memory signatures) public view { // TODO: make a verifyCurrentQuorum call on the core bridge so that there is only 1 cross call instead of 4 uint32 gsi = wormhole.getCurrentGuardianSetIndex(); IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(gsi); @@ -83,71 +218,4 @@ contract QueryResponse { /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet. } - - function parseEthCallResponse(bytes memory response) internal pure returns (EthCallResponse memory r) { - uint index = 0; - - r.senderChainId = response.toUint16(index); - index += 2; - - if (r.senderChainId == 0) { - r.requestId = response.slice(index, 65); - index += 65; - } else { - r.requestId = response.slice(index, 32); - index += 32; - } - - r.requestType = response.toUint8(index); - index += 1; - - require(r.requestType == 1, "invalid request type"); - - r.requestChainId = response.toUint16(index); - index += 2; - - r.requestNonce = response.toUint32(index); - index += 4; - - r.requestTo = response.toAddress(index); - index += 20; - - uint32 len = response.toUint32(index); - index += 4; - r.requestData = response.slice(index, len); - index += len; - - len = response.toUint32(index); - index += 4; - r.requestBlock = response.slice(index, len); - index += len; - - r.blockNumber = response.toUint64(index); - index += 8; - - r.blockHash = response.toBytes32(index); - index += 32; - - r.blockTime = response.toUint32(index); - index += 4; - - len = response.toUint32(index); - index += 4; - r.result = response.slice(index, len); - index += len; - - require(response.length == index, "invalid response"); - } - - function processStringResult(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (string memory result) { - verifyQueryResponse(response, signatures); - EthCallResponse memory parsed = parseEthCallResponse(response); - // Polygon - require(parsed.requestChainId == 5, "invalid request chain"); - // WMATIC - require(parsed.requestTo == 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270, "invalid request to"); - // Name - require(parsed.requestData.equal(abi.encodeWithSignature("name()")), "invalid request data"); - (result) = abi.decode(parsed.result, (string)); - } } diff --git a/ethereum/scripts/test_query.js b/ethereum/scripts/test_query.js index c478f662bb..3d38815b58 100644 --- a/ethereum/scripts/test_query.js +++ b/ethereum/scripts/test_query.js @@ -1,16 +1,21 @@ // run this script with truffle exec +// node_modules/.bin/truffle exec scripts/test_query.js const jsonfile = require("jsonfile"); const QueryResponseABI = jsonfile.readFileSync( "../build/contracts/QueryResponse.json" ).abi; + const responseBytes = - "0x00004d2fded93c872040330a7d4a60cb4431d6c929c720437ed345daaff928f786f45fa31c825bad2714a69ca3f7d1324f8f9d51dc452fdfacff65ff4c6ad7e7390301010005000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde03000000066c6174657374000000000295f40396c790fb8cb9407de03f61daa46ef15a3c20d301e09af14c850185294c07580c6477b4cf000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000"; + "0x010000ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f2000000005301dd9914c6010005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd01000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; + +const sigBytes = + "ba36cd576a0f9a8a37ec5ea6a174857922f2f170cd7ec62edcbe74b1cc7258d301e8690cfd627e608d63b5d165e2190ba081bb84f5cf473fd353109e152f72fa00"; const sigs = [ [ - "0xb8320f25a42b2b4a832e18e5b68dc12f269d7d8a8d9d43c76107d74d4d7202eb", - "0x6d8258da2a60de95ebce1283ba6ec124a2301a864e35979ebb48af5981e0fae8", - "0x1b", // zero add 27 + "0x" + sigBytes.substring(0, 64), + "0x" + sigBytes.substring(64, 128), + "0x" + (parseInt(sigBytes.substring(128, 130), 16) + 27).toString(16), // last byte plus magic 27 "0x00", ], ]; @@ -34,22 +39,39 @@ module.exports = async function(callback) { const hashResult = await initialized.methods .getResponseHash(responseBytes) .call(); - console.log(hashResult); + console.log("hash:", hashResult); const digestResult = await initialized.methods .getResponseDigest(responseBytes) .call(); - console.log(digestResult); + console.log("digest:", digestResult); const verify = await initialized.methods - .verifyQueryResponse(responseBytes, sigs) + .verifyQueryResponseSignatures(responseBytes, sigs) .call(); - console.log(verify); + console.log("verify result:", verify); - const result = await initialized.methods - .processStringResult(responseBytes, sigs) + const response = await initialized.methods + .parseAndVerifyQueryResponse(responseBytes, sigs) .call(); - console.log(result); + console.log("response:", response); + + for (let idx = 0; idx < response.responses.length; ++idx) { + const pcr = response.responses[idx]; + if (pcr.queryType !== "1") { + console.error( + "eth query result" + idx + " has an invalid query type:", + pcr.queryType + ); + } else { + const ethResult = await initialized.methods + .parseEthCallQueryResponse(pcr) + .call(); + console.log("eth query result" + idx + ":", ethResult); + } + } + + console.log("Test complete"); callback(); } catch (e) { From 977618cf656d813112a4065d333a8309b443009b Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Fri, 21 Jul 2023 11:09:06 -0500 Subject: [PATCH 22/37] Start using forge tests --- ethereum/contracts/query/QueryResponse.sol | 12 +-- ethereum/forge-test/query/Query.t.sol | 119 +++++++++++++++++++++ ethereum/scripts/test_query.js | 14 ++- 3 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 ethereum/forge-test/query/Query.t.sol diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index 4df1c339d2..263db40cc2 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -44,11 +44,6 @@ contract QueryResponse { } bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); - IWormhole public immutable wormhole; - - constructor (address _wormhole) { - wormhole = IWormhole(_wormhole); - } function getResponseHash(bytes memory response) public pure returns (bytes32) { return keccak256(response); @@ -59,8 +54,8 @@ contract QueryResponse { } /// @dev parseAndVerifyQueryResponse verifies the query response and returns the parsed response. - function parseAndVerifyQueryResponse(bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) { - verifyQueryResponseSignatures(response, signatures); + function parseAndVerifyQueryResponse(address wormhole, bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) { + verifyQueryResponseSignatures(wormhole, response, signatures); uint index = 0; @@ -182,7 +177,8 @@ contract QueryResponse { * IWormhole.Signature expects the last byte to be bumped by 27 * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174 */ - function verifyQueryResponseSignatures(bytes memory response, IWormhole.Signature[] memory signatures) public view { + function verifyQueryResponseSignatures(address _wormhole, bytes memory response, IWormhole.Signature[] memory signatures) public view { + IWormhole wormhole = IWormhole(_wormhole); // TODO: make a verifyCurrentQuorum call on the core bridge so that there is only 1 cross call instead of 4 uint32 gsi = wormhole.getCurrentGuardianSetIndex(); IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(gsi); diff --git a/ethereum/forge-test/query/Query.t.sol b/ethereum/forge-test/query/Query.t.sol new file mode 100644 index 0000000000..7824e6f2bd --- /dev/null +++ b/ethereum/forge-test/query/Query.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache 2 + +// forge test --match-contract QueryResponse + +pragma solidity ^0.8.0; + +import "../../contracts/query/QueryResponse.sol"; +import "../../contracts/Implementation.sol"; +import "../../contracts/Setup.sol"; +import "../../contracts/Wormhole.sol"; +import "forge-std/Test.sol"; + +contract TestQueryResponse is Test { + bytes resp = hex"010000ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f2000000005301dd9914c6010005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd01000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; + + bytes32 sigR = hex"ba36cd576a0f9a8a37ec5ea6a174857922f2f170cd7ec62edcbe74b1cc7258d3"; + bytes32 sigS = hex"01e8690cfd627e608d63b5d165e2190ba081bb84f5cf473fd353109e152f72fa"; + uint8 sigV = 27; // last byte plus magic 27 + uint8 sigGuardianIndex = 0; + + bytes32 expectedHash = 0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e; + bytes32 expectedDigetst = 0x5b84b19c68ee0b37899230175a92ee6eda4c5192e8bffca1d057d811bb3660e2; + + QueryResponse qr; + Wormhole wormhole; + + function setUp() public { + wormhole = deployWormhole(); + qr = new QueryResponse(); + } + + uint16 constant TEST_CHAIN_ID = 2; + address constant DEVNET_GUARDIAN = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe; + uint16 constant GOVERNANCE_CHAIN_ID = 1; + bytes32 constant GOVERNANCE_CONTRACT = 0x0000000000000000000000000000000000000000000000000000000000000004; + + function deployWormhole() public returns (Wormhole) { + // Deploy the Setup contract. + Setup setup = new Setup(); + + // Deploy the Implementation contract. + Implementation implementation = new Implementation(); + + address[] memory guardians = new address[](1); + guardians[0] = DEVNET_GUARDIAN; + + // Deploy the Wormhole contract. + wormhole = new Wormhole( + address(setup), + abi.encodeWithSelector( + bytes4(keccak256("setup(address,address[],uint16,uint16,bytes32,uint256)")), + address(implementation), + guardians, + TEST_CHAIN_ID, + GOVERNANCE_CHAIN_ID, + GOVERNANCE_CONTRACT, + block.chainid // evm chain id + ) + ); + + return wormhole; + } + + function test_getResponseHash() public { + bytes32 hash = qr.getResponseHash(resp); + assertEq(hash, expectedHash); + } + + function test_getResponseDigest() public { + bytes32 digest = qr.getResponseDigest(resp); + assertEq(digest, expectedDigetst); + } + + function test_verifyQueryResponseSignatures() public view { + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + qr.verifyQueryResponseSignatures(address(wormhole), resp, signatures); + } + + function test_parseAndVerifyQueryResponse() public { + IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); + signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); + QueryResponse.ParsedQueryResponse memory r = qr.parseAndVerifyQueryResponse(address(wormhole), resp, signatures); + assertEq(r.version, 1); + assertEq(r.senderChainId, 0); + assertEq(r.requestId, hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200"); + assertEq(r.nonce, 3717797062); + assertEq(r.responses.length, 1); + assertEq(r.responses[0].chainId, 5); + assertEq(r.responses[0].queryType, 1); + assertEq(r.responses[0].request, hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"); + assertEq(r.responses[0].response, hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"); + } + + function test_parseEthCallQueryResponse() public { + QueryResponse.ParsedPerChainQueryResponse memory r = QueryResponse.ParsedPerChainQueryResponse({ + chainId: 5, + queryType: 1, + request: hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd", + response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" + }); + + QueryResponse.EthCallQueryResponse memory eqr = qr.parseEthCallQueryResponse(r); + assertEq(eqr.requestBlockId, hex"307832613631616334"); + assertEq(eqr.blockNum, 44440260); + assertEq(eqr.blockHash, hex"c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d04"); + assertEq(eqr.blockTime, 1687961579000000); + assertEq(eqr.result.length, 2); + + assertEq(eqr.result[0].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)); + assertEq(eqr.result[0].callData, hex"06fdde03"); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000"); + + assertEq(eqr.result[1].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)); + assertEq(eqr.result[1].callData, hex"18160ddd"); + assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000007ae5649beabeddf889364a"); + + } +} diff --git a/ethereum/scripts/test_query.js b/ethereum/scripts/test_query.js index 3d38815b58..772fd2629a 100644 --- a/ethereum/scripts/test_query.js +++ b/ethereum/scripts/test_query.js @@ -20,7 +20,7 @@ const sigs = [ ], ]; const expectedHash = - "0x3c4628ca459c0ee5344d91146776f46627bdbf189f4f045d9dedf480861c05f3"; + "0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e"; const expectedDigest = "0x616674308c1ab1b468665f21fd3808a8fc5807a4ca9859b681d2e3f7ace97cc2"; @@ -47,12 +47,20 @@ module.exports = async function(callback) { console.log("digest:", digestResult); const verify = await initialized.methods - .verifyQueryResponseSignatures(responseBytes, sigs) + .verifyQueryResponseSignatures( + "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", + responseBytes, + sigs + ) .call(); console.log("verify result:", verify); const response = await initialized.methods - .parseAndVerifyQueryResponse(responseBytes, sigs) + .parseAndVerifyQueryResponse( + "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", + responseBytes, + sigs + ) .call(); console.log("response:", response); From d8f84c25a54c8d65a292b20c9b535af8791ec0d2 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 31 Jul 2023 10:55:02 -0500 Subject: [PATCH 23/37] Convert to a library --- ethereum/contracts/query/QueryResponse.sol | 11 ++- ethereum/forge-test/query/Query.t.sol | 18 ++--- ethereum/scripts/test_query.js | 88 ---------------------- 3 files changed, 15 insertions(+), 102 deletions(-) delete mode 100644 ethereum/scripts/test_query.js diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index 263db40cc2..00469750fe 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -6,8 +6,8 @@ pragma solidity ^0.8.0; import "../libraries/external/BytesLib.sol"; import "../interfaces/IWormhole.sol"; -// TODO: move functions to library -contract QueryResponse { +/// @dev QueryResponse is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses. +library QueryResponse { using BytesLib for bytes; /// @dev ParsedQueryResponse is returned by parseAndVerifyQueryResponse(). @@ -27,7 +27,7 @@ contract QueryResponse { bytes response; } - /// @dev ParsedPerChainQueryResponse describes an ETH call per-chain query. + /// @dev EthCallQueryResponse describes an ETH call per-chain query. struct EthCallQueryResponse { bytes requestBlockId; uint64 blockNum; @@ -36,7 +36,7 @@ contract QueryResponse { EthCallData [] result; } - /// @dev ParsedPerChainQueryResponse describes a single ETH call query / response pair. + /// @dev EthCallData describes a single ETH call query / response pair. struct EthCallData { address contractAddress; bytes callData; @@ -45,10 +45,12 @@ contract QueryResponse { bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); + /// @dev getResponseHash computes the hash of the specified query response. function getResponseHash(bytes memory response) public pure returns (bytes32) { return keccak256(response); } + /// @dev getResponseDigest computes the digest of the specified query response. function getResponseDigest(bytes memory response) public pure returns (bytes32) { return keccak256(abi.encodePacked(responsePrefix,getResponseHash(response))); } @@ -215,3 +217,4 @@ contract QueryResponse { /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet. } } + diff --git a/ethereum/forge-test/query/Query.t.sol b/ethereum/forge-test/query/Query.t.sol index 7824e6f2bd..dba883d02b 100644 --- a/ethereum/forge-test/query/Query.t.sol +++ b/ethereum/forge-test/query/Query.t.sol @@ -21,12 +21,10 @@ contract TestQueryResponse is Test { bytes32 expectedHash = 0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e; bytes32 expectedDigetst = 0x5b84b19c68ee0b37899230175a92ee6eda4c5192e8bffca1d057d811bb3660e2; - QueryResponse qr; Wormhole wormhole; function setUp() public { - wormhole = deployWormhole(); - qr = new QueryResponse(); + wormhole = deployWormholeForTest(); } uint16 constant TEST_CHAIN_ID = 2; @@ -34,7 +32,7 @@ contract TestQueryResponse is Test { uint16 constant GOVERNANCE_CHAIN_ID = 1; bytes32 constant GOVERNANCE_CONTRACT = 0x0000000000000000000000000000000000000000000000000000000000000004; - function deployWormhole() public returns (Wormhole) { + function deployWormholeForTest() public returns (Wormhole) { // Deploy the Setup contract. Setup setup = new Setup(); @@ -62,25 +60,25 @@ contract TestQueryResponse is Test { } function test_getResponseHash() public { - bytes32 hash = qr.getResponseHash(resp); + bytes32 hash = QueryResponse.getResponseHash(resp); assertEq(hash, expectedHash); } function test_getResponseDigest() public { - bytes32 digest = qr.getResponseDigest(resp); + bytes32 digest = QueryResponse.getResponseDigest(resp); assertEq(digest, expectedDigetst); } function test_verifyQueryResponseSignatures() public view { IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); - qr.verifyQueryResponseSignatures(address(wormhole), resp, signatures); + QueryResponse.verifyQueryResponseSignatures(address(wormhole), resp, signatures); } function test_parseAndVerifyQueryResponse() public { IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); - QueryResponse.ParsedQueryResponse memory r = qr.parseAndVerifyQueryResponse(address(wormhole), resp, signatures); + QueryResponse.ParsedQueryResponse memory r = QueryResponse.parseAndVerifyQueryResponse(address(wormhole), resp, signatures); assertEq(r.version, 1); assertEq(r.senderChainId, 0); assertEq(r.requestId, hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200"); @@ -93,6 +91,7 @@ contract TestQueryResponse is Test { } function test_parseEthCallQueryResponse() public { + // Take the data extracted by the previous test and break it down even further. QueryResponse.ParsedPerChainQueryResponse memory r = QueryResponse.ParsedPerChainQueryResponse({ chainId: 5, queryType: 1, @@ -100,7 +99,7 @@ contract TestQueryResponse is Test { response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" }); - QueryResponse.EthCallQueryResponse memory eqr = qr.parseEthCallQueryResponse(r); + QueryResponse.EthCallQueryResponse memory eqr = QueryResponse.parseEthCallQueryResponse(r); assertEq(eqr.requestBlockId, hex"307832613631616334"); assertEq(eqr.blockNum, 44440260); assertEq(eqr.blockHash, hex"c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d04"); @@ -114,6 +113,5 @@ contract TestQueryResponse is Test { assertEq(eqr.result[1].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)); assertEq(eqr.result[1].callData, hex"18160ddd"); assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000007ae5649beabeddf889364a"); - } } diff --git a/ethereum/scripts/test_query.js b/ethereum/scripts/test_query.js deleted file mode 100644 index 772fd2629a..0000000000 --- a/ethereum/scripts/test_query.js +++ /dev/null @@ -1,88 +0,0 @@ -// run this script with truffle exec -// node_modules/.bin/truffle exec scripts/test_query.js - -const jsonfile = require("jsonfile"); -const QueryResponseABI = jsonfile.readFileSync( - "../build/contracts/QueryResponse.json" -).abi; - -const responseBytes = - "0x010000ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f2000000005301dd9914c6010005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd01000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; - -const sigBytes = - "ba36cd576a0f9a8a37ec5ea6a174857922f2f170cd7ec62edcbe74b1cc7258d301e8690cfd627e608d63b5d165e2190ba081bb84f5cf473fd353109e152f72fa00"; -const sigs = [ - [ - "0x" + sigBytes.substring(0, 64), - "0x" + sigBytes.substring(64, 128), - "0x" + (parseInt(sigBytes.substring(128, 130), 16) + 27).toString(16), // last byte plus magic 27 - "0x00", - ], -]; -const expectedHash = - "0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e"; -const expectedDigest = - "0x616674308c1ab1b468665f21fd3808a8fc5807a4ca9859b681d2e3f7ace97cc2"; - -module.exports = async function(callback) { - try { - const QueryResponse = await artifacts.require("QueryResponse"); - //Query deploy - const queryAddress = ( - await QueryResponse.new("0xC89Ce4735882C9F0f0FE26686c53074E09B0D550") - ).address; - - console.log("QueryResponse deployed at: " + queryAddress); - - const initialized = new web3.eth.Contract(QueryResponseABI, queryAddress); - - const hashResult = await initialized.methods - .getResponseHash(responseBytes) - .call(); - console.log("hash:", hashResult); - - const digestResult = await initialized.methods - .getResponseDigest(responseBytes) - .call(); - console.log("digest:", digestResult); - - const verify = await initialized.methods - .verifyQueryResponseSignatures( - "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", - responseBytes, - sigs - ) - .call(); - console.log("verify result:", verify); - - const response = await initialized.methods - .parseAndVerifyQueryResponse( - "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", - responseBytes, - sigs - ) - .call(); - console.log("response:", response); - - for (let idx = 0; idx < response.responses.length; ++idx) { - const pcr = response.responses[idx]; - if (pcr.queryType !== "1") { - console.error( - "eth query result" + idx + " has an invalid query type:", - pcr.queryType - ); - } else { - const ethResult = await initialized.methods - .parseEthCallQueryResponse(pcr) - .call(); - console.log("eth query result" + idx + ":", ethResult); - } - } - - console.log("Test complete"); - - callback(); - } catch (e) { - callback(e); - } -}; From 97b5822170858b516284b0a9b1fa991b9d32cde0 Mon Sep 17 00:00:00 2001 From: kev1n-peters <96065607+kev1n-peters@users.noreply.github.com> Date: Fri, 25 Aug 2023 13:15:21 -0500 Subject: [PATCH 24/37] ccq: Query Server (#3113) The CCQ Query Server is an HTTP server / RESTful service that handles query requests, publishes them to the gossip network, waits for the responses to reach quorum, and then returns the response. --- Tiltfile | 15 ++ devnet/query-server.yaml | 54 ++++++++ node/cmd/ccq/http.go | 133 ++++++++++++++++++ node/cmd/ccq/p2p.go | 237 ++++++++++++++++++++++++++++++++ node/cmd/ccq/pending_request.go | 60 ++++++++ node/cmd/ccq/query_server.go | 110 +++++++++++++++ node/cmd/ccq/utils.go | 41 ++++++ node/cmd/root.go | 2 + 8 files changed, 652 insertions(+) create mode 100644 devnet/query-server.yaml create mode 100644 node/cmd/ccq/http.go create mode 100644 node/cmd/ccq/p2p.go create mode 100644 node/cmd/ccq/pending_request.go create mode 100644 node/cmd/ccq/query_server.go create mode 100644 node/cmd/ccq/utils.go diff --git a/Tiltfile b/Tiltfile index 1662011536..390face566 100644 --- a/Tiltfile +++ b/Tiltfile @@ -72,6 +72,7 @@ config.define_bool("wormchain", False, "Enable a wormchain node") config.define_bool("ibc_relayer", False, "Enable IBC relayer between cosmos chains") config.define_bool("redis", False, "Enable a redis instance") config.define_bool("generic_relayer", False, "Enable the generic relayer off-chain component") +config.define_bool("query_server", False, "Enable cross-chain query server") cfg = config.parse() num_guardians = int(cfg.get("num", "1")) @@ -96,6 +97,7 @@ ibc_relayer = cfg.get("ibc_relayer", ci) btc = cfg.get("btc", False) redis = cfg.get('redis', ci) generic_relayer = cfg.get("generic_relayer", ci) +query_server = cfg.get("query_server", ci) if ci: guardiand_loglevel = cfg.get("guardiand_loglevel", "warn") @@ -898,3 +900,16 @@ if aptos: labels = ["aptos"], trigger_mode = trigger_mode, ) + +if query_server: + k8s_yaml_with_ns("devnet/query-server.yaml") + + k8s_resource( + "query-server", + resource_deps = ["guardian"], + port_forwards = [ + port_forward(6069, name = "REST [:6069]", host = webHost) + ], + labels = ["query-server"], + trigger_mode = trigger_mode + ) \ No newline at end of file diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml new file mode 100644 index 0000000000..90a62214da --- /dev/null +++ b/devnet/query-server.yaml @@ -0,0 +1,54 @@ +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 + - --nodeKey + - /tmp/node.key + - --listenAddr + - "[::]:6069" + - --ethRPC + - http://eth-devnet:8545 + - --ethContract + - "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" + # Hardcoded devnet bootstrap (generated from deterministic key in guardiand) + - --bootstrap + - /dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw + - --logLevel=info + ports: + - containerPort: 6069 + name: rest + protocol: TCP + readinessProbe: + tcpSocket: + port: rest diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go new file mode 100644 index 0000000000..128e8a7942 --- /dev/null +++ b/node/cmd/ccq/http.go @@ -0,0 +1,133 @@ +package ccq + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "sort" + "time" + + 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" + "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 + pendingResponses *PendingResponses +} + +func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { + var q queryRequest + err := json.NewDecoder(r.Body).Decode(&q) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + queryRequestBytes, err := hex.DecodeString(q.Bytes) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // TODO: check if request signer is authorized on Wormchain + + signature, err := hex.DecodeString(q.Signature) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: signature, + } + + // TODO: validate request before publishing + + m := gossipv1.GossipMessage{ + Message: &gossipv1.GossipMessage_SignedQueryRequest{ + SignedQueryRequest: signedQueryRequest, + }, + } + + b, err := proto.Marshal(&m) + if err != nil { + 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 { + 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 NewHTTPServer(addr string, t *pubsub.Topic, p *PendingResponses) *http.Server { + s := &httpServer{ + topic: t, + pendingResponses: p, + } + r := mux.NewRouter() + r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT") + return &http.Server{ + Addr: addr, + Handler: r, + ReadHeaderTimeout: 5 * time.Second, + } +} diff --git a/node/cmd/ccq/p2p.go b/node/cmd/ccq/p2p.go new file mode 100644 index 0000000000..1dbb5f95bb --- /dev/null +++ b/node/cmd/ccq/p2p.go @@ -0,0 +1,237 @@ +package ccq + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/certusone/wormhole/node/pkg/p2p" + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "github.com/certusone/wormhole/node/pkg/query" + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/libp2p/go-libp2p" + dht "github.com/libp2p/go-libp2p-kad-dht" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/libp2p/go-libp2p/core/routing" + libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" + libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "github.com/multiformats/go-multiaddr" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +type GuardianSignature struct { + Index int + Signature string +} + +type SignedResponse struct { + Response *query.QueryResponsePublication + Signatures []GuardianSignature +} + +type P2PSub struct { + sub *pubsub.Subscription + topic *pubsub.Topic + host host.Host +} + +func runP2P(ctx context.Context, priv crypto.PrivKey, port uint, networkID, bootstrap, ethRpcUrl, ethCoreAddr string, pendingResponses *PendingResponses, logger *zap.Logger) (*P2PSub, error) { + // p2p setup + components := p2p.DefaultComponents() + components.Port = port + h, err := libp2p.New( + // Use the keypair we generated + libp2p.Identity(priv), + + // Multiple listen addresses + libp2p.ListenAddrStrings( + components.ListeningAddresses()..., + ), + + // Enable TLS security as the only security protocol. + libp2p.Security(libp2ptls.ID, libp2ptls.New), + + // Enable QUIC transport as the only transport. + libp2p.Transport(libp2pquic.NewTransport), + + // Let's prevent our peer from having too many + // connections by attaching a connection manager. + libp2p.ConnectionManager(components.ConnMgr), + + // Let this host use the DHT to find other hosts + libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { + logger.Info("Connecting to bootstrap peers", zap.String("bootstrap_peers", bootstrap)) + bootstrappers := make([]peer.AddrInfo, 0) + for _, addr := range strings.Split(bootstrap, ",") { + if addr == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + logger.Error("Invalid bootstrap address", zap.String("peer", addr), zap.Error(err)) + continue + } + if pi.ID == h.ID() { + logger.Info("We're a bootstrap node") + continue + } + bootstrappers = append(bootstrappers, *pi) + } + idht, err := dht.New(ctx, h, dht.Mode(dht.ModeServer), + // This intentionally makes us incompatible with the global IPFS DHT + dht.ProtocolPrefix(protocol.ID("/"+networkID)), + dht.BootstrapPeers(bootstrappers...), + ) + return idht, err + }), + ) + + if err != nil { + return nil, err + } + + topicName := fmt.Sprintf("%s/%s", networkID, "broadcast") + + logger.Info("Subscribing pubsub topic", zap.String("topic", topicName)) + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, err + } + + topic, err := ps.Join(topicName) + if err != nil { + logger.Error("failed to join topic", zap.Error(err)) + return nil, err + } + + sub, err := topic.Subscribe() + if err != nil { + logger.Error("failed to subscribe topic", zap.Error(err)) + return nil, err + } + + logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), + zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) + + // Wait for peers + for len(topic.ListPeers()) < 1 { + time.Sleep(time.Millisecond * 100) + } + + // Fetch the initial current guardian set + guardianSet, err := FetchCurrentGuardianSet(ethRpcUrl, ethCoreAddr) + if err != nil { + logger.Fatal("Failed to fetch current guardian set", zap.Error(err)) + } + quorum := vaa.CalculateQuorum(len(guardianSet.Keys)) + + // Listen to the p2p network for query responses + go func() { + // Maps the request signature to a map of response digests which maps to a list of guardian signatures. + // A request could have responses with different digests, because the guardians could have + // different results returned for the query in the event of a rollback. + responses := make(map[string]map[ethCommon.Hash][]GuardianSignature) + for { + envelope, err := sub.Next(ctx) + if err != nil { + logger.Error("Failed to read next pubsub message", zap.Error(err)) + break + } + var msg gossipv1.GossipMessage + err = proto.Unmarshal(envelope.Data, &msg) + if err != nil { + logger.Error("received invalid message", zap.Binary("data", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + continue + } + switch m := msg.Message.(type) { + case *gossipv1.GossipMessage_SignedQueryResponse: + logger.Debug("query response received", zap.Any("response", m.SignedQueryResponse)) + var queryResponse query.QueryResponsePublication + err := queryResponse.Unmarshal(m.SignedQueryResponse.QueryResponse) + if err != nil { + logger.Error("failed to unmarshal response", zap.Error(err)) + continue + } + requestSignature := hex.EncodeToString(queryResponse.Request.Signature) + // Check that we're handling the request for this response + pendingResponse := pendingResponses.Get(requestSignature) + if pendingResponse == nil { + logger.Debug("skipping query response for unknown request", zap.String("signature", requestSignature)) + continue + } + // Make sure that the request bytes match + if !bytes.Equal(queryResponse.Request.QueryRequest, pendingResponse.req.QueryRequest) || + !bytes.Equal(queryResponse.Request.Signature, pendingResponse.req.Signature) { + continue + } + digest := query.GetQueryResponseDigestFromBytes(m.SignedQueryResponse.QueryResponse) + signerBytes, err := ethCrypto.Ecrecover(digest.Bytes(), m.SignedQueryResponse.Signature) + if err != nil { + logger.Error("failed to verify signature on response", + zap.String("digest", digest.Hex()), + zap.String("signature", hex.EncodeToString(m.SignedQueryResponse.Signature)), + zap.Error(err)) + continue + } + signerAddress := ethCommon.BytesToAddress(ethCrypto.Keccak256(signerBytes[1:])[12:]) + keyIdx, hasKeyIdx := guardianSet.KeyIndex(signerAddress) + + if hasKeyIdx { + if _, ok := responses[requestSignature]; !ok { + responses[requestSignature] = make(map[ethCommon.Hash][]GuardianSignature) + } + found := false + for _, gs := range responses[requestSignature][digest] { + if gs.Index == keyIdx { + found = true + break + } + } + if found { + // Already handled the response from this guardian + continue + } + responses[requestSignature][digest] = append(responses[requestSignature][digest], GuardianSignature{ + Index: keyIdx, + Signature: hex.EncodeToString(m.SignedQueryResponse.Signature), + }) + // quorum is reached when a super-majority of guardians have signed a response with the same digest + if len(responses[requestSignature][digest]) >= quorum { + s := &SignedResponse{ + Response: &queryResponse, + Signatures: responses[requestSignature][digest], + } + delete(responses, requestSignature) + pendingResponse.ch <- s + } + } else { + logger.Warn("received observation by unknown guardian - is our guardian set outdated?", + zap.String("digest", digest.Hex()), zap.String("address", signerAddress.Hex()), + ) + } + } + } + }() + + return &P2PSub{ + sub: sub, + topic: topic, + host: h, + }, nil +} diff --git a/node/cmd/ccq/pending_request.go b/node/cmd/ccq/pending_request.go new file mode 100644 index 0000000000..ecc0063f34 --- /dev/null +++ b/node/cmd/ccq/pending_request.go @@ -0,0 +1,60 @@ +package ccq + +import ( + "encoding/hex" + "sync" + + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" +) + +type PendingResponse struct { + req *gossipv1.SignedQueryRequest + ch chan *SignedResponse +} + +func NewPendingResponse(req *gossipv1.SignedQueryRequest) *PendingResponse { + return &PendingResponse{ + req: req, + ch: make(chan *SignedResponse), + } +} + +type PendingResponses struct { + pendingResponses map[string]*PendingResponse + mu sync.RWMutex +} + +func NewPendingResponses() *PendingResponses { + return &PendingResponses{ + pendingResponses: make(map[string]*PendingResponse), + } +} + +func (p *PendingResponses) Add(r *PendingResponse) bool { + signature := hex.EncodeToString(r.req.Signature) + p.mu.Lock() + defer p.mu.Unlock() + if _, ok := p.pendingResponses[signature]; ok { + // the request w/ this signature is already being handled + // don't overwrite + return false + } + p.pendingResponses[signature] = r + return true +} + +func (p *PendingResponses) Get(signature string) *PendingResponse { + p.mu.RLock() + defer p.mu.RUnlock() + if r, ok := p.pendingResponses[signature]; ok { + return r + } + return nil +} + +func (p *PendingResponses) Remove(r *PendingResponse) { + signature := hex.EncodeToString(r.req.Signature) + p.mu.Lock() + defer p.mu.Unlock() + delete(p.pendingResponses, signature) +} diff --git a/node/cmd/ccq/query_server.go b/node/cmd/ccq/query_server.go new file mode 100644 index 0000000000..073a73a585 --- /dev/null +++ b/node/cmd/ccq/query_server.go @@ -0,0 +1,110 @@ +package ccq + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/certusone/wormhole/node/pkg/common" + ipfslog "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var ( + p2pNetworkID *string + p2pPort *uint + p2pBootstrap *string + listenAddr *string + nodeKeyPath *string + ethRPC *string + ethContract *string + logLevel *string +) + +func init() { + p2pNetworkID = QueryServerCmd.Flags().String("network", "/wormhole/dev", "P2P network identifier") + p2pPort = QueryServerCmd.Flags().Uint("port", 8999, "P2P UDP listener port") + p2pBootstrap = QueryServerCmd.Flags().String("bootstrap", "", "P2P bootstrap peers (comma-separated)") + listenAddr = QueryServerCmd.Flags().String("listenAddr", "[::]:6069", "Listen address for query server (disabled if blank)") + nodeKeyPath = QueryServerCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)") + ethRPC = QueryServerCmd.Flags().String("ethRPC", "", "Ethereum RPC for fetching current guardian set") + ethContract = QueryServerCmd.Flags().String("ethContract", "", "Ethereum core bridge address for fetching current guardian set") + logLevel = QueryServerCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)") +} + +var QueryServerCmd = &cobra.Command{ + Use: "query-server", + Short: "Run the cross-chain query server", + Run: runQueryServer, +} + +func runQueryServer(cmd *cobra.Command, args []string) { + common.SetRestrictiveUmask() + + // Setup logging + lvl, err := ipfslog.LevelFromString(*logLevel) + if err != nil { + fmt.Println("Invalid log level") + os.Exit(1) + } + + logger := ipfslog.Logger("query-server").Desugar() + ipfslog.SetAllLoggers(lvl) + + // Verify flags + if *nodeKeyPath == "" { + logger.Fatal("Please specify --nodeKey") + } + if *p2pBootstrap == "" { + logger.Fatal("Please specify --bootstrap") + } + if *ethRPC == "" { + logger.Fatal("Please specify --ethRPC") + } + if *ethContract == "" { + logger.Fatal("Please specify --ethContract") + } + + // Load p2p private key + var priv crypto.PrivKey + priv, err = common.GetOrCreateNodeKey(logger, *nodeKeyPath) + if err != nil { + logger.Fatal("Failed to load node key", zap.Error(err)) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Run p2p + pendingResponses := NewPendingResponses() + p2p, err := runP2P(ctx, priv, *p2pPort, *p2pNetworkID, *p2pBootstrap, *ethRPC, *ethContract, pendingResponses, logger) + if err != nil { + logger.Fatal("Failed to start p2p", zap.Error(err)) + } + + // Start the HTTP server + go func() { + s := NewHTTPServer(*listenAddr, p2p.topic, pendingResponses) + logger.Sugar().Infof("Server listening on %s", *listenAddr) + err := s.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + logger.Fatal("Server closed unexpectedly", zap.Error(err)) + } + }() + + <-ctx.Done() + logger.Info("Context cancelled, exiting...") + + // Cleanly shutdown + // Without this the same host won't properly discover peers until some timeout + p2p.sub.Cancel() + if err := p2p.topic.Close(); err != nil { + logger.Error("Error closing the topic", zap.Error(err)) + } + if err := p2p.host.Close(); err != nil { + logger.Error("Error closing the host", zap.Error(err)) + } +} diff --git a/node/cmd/ccq/utils.go b/node/cmd/ccq/utils.go new file mode 100644 index 0000000000..b0b1959e52 --- /dev/null +++ b/node/cmd/ccq/utils.go @@ -0,0 +1,41 @@ +package ccq + +import ( + "context" + "fmt" + "time" + + "github.com/certusone/wormhole/node/pkg/common" + 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" + 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 +} diff --git a/node/cmd/root.go b/node/cmd/root.go index a4b39502f3..f221b47969 100644 --- a/node/cmd/root.go +++ b/node/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/certusone/wormhole/node/cmd/ccq" "github.com/certusone/wormhole/node/cmd/debug" "github.com/certusone/wormhole/node/cmd/spy" "github.com/certusone/wormhole/node/pkg/version" @@ -48,6 +49,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.guardiand.yaml)") rootCmd.AddCommand(guardiand.NodeCmd) rootCmd.AddCommand(spy.SpyCmd) + rootCmd.AddCommand(ccq.QueryServerCmd) rootCmd.AddCommand(guardiand.KeygenCmd) rootCmd.AddCommand(guardiand.AdminCmd) rootCmd.AddCommand(guardiand.TemplateCmd) From bf29fb43229bad012290e7d7c9182391871a9f61 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Mon, 28 Aug 2023 11:25:09 -0500 Subject: [PATCH 25/37] sdk/js-query: initial commit --- sdk/js-query/.gitignore | 23 + sdk/js-query/.prettierrc.json | 12 + sdk/js-query/CHANGELOG.md | 3 + sdk/js-query/LICENSE | 13 + sdk/js-query/README.md | 1 + sdk/js-query/jest.config.ts | 7 + sdk/js-query/package-lock.json | 8784 ++++++++++++++++++++++++ sdk/js-query/package.json | 37 + sdk/js-query/src/index.ts | 1 + sdk/js-query/src/query/BinaryWriter.ts | 54 + sdk/js-query/src/query/consts.ts | 1 + sdk/js-query/src/query/ethCall.test.ts | 115 + sdk/js-query/src/query/ethCall.ts | 47 + sdk/js-query/src/query/index.ts | 5 + sdk/js-query/src/query/request.ts | 70 + sdk/js-query/src/query/response.ts | 36 + sdk/js-query/src/query/utils.ts | 33 + sdk/js-query/tsconfig-cjs.json | 7 + sdk/js-query/tsconfig.json | 16 + 19 files changed, 9265 insertions(+) create mode 100644 sdk/js-query/.gitignore create mode 100644 sdk/js-query/.prettierrc.json create mode 100644 sdk/js-query/CHANGELOG.md create mode 100644 sdk/js-query/LICENSE create mode 100644 sdk/js-query/README.md create mode 100644 sdk/js-query/jest.config.ts create mode 100644 sdk/js-query/package-lock.json create mode 100644 sdk/js-query/package.json create mode 100644 sdk/js-query/src/index.ts create mode 100644 sdk/js-query/src/query/BinaryWriter.ts create mode 100644 sdk/js-query/src/query/consts.ts create mode 100644 sdk/js-query/src/query/ethCall.test.ts create mode 100644 sdk/js-query/src/query/ethCall.ts create mode 100644 sdk/js-query/src/query/index.ts create mode 100644 sdk/js-query/src/query/request.ts create mode 100644 sdk/js-query/src/query/response.ts create mode 100644 sdk/js-query/src/query/utils.ts create mode 100644 sdk/js-query/tsconfig-cjs.json create mode 100644 sdk/js-query/tsconfig.json diff --git a/sdk/js-query/.gitignore b/sdk/js-query/.gitignore new file mode 100644 index 0000000000..6ef29c3e06 --- /dev/null +++ b/sdk/js-query/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env* + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# build +/lib diff --git a/sdk/js-query/.prettierrc.json b/sdk/js-query/.prettierrc.json new file mode 100644 index 0000000000..28ec018a9c --- /dev/null +++ b/sdk/js-query/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/sdk/js-query/CHANGELOG.md b/sdk/js-query/CHANGELOG.md new file mode 100644 index 0000000000..edfacc940e --- /dev/null +++ b/sdk/js-query/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +Initial release \ No newline at end of file diff --git a/sdk/js-query/LICENSE b/sdk/js-query/LICENSE new file mode 100644 index 0000000000..31aa61c3e9 --- /dev/null +++ b/sdk/js-query/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Wormhole Project Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/sdk/js-query/README.md b/sdk/js-query/README.md new file mode 100644 index 0000000000..b7cd7eebc7 --- /dev/null +++ b/sdk/js-query/README.md @@ -0,0 +1 @@ +Wormhole cross-chain queries SDK \ No newline at end of file diff --git a/sdk/js-query/jest.config.ts b/sdk/js-query/jest.config.ts new file mode 100644 index 0000000000..71dbebd252 --- /dev/null +++ b/sdk/js-query/jest.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@jest/types"; + +const config: Config.InitialOptions = { + preset: "ts-jest", + testEnvironment: "node", +}; +export default config; diff --git a/sdk/js-query/package-lock.json b/sdk/js-query/package-lock.json new file mode 100644 index 0000000000..8f33c5473e --- /dev/null +++ b/sdk/js-query/package-lock.json @@ -0,0 +1,8784 @@ +{ + "name": "@wormhole-foundation/wormhole-query-sdk", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@wormhole-foundation/wormhole-query-sdk", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@types/elliptic": "^6.4.14", + "elliptic": "^6.5.4" + }, + "devDependencies": { + "axios": "^1.4.0", + "jest": "^29.5.0", + "prettier": "^2.3.2", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.1.3", + "web3": "^4.0.1" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", + "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true, + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "dependencies": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@noble/curves": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.0.0.tgz", + "integrity": "sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "1.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.0.tgz", + "integrity": "sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/curves": "~1.0.0", + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz", + "integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bn.js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", + "integrity": "sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/elliptic": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", + "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", + "dependencies": { + "@types/bn.js": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001505", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", + "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.11" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.434", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz", + "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.0.0.tgz", + "integrity": "sha512-g25m4EtfQGjstWgVE1aIz7XYYjf3kH5kG17ULWVB5dH6uLahsoltOhACzSxyDV+fhn4gbR4xRrOXGe6r2uh4Bg==", + "dev": true, + "dependencies": { + "@noble/curves": "1.0.0", + "@noble/hashes": "1.3.0", + "@scure/bip32": "1.3.0", + "@scure/bip39": "1.2.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", + "dev": true, + "dependencies": { + "is-property": "^1.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-my-ip-valid": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", + "dev": true + }, + "node_modules/is-my-json-valid": { + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", + "dev": true, + "dependencies": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/ts-jest": { + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", + "integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web3": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3/-/web3-4.0.1.tgz", + "integrity": "sha512-IVxPbRy3A+RYB2+NYReNPLDXvE2iamTTvx1oNjM4UdbhNt/KQujQusOaRfSpGqfIKBCIYrim1c5LSCFcKlfQhA==", + "dev": true, + "dependencies": { + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-abi": "^4.0.1", + "web3-eth-accounts": "^4.0.1", + "web3-eth-contract": "^4.0.1", + "web3-eth-ens": "^4.0.1", + "web3-eth-iban": "^4.0.1", + "web3-eth-personal": "^4.0.1", + "web3-net": "^4.0.1", + "web3-providers-http": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-core": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-4.0.1.tgz", + "integrity": "sha512-yGd9FuUhSeLXeSmj+S5YBNdBJfQgBsGGN+uqFRvQKGrKbOp7SXRVNxwTL/JKCLJW2rulcw0JrPD8Ope0A1YvZA==", + "dev": true, + "dependencies": { + "web3-errors": "^1.0.0", + "web3-eth-iban": "^4.0.1", + "web3-providers-http": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + }, + "optionalDependencies": { + "web3-providers-ipc": "^4.0.1" + } + }, + "node_modules/web3-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-errors/-/web3-errors-1.0.0.tgz", + "integrity": "sha512-UadVmAm7FrWfIglZEbyKxEEeVp4p7SMrx1q1SNbX4Cngmsenth96oH+4GSSFLyDASGyWr/yDSDU2alEUTf0yug==", + "dev": true, + "dependencies": { + "web3-types": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-4.0.1.tgz", + "integrity": "sha512-5Tm6uusfZlWDby1zc8unoHtph5wpLoBgnRvtkzB3ZCwnQKL4KU2kqO/y4sUTSVrTM30y/CmZagTW9PKyRAt0UA==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.5", + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth-abi": "^4.0.1", + "web3-eth-accounts": "^4.0.1", + "web3-net": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-abi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-4.0.1.tgz", + "integrity": "sha512-l4vS3oxec8A5bO5ognCQQY+ZonPolw77roNVnFdqkmf3MQpUHHovxCn1kFD+eeiT3DpeSt6GbVT9Zt6koA/LHw==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-accounts": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-4.0.1.tgz", + "integrity": "sha512-4SyowjO930H8/Rz6jspYaW2jCbEpqPYKDU/W2WFOHl7KiJ0edoO3mVsupCGAJQCbDG77ijSwMszHj8pA5KhB+A==", + "dev": true, + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "crc-32": "^1.2.2", + "ethereum-cryptography": "^2.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-contract": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-4.0.1.tgz", + "integrity": "sha512-uVVb1ZZre/kwZIDFJBu7y2LdW5BZO3HJwQKhdqLmnyPTLWTnyKE8Mq2pX5eUZzpoSqXlJCoh0GeAQnIblXYsAw==", + "dev": true, + "dependencies": { + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-abi": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-ens": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-4.0.1.tgz", + "integrity": "sha512-AIPNKs5EyY+w9grIaDkaOxApilBT8Gi7RxJfrVGjR9UnGbHRiH3QX//Y7ZEEkFrhKnZMZ2uim81gyTHP4ujYmg==", + "dev": true, + "dependencies": { + "@adraffy/ens-normalize": "^1.8.8", + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-contract": "^4.0.1", + "web3-net": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-iban": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-4.0.1.tgz", + "integrity": "sha512-SSwbB2+8+IlF97zk6wwdDv2rPAoIfXAsjLKBCRy6abf4lLFX3M1s80ZLCXISeB3DR72MvO4iqA5/hHlUu9Jlcg==", + "dev": true, + "dependencies": { + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-personal": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-4.0.1.tgz", + "integrity": "sha512-fv/PiUYNtQhjYanHJ+veT5xp7+l+HUGk2/vklGxwl9ntrzvgdYJ7Z87WXI+dqzYAljyuunsjEVP4N5QPED5n7g==", + "dev": true, + "dependencies": { + "web3-core": "^4.0.1", + "web3-eth": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-net": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-4.0.1.tgz", + "integrity": "sha512-Fa4NyGyjx/aZwNxdFg1tSkZAQKyEYxfGOFjmsPCzfOC2zaFExv06UPgDEU+aOiY28cs+kTcx5mhjBKn9PLRraw==", + "dev": true, + "dependencies": { + "web3-core": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-http": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-4.0.1.tgz", + "integrity": "sha512-scdCB7bmUkZon3nxtP1LRByt16wiaksZtFOwk/sFrVHMbjYjqMvY5asWF+omTgawM20Ga22ESrV2l5FFsQodqA==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.1.5", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-ipc": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-4.0.1.tgz", + "integrity": "sha512-F93UU9LyY5XIC3pHd2Ah3FO6lAbfkPoPUa9yYHgZhwWteZkeo8mDThDYg90QUBvP7qt22vyVpwVNXpw6Hs/QMg==", + "dev": true, + "optional": true, + "dependencies": { + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-4.0.1.tgz", + "integrity": "sha512-TkNLyCkdZ7bBURbSv4+/AP6K4WjS24vuNFbJSyBDgJfmCQxi2/3hX/l+XT/AqkHj7c8amm4yuOZ6JnIkwIlzaw==", + "dev": true, + "dependencies": { + "isomorphic-ws": "^5.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "ws": "^8.8.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-rpc-methods": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-rpc-methods/-/web3-rpc-methods-1.0.0.tgz", + "integrity": "sha512-s3awsumvzz0pHxPi3oZxA9IK0Ei1lfZnNqkZ9AMhJjKpIXcPuUhUjYxiAsL1Q9pEcn5vGOLfq1RHNUdXrhNOrQ==", + "dev": true, + "dependencies": { + "web3-core": "^4.0.1", + "web3-types": "^1.0.0", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-types/-/web3-types-1.0.0.tgz", + "integrity": "sha512-X6MwXgaZmSCEmqwLnUYVVDn5N3G8RlKStizyy+yOK7qP2VHflM8Pk9ja3VifIXmT1cHgdfLKNBapwAict1X+IA==", + "dev": true, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-4.0.1.tgz", + "integrity": "sha512-q5Pys++MarxUtN/OWrtv7l2kpNBJdDbV13/doO7A2W8I+TqigakKEJQtKiyAIbfnifrIZqyT7+/zzCfPS/sLnw==", + "dev": true, + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-validator": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-validator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-validator/-/web3-validator-1.0.0.tgz", + "integrity": "sha512-WShojVeF7hcaPGzO9vgZukqxd6NWL5A9sIv5uhZzK0mGPvPvc0wqSdKeiwby0cFDH09AW2Q1Qz6knKhXDe7CzA==", + "dev": true, + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "is-my-json-valid": "^2.20.6", + "util": "^0.12.5", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@adraffy/ens-normalize": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", + "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true + }, + "@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true + }, + "@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "dev": true, + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "dev": true, + "requires": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "dev": true, + "requires": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "dev": true + }, + "@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "dev": true, + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "dev": true, + "requires": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + } + }, + "@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "requires": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + } + }, + "@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + } + }, + "@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@noble/curves": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.0.0.tgz", + "integrity": "sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==", + "dev": true, + "requires": { + "@noble/hashes": "1.3.0" + } + }, + "@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "dev": true + }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "dev": true + }, + "@scure/bip32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.0.tgz", + "integrity": "sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==", + "dev": true, + "requires": { + "@noble/curves": "~1.0.0", + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "@scure/bip39": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz", + "integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==", + "dev": true, + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/bn.js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", + "integrity": "sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==", + "requires": { + "@types/node": "*" + } + }, + "@types/elliptic": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", + "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", + "requires": { + "@types/bn.js": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + }, + "@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "requires": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001505", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", + "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dev": true, + "requires": { + "node-fetch": "^2.6.11" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.434", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz", + "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==", + "dev": true + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "ethereum-cryptography": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.0.0.tgz", + "integrity": "sha512-g25m4EtfQGjstWgVE1aIz7XYYjf3kH5kG17ULWVB5dH6uLahsoltOhACzSxyDV+fhn4gbR4xRrOXGe6r2uh4Bg==", + "dev": true, + "requires": { + "@noble/curves": "1.0.0", + "@noble/hashes": "1.3.0", + "@scure/bip32": "1.3.0", + "@scure/bip39": "1.2.0" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "requires": {} + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + } + }, + "jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + } + }, + "jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true + }, + "jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true + }, + "jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "requires": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + } + }, + "jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "ts-jest": { + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", + "integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "web3": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3/-/web3-4.0.1.tgz", + "integrity": "sha512-IVxPbRy3A+RYB2+NYReNPLDXvE2iamTTvx1oNjM4UdbhNt/KQujQusOaRfSpGqfIKBCIYrim1c5LSCFcKlfQhA==", + "dev": true, + "requires": { + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-abi": "^4.0.1", + "web3-eth-accounts": "^4.0.1", + "web3-eth-contract": "^4.0.1", + "web3-eth-ens": "^4.0.1", + "web3-eth-iban": "^4.0.1", + "web3-eth-personal": "^4.0.1", + "web3-net": "^4.0.1", + "web3-providers-http": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-core": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-4.0.1.tgz", + "integrity": "sha512-yGd9FuUhSeLXeSmj+S5YBNdBJfQgBsGGN+uqFRvQKGrKbOp7SXRVNxwTL/JKCLJW2rulcw0JrPD8Ope0A1YvZA==", + "dev": true, + "requires": { + "web3-errors": "^1.0.0", + "web3-eth-iban": "^4.0.1", + "web3-providers-http": "^4.0.1", + "web3-providers-ipc": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-errors/-/web3-errors-1.0.0.tgz", + "integrity": "sha512-UadVmAm7FrWfIglZEbyKxEEeVp4p7SMrx1q1SNbX4Cngmsenth96oH+4GSSFLyDASGyWr/yDSDU2alEUTf0yug==", + "dev": true, + "requires": { + "web3-types": "^1.0.0" + } + }, + "web3-eth": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-4.0.1.tgz", + "integrity": "sha512-5Tm6uusfZlWDby1zc8unoHtph5wpLoBgnRvtkzB3ZCwnQKL4KU2kqO/y4sUTSVrTM30y/CmZagTW9PKyRAt0UA==", + "dev": true, + "requires": { + "setimmediate": "^1.0.5", + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth-abi": "^4.0.1", + "web3-eth-accounts": "^4.0.1", + "web3-net": "^4.0.1", + "web3-providers-ws": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-eth-abi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-4.0.1.tgz", + "integrity": "sha512-l4vS3oxec8A5bO5ognCQQY+ZonPolw77roNVnFdqkmf3MQpUHHovxCn1kFD+eeiT3DpeSt6GbVT9Zt6koA/LHw==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + } + }, + "web3-eth-accounts": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-4.0.1.tgz", + "integrity": "sha512-4SyowjO930H8/Rz6jspYaW2jCbEpqPYKDU/W2WFOHl7KiJ0edoO3mVsupCGAJQCbDG77ijSwMszHj8pA5KhB+A==", + "dev": true, + "requires": { + "@ethereumjs/rlp": "^4.0.1", + "crc-32": "^1.2.2", + "ethereum-cryptography": "^2.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-eth-contract": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-4.0.1.tgz", + "integrity": "sha512-uVVb1ZZre/kwZIDFJBu7y2LdW5BZO3HJwQKhdqLmnyPTLWTnyKE8Mq2pX5eUZzpoSqXlJCoh0GeAQnIblXYsAw==", + "dev": true, + "requires": { + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-abi": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-eth-ens": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-4.0.1.tgz", + "integrity": "sha512-AIPNKs5EyY+w9grIaDkaOxApilBT8Gi7RxJfrVGjR9UnGbHRiH3QX//Y7ZEEkFrhKnZMZ2uim81gyTHP4ujYmg==", + "dev": true, + "requires": { + "@adraffy/ens-normalize": "^1.8.8", + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0", + "web3-eth": "^4.0.1", + "web3-eth-contract": "^4.0.1", + "web3-net": "^4.0.1", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-eth-iban": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-4.0.1.tgz", + "integrity": "sha512-SSwbB2+8+IlF97zk6wwdDv2rPAoIfXAsjLKBCRy6abf4lLFX3M1s80ZLCXISeB3DR72MvO4iqA5/hHlUu9Jlcg==", + "dev": true, + "requires": { + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-eth-personal": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-4.0.1.tgz", + "integrity": "sha512-fv/PiUYNtQhjYanHJ+veT5xp7+l+HUGk2/vklGxwl9ntrzvgdYJ7Z87WXI+dqzYAljyuunsjEVP4N5QPED5n7g==", + "dev": true, + "requires": { + "web3-core": "^4.0.1", + "web3-eth": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "web3-validator": "^1.0.0" + } + }, + "web3-net": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-4.0.1.tgz", + "integrity": "sha512-Fa4NyGyjx/aZwNxdFg1tSkZAQKyEYxfGOFjmsPCzfOC2zaFExv06UPgDEU+aOiY28cs+kTcx5mhjBKn9PLRraw==", + "dev": true, + "requires": { + "web3-core": "^4.0.1", + "web3-rpc-methods": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + } + }, + "web3-providers-http": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-4.0.1.tgz", + "integrity": "sha512-scdCB7bmUkZon3nxtP1LRByt16wiaksZtFOwk/sFrVHMbjYjqMvY5asWF+omTgawM20Ga22ESrV2l5FFsQodqA==", + "dev": true, + "requires": { + "cross-fetch": "^3.1.5", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + } + }, + "web3-providers-ipc": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-4.0.1.tgz", + "integrity": "sha512-F93UU9LyY5XIC3pHd2Ah3FO6lAbfkPoPUa9yYHgZhwWteZkeo8mDThDYg90QUBvP7qt22vyVpwVNXpw6Hs/QMg==", + "dev": true, + "optional": true, + "requires": { + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1" + } + }, + "web3-providers-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-4.0.1.tgz", + "integrity": "sha512-TkNLyCkdZ7bBURbSv4+/AP6K4WjS24vuNFbJSyBDgJfmCQxi2/3hX/l+XT/AqkHj7c8amm4yuOZ6JnIkwIlzaw==", + "dev": true, + "requires": { + "isomorphic-ws": "^5.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-utils": "^4.0.1", + "ws": "^8.8.1" + } + }, + "web3-rpc-methods": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-rpc-methods/-/web3-rpc-methods-1.0.0.tgz", + "integrity": "sha512-s3awsumvzz0pHxPi3oZxA9IK0Ei1lfZnNqkZ9AMhJjKpIXcPuUhUjYxiAsL1Q9pEcn5vGOLfq1RHNUdXrhNOrQ==", + "dev": true, + "requires": { + "web3-core": "^4.0.1", + "web3-types": "^1.0.0", + "web3-validator": "^1.0.0" + } + }, + "web3-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-types/-/web3-types-1.0.0.tgz", + "integrity": "sha512-X6MwXgaZmSCEmqwLnUYVVDn5N3G8RlKStizyy+yOK7qP2VHflM8Pk9ja3VifIXmT1cHgdfLKNBapwAict1X+IA==", + "dev": true + }, + "web3-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-4.0.1.tgz", + "integrity": "sha512-q5Pys++MarxUtN/OWrtv7l2kpNBJdDbV13/doO7A2W8I+TqigakKEJQtKiyAIbfnifrIZqyT7+/zzCfPS/sLnw==", + "dev": true, + "requires": { + "ethereum-cryptography": "^2.0.0", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0", + "web3-validator": "^1.0.0" + } + }, + "web3-validator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web3-validator/-/web3-validator-1.0.0.tgz", + "integrity": "sha512-WShojVeF7hcaPGzO9vgZukqxd6NWL5A9sIv5uhZzK0mGPvPvc0wqSdKeiwby0cFDH09AW2Q1Qz6knKhXDe7CzA==", + "dev": true, + "requires": { + "ethereum-cryptography": "^2.0.0", + "is-my-json-valid": "^2.20.6", + "util": "^0.12.5", + "web3-errors": "^1.0.0", + "web3-types": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/sdk/js-query/package.json b/sdk/js-query/package.json new file mode 100644 index 0000000000..b1697e58f5 --- /dev/null +++ b/sdk/js-query/package.json @@ -0,0 +1,37 @@ +{ + "name": "@wormhole-foundation/wormhole-query-sdk", + "version": "0.0.1", + "description": "Wormhole cross-chain query SDK", + "homepage": "https://wormhole.com", + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "files": [ + "lib/" + ], + "scripts": { + "test": "jest --verbose", + "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json" + }, + "keywords": [ + "wormhole", + "sdk", + "cross-chain", + "query" + ], + "author": "Wormhole Foundation", + "license": "Apache-2.0", + "devDependencies": { + "axios": "^1.4.0", + "jest": "^29.5.0", + "prettier": "^2.3.2", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.1.3", + "web3": "^4.0.1" + }, + "sideEffects": false, + "dependencies": { + "@types/elliptic": "^6.4.14", + "elliptic": "^6.5.4" + } +} diff --git a/sdk/js-query/src/index.ts b/sdk/js-query/src/index.ts new file mode 100644 index 0000000000..55e90a3ef7 --- /dev/null +++ b/sdk/js-query/src/index.ts @@ -0,0 +1 @@ +export * from "./query"; diff --git a/sdk/js-query/src/query/BinaryWriter.ts b/sdk/js-query/src/query/BinaryWriter.ts new file mode 100644 index 0000000000..39db2fbf45 --- /dev/null +++ b/sdk/js-query/src/query/BinaryWriter.ts @@ -0,0 +1,54 @@ +// BinaryWriter appends data to the end of a buffer, resizing the buffer as needed +// Numbers are encoded as big endian +export class BinaryWriter { + private _buffer: Buffer; + private _offset: number; + + constructor(initialSize: number = 1024) { + this._buffer = Buffer.alloc(initialSize); + this._offset = 0; + } + + // Ensure the buffer has the capacity to write `size` bytes, otherwise allocate more memory + _ensure(size: number) { + const remaining = this._buffer.length - this._offset; + if (remaining < size) { + const oldBuffer = this._buffer; + const newSize = this._buffer.length * 2 + size; + this._buffer = Buffer.alloc(newSize); + oldBuffer.copy(this._buffer); + } + } + + writeUint8(value: number) { + this._ensure(1); + this._buffer.writeUint8(value, this._offset); + this._offset += 1; + return this; + } + + writeUint16(value: number) { + this._ensure(2); + this._offset = this._buffer.writeUint16BE(value, this._offset); + return this; + } + + writeUint32(value: number) { + this._ensure(4); + this._offset = this._buffer.writeUint32BE(value, this._offset); + return this; + } + + writeUint8Array(value: Uint8Array) { + this._ensure(value.length); + this._buffer.set(value, this._offset); + this._offset += value.length; + return this; + } + + data(): Uint8Array { + const copy = new Uint8Array(this._offset); + copy.set(this._buffer.subarray(0, this._offset)); + return copy; + } +} diff --git a/sdk/js-query/src/query/consts.ts b/sdk/js-query/src/query/consts.ts new file mode 100644 index 0000000000..c6d6831db1 --- /dev/null +++ b/sdk/js-query/src/query/consts.ts @@ -0,0 +1 @@ +export type Network = "MAINNET" | "TESTNET" | "DEVNET"; diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts new file mode 100644 index 0000000000..e95060694e --- /dev/null +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -0,0 +1,115 @@ +import { + afterAll, + beforeAll, + describe, + expect, + jest, + test, +} from "@jest/globals"; +import Web3, { ETH_DATA_FORMAT } from "web3"; +import axios from "axios"; +import * as elliptic from "elliptic"; +import { + EthCallData, + EthCallQueryRequest, + PerChainQueryRequest, + QueryRequest, + QueryResponse, + sign, +} from ".."; + +jest.setTimeout(60000); + +const CI = false; +const ETH_NODE_URL = CI ? "ws://eth-devnet:8545" : "ws://localhost:8545"; + +const QUERY_SERVER_URL = "http://localhost:6069/v1/query"; +const PRIVATE_KEY = + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; +const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"; + +let web3: Web3; + +beforeAll(() => { + web3 = new Web3(ETH_NODE_URL); +}); + +afterAll(() => { + web3.provider?.disconnect(); +}); + +function createTestEthCallData( + to: string, + name: string, + outputType: string +): EthCallData { + return { + to, + data: web3.eth.abi.encodeFunctionCall( + { + constant: true, + inputs: [], + name, + outputs: [{ name, type: outputType }], + payable: false, + stateMutability: "view", + type: "function", + }, + [] + ), + }; +} + +describe("eth call", () => { + test("serialize request", () => { + const toAddress = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270"; + const nameCallData = createTestEthCallData(toAddress, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + toAddress, + "totalSupply", + "uint256" + ); + const ethCall = new EthCallQueryRequest("0x28d9630", [ + nameCallData, + totalSupplyCallData, + ]); + const chainId = 5; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + expect(Buffer.from(serialized).toString("hex")).toEqual( + "0100000001010005010000004600000009307832386439363330020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd" + ); + }); + test("parse response", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + WETH_ADDRESS, + "totalSupply", + "uint256" + ); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [ + nameCallData, + totalSupplyCallData, + ]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const digest = QueryRequest.digest("DEVNET", serialized); + const signature = sign(PRIVATE_KEY, digest); + const response = await axios.put(QUERY_SERVER_URL, { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }); + expect(response.status).toBe(200); + const queryResponse = QueryResponse.fromBytes( + Buffer.from(response.data.bytes, "hex") + ); + // TODO: verify signatures + // TOOD: verify query response + }); +}); diff --git a/sdk/js-query/src/query/ethCall.ts b/sdk/js-query/src/query/ethCall.ts new file mode 100644 index 0000000000..c410b956a4 --- /dev/null +++ b/sdk/js-query/src/query/ethCall.ts @@ -0,0 +1,47 @@ +import { BinaryWriter } from "./BinaryWriter"; +import { ChainQueryType, ChainSpecificQuery } from "./request"; +import { ChainSpecificResponse } from "./response"; +import { hexToUint8Array } from "./utils"; + +export interface EthCallData { + to: string; + data: string; +} + +export class EthCallQueryRequest implements ChainSpecificQuery { + constructor(public blockId: string, public callData: EthCallData[]) {} + + type(): ChainQueryType { + return ChainQueryType.EthCall; + } + + serialize(): Uint8Array { + const writer = new BinaryWriter() + .writeUint32(this.blockId.length) + .writeUint8Array(Buffer.from(this.blockId)) + .writeUint8(this.callData.length); + this.callData.forEach(({ to, data }) => { + const dataArray = hexToUint8Array(data); + writer + .writeUint8Array(hexToUint8Array(to)) + .writeUint32(dataArray.length) + .writeUint8Array(dataArray); + }); + return writer.data(); + } +} + +export class EthCallQueryResponse implements ChainSpecificResponse { + constructor( + public blockNumber: number, + public hash: string, + public time: string, + public results: string[][] + ) {} + + type(): ChainQueryType { + return ChainQueryType.EthCall; + } + + // static fromBytes(bytes: Uint8Array): EthCallQueryResponse {} +} diff --git a/sdk/js-query/src/query/index.ts b/sdk/js-query/src/query/index.ts new file mode 100644 index 0000000000..3d29f3bcea --- /dev/null +++ b/sdk/js-query/src/query/index.ts @@ -0,0 +1,5 @@ +export * from "./request"; +export * from "./response"; +export * from "./utils"; +export * from "./ethCall"; +export * from "./consts"; diff --git a/sdk/js-query/src/query/request.ts b/sdk/js-query/src/query/request.ts new file mode 100644 index 0000000000..df0c51f682 --- /dev/null +++ b/sdk/js-query/src/query/request.ts @@ -0,0 +1,70 @@ +import { BinaryWriter } from "./BinaryWriter"; +import { Network } from "./consts"; +import { utils } from "web3"; +import { hexToUint8Array } from "./utils"; + +export const MAINNET_QUERY_REQUEST_PREFIX = + "mainnet_query_request_000000000000|"; + +export const TESTNET_QUERY_REQUEST_PREFIX = + "testnet_query_request_000000000000|"; + +export const DEVNET_QUERY_REQUEST_PREFIX = + "devnet_query_request_0000000000000|"; + +export function getPrefix(network: Network) { + return network === "MAINNET" + ? MAINNET_QUERY_REQUEST_PREFIX + : network === "TESTNET" + ? TESTNET_QUERY_REQUEST_PREFIX + : DEVNET_QUERY_REQUEST_PREFIX; +} + +export class QueryRequest { + constructor( + public nonce: number, + public requests: PerChainQueryRequest[] = [], + public version: number = 1 + ) {} + + serialize(): Uint8Array { + const writer = new BinaryWriter() + .writeUint8(this.version) + .writeUint32(this.nonce) + .writeUint8(this.requests.length); + this.requests.forEach((request) => + writer.writeUint8Array(request.serialize()) + ); + return writer.data(); + } + + static digest(network: Network, bytes: Uint8Array): Uint8Array { + const prefix = getPrefix(network); + const data = Buffer.concat([Buffer.from(prefix), bytes]); + return hexToUint8Array(utils.keccak256(data).slice(2)); + } +} + +export class PerChainQueryRequest { + constructor(public chainId: number, public query: ChainSpecificQuery) {} + + serialize(): Uint8Array { + const writer = new BinaryWriter() + .writeUint16(this.chainId) + .writeUint8(this.query.type()); + const queryData = this.query.serialize(); + return writer + .writeUint32(queryData.length) + .writeUint8Array(queryData) + .data(); + } +} + +export interface ChainSpecificQuery { + type(): ChainQueryType; + serialize(): Uint8Array; +} + +export enum ChainQueryType { + EthCall = 1, +} diff --git a/sdk/js-query/src/query/response.ts b/sdk/js-query/src/query/response.ts new file mode 100644 index 0000000000..99cbd670f3 --- /dev/null +++ b/sdk/js-query/src/query/response.ts @@ -0,0 +1,36 @@ +import { ChainQueryType } from "./request"; + +// TODO: implement query response parsing + +export class QueryResponse { + signatures: string[] = []; + bytes: string = ""; + + // constructor(signatures: string[], bytes: string) { + + // } + + static fromBytes(bytes: Uint8Array): Uint8Array { + //const reader: Reader = { + // buffer: Buffer.from(bytes), + // i: 0, + //}; + //// Request + //const requestChain = reader.buffer.readUint16BE(reader.i); + //reader.i += 2; + //const signature = buffer.toString("hex", offset, offset + 65); + //offset += 65; + //const request = null; + //// Response + //const numPerChainResponses = buffer.readUint8(offset); + return new Uint8Array(); + } +} + +export class PerChainQueryResponse { + constructor(public chainId: number, public response: ChainSpecificResponse) {} +} + +export interface ChainSpecificResponse { + type(): ChainQueryType; +} diff --git a/sdk/js-query/src/query/utils.ts b/sdk/js-query/src/query/utils.ts new file mode 100644 index 0000000000..663058ad59 --- /dev/null +++ b/sdk/js-query/src/query/utils.ts @@ -0,0 +1,33 @@ +import * as elliptic from "elliptic"; + +export function isValidHexString(s: string) { + if (s.length % 2 !== 0) { + throw new Error("hex string length must be even"); + } + return /^(0x)?[0-9a-fA-F]+$/.test(s); +} + +export function hexToUint8Array(s: string): Uint8Array { + if (!isValidHexString(s)) { + throw new Error(`${s} is not hex`); + } + return new Uint8Array( + Buffer.from(s.startsWith("0x") ? s.slice(2) : s, "hex") + ); +} + +/** + * @param key Private key used to sign `data` + * @param data Data for signing + * @returns ECDSA secp256k1 signature + */ +export function sign(key: string, data: Uint8Array): string { + const ec = new elliptic.ec("secp256k1"); + const keyPair = ec.keyFromPrivate(key); + const signature = keyPair.sign(data, { canonical: true }); + const packed = + signature.r.toString("hex").padStart(64, "0") + + signature.s.toString("hex").padStart(64, "0") + + Buffer.from([signature.recoveryParam ?? 0]).toString("hex"); + return packed; +} diff --git a/sdk/js-query/tsconfig-cjs.json b/sdk/js-query/tsconfig-cjs.json new file mode 100644 index 0000000000..945c51f27a --- /dev/null +++ b/sdk/js-query/tsconfig-cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./lib/cjs" + } +} diff --git a/sdk/js-query/tsconfig.json b/sdk/js-query/tsconfig.json new file mode 100644 index 0000000000..0701709c85 --- /dev/null +++ b/sdk/js-query/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "outDir": "./lib/esm", + "strict": true, + "esModuleInterop": true, + "downlevelIteration": true, + "allowJs": true, + "lib": ["dom", "es5", "scripthost", "es2020.bigint"] + }, + "include": ["src"], + "exclude": ["node_modules", "**/*.test.*", "**/__tests__/*"] +} From 31a75553b01265387f1a750ff7f1e826069a9a7c Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 1 Sep 2023 15:06:51 -0500 Subject: [PATCH 26/37] CCQ: Gossip split (#3320) * CCQ: Gossip Split * Fix error handling --- devnet/node.yaml | 1 + devnet/query-server.yaml | 4 +- node/cmd/ccq/p2p.go | 46 +++-- node/cmd/ccq/query_server.go | 53 ++++-- node/cmd/guardiand/node.go | 9 +- node/cmd/spy/spy.go | 4 +- node/hack/query/querier.key | Bin 0 -> 68 bytes node/hack/query/send_req.go | 47 +++-- node/hack/query/test/query_test.go | 33 ++-- node/pkg/node/node_test.go | 2 +- node/pkg/node/options.go | 5 +- node/pkg/p2p/ccq_p2p.go | 296 +++++++++++++++++++++++++++++ node/pkg/p2p/p2p.go | 73 +++---- node/pkg/p2p/watermark_test.go | 3 + 14 files changed, 464 insertions(+), 112 deletions(-) create mode 100644 node/hack/query/querier.key create mode 100644 node/pkg/p2p/ccq_p2p.go diff --git a/devnet/node.yaml b/devnet/node.yaml index a2f797c8c3..35dcabbd97 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -163,6 +163,7 @@ spec: # - --chainGovernorEnabled=true - --ccqEnabled=true - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe + - --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH # - --logLevel=debug securityContext: capabilities: diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml index 90a62214da..f5df1bdb40 100644 --- a/devnet/query-server.yaml +++ b/devnet/query-server.yaml @@ -34,7 +34,7 @@ spec: - /guardiand - query-server - --nodeKey - - /tmp/node.key + - node/hack/query/querier.key - --listenAddr - "[::]:6069" - --ethRPC @@ -43,7 +43,7 @@ spec: - "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" # Hardcoded devnet bootstrap (generated from deterministic key in guardiand) - --bootstrap - - /dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw + - /dns4/guardian-0.guardian/udp/8996/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw - --logLevel=info ports: - containerPort: 6069 diff --git a/node/cmd/ccq/p2p.go b/node/cmd/ccq/p2p.go index 1dbb5f95bb..d890fc22cf 100644 --- a/node/cmd/ccq/p2p.go +++ b/node/cmd/ccq/p2p.go @@ -40,9 +40,10 @@ type SignedResponse struct { } type P2PSub struct { - sub *pubsub.Subscription - topic *pubsub.Topic - host host.Host + sub *pubsub.Subscription + topic_req *pubsub.Topic + topic_resp *pubsub.Topic + host host.Host } func runP2P(ctx context.Context, priv crypto.PrivKey, port uint, networkID, bootstrap, ethRpcUrl, ethCoreAddr string, pendingResponses *PendingResponses, logger *zap.Logger) (*P2PSub, error) { @@ -105,23 +106,39 @@ func runP2P(ctx context.Context, priv crypto.PrivKey, port uint, networkID, boot return nil, err } - topicName := fmt.Sprintf("%s/%s", networkID, "broadcast") + topic_req := fmt.Sprintf("%s/%s", networkID, "ccq_req") + topic_resp := fmt.Sprintf("%s/%s", networkID, "ccq_resp") - logger.Info("Subscribing pubsub topic", zap.String("topic", topicName)) - ps, err := pubsub.NewGossipSub(ctx, h) + logger.Info("Subscribing pubsub topic", zap.String("topic_req", topic_req), zap.String("topic_resp", topic_resp)) + + // Comment from security team in PR #2981: CCQServers should have a parameter of D = 36, Dlo = 19, Dhi = 40, Dout = 18 such that they can reach all Guardians directly. + gossipParams := pubsub.DefaultGossipSubParams() + gossipParams.D = 36 + gossipParams.Dlo = 19 + gossipParams.Dhi = 40 + gossipParams.Dout = 18 + + ps, err := pubsub.NewGossipSub(ctx, h, pubsub.WithGossipSubParams(gossipParams)) + if err != nil { + logger.Error("failed to create gossip subscription", zap.Error(err)) + return nil, err + } + + th_req, err := ps.Join(topic_req) if err != nil { + logger.Error("failed to join request topic", zap.String("topic_req", topic_req), zap.Error(err)) return nil, err } - topic, err := ps.Join(topicName) + th_resp, err := ps.Join(topic_resp) if err != nil { - logger.Error("failed to join topic", zap.Error(err)) + logger.Error("failed to join response topic", zap.String("topic_resp", topic_resp), zap.Error(err)) return nil, err } - sub, err := topic.Subscribe() + sub, err := th_resp.Subscribe() if err != nil { - logger.Error("failed to subscribe topic", zap.Error(err)) + logger.Error("failed to subscribe to response topic", zap.Error(err)) return nil, err } @@ -129,7 +146,7 @@ func runP2P(ctx context.Context, priv crypto.PrivKey, port uint, networkID, boot zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) // Wait for peers - for len(topic.ListPeers()) < 1 { + for len(th_req.ListPeers()) < 1 { time.Sleep(time.Millisecond * 100) } @@ -230,8 +247,9 @@ func runP2P(ctx context.Context, priv crypto.PrivKey, port uint, networkID, boot }() return &P2PSub{ - sub: sub, - topic: topic, - host: h, + sub: sub, + topic_req: th_req, + topic_resp: th_resp, + host: h, }, nil } diff --git a/node/cmd/ccq/query_server.go b/node/cmd/ccq/query_server.go index 073a73a585..de26c27856 100644 --- a/node/cmd/ccq/query_server.go +++ b/node/cmd/ccq/query_server.go @@ -7,6 +7,8 @@ import ( "os" "github.com/certusone/wormhole/node/pkg/common" + "github.com/certusone/wormhole/node/pkg/telemetry" + "github.com/certusone/wormhole/node/pkg/version" ipfslog "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/crypto" "github.com/spf13/cobra" @@ -14,25 +16,29 @@ import ( ) var ( - p2pNetworkID *string - p2pPort *uint - p2pBootstrap *string - listenAddr *string - nodeKeyPath *string - ethRPC *string - ethContract *string - logLevel *string + p2pNetworkID *string + p2pPort *uint + p2pBootstrap *string + listenAddr *string + nodeKeyPath *string + ethRPC *string + ethContract *string + logLevel *string + telemetryLokiURL *string + telemetryNodeName *string ) func init() { p2pNetworkID = QueryServerCmd.Flags().String("network", "/wormhole/dev", "P2P network identifier") - p2pPort = QueryServerCmd.Flags().Uint("port", 8999, "P2P UDP listener port") + p2pPort = QueryServerCmd.Flags().Uint("port", 8996, "P2P UDP listener port") p2pBootstrap = QueryServerCmd.Flags().String("bootstrap", "", "P2P bootstrap peers (comma-separated)") listenAddr = QueryServerCmd.Flags().String("listenAddr", "[::]:6069", "Listen address for query server (disabled if blank)") nodeKeyPath = QueryServerCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)") ethRPC = QueryServerCmd.Flags().String("ethRPC", "", "Ethereum RPC for fetching current guardian set") ethContract = QueryServerCmd.Flags().String("ethContract", "", "Ethereum core bridge address for fetching current guardian set") logLevel = QueryServerCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)") + telemetryLokiURL = QueryServerCmd.Flags().String("telemetryLokiURL", "", "Loki cloud logging URL") + telemetryNodeName = QueryServerCmd.Flags().String("telemetryNodeName", "", "Node name used in telemetry") } var QueryServerCmd = &cobra.Command{ @@ -54,6 +60,26 @@ func runQueryServer(cmd *cobra.Command, args []string) { logger := ipfslog.Logger("query-server").Desugar() ipfslog.SetAllLoggers(lvl) + if *telemetryLokiURL != "" { + logger.Info("Using Loki telemetry logger") + if *telemetryNodeName == "" { + logger.Fatal("if --telemetryLokiURL is specified --telemetryNodeName must be specified") + } + labels := map[string]string{ + "network": *p2pNetworkID, + "node_name": *telemetryNodeName, + "version": version.Version(), + } + + tm, err := telemetry.NewLokiCloudLogger(context.Background(), logger, *telemetryLokiURL, "ccq_server", true, labels) + if err != nil { + logger.Fatal("Failed to initialize telemetry", zap.Error(err)) + } + + defer tm.Close() + logger = tm.WrapLogger(logger) // Wrap logger with telemetry logger + } + // Verify flags if *nodeKeyPath == "" { logger.Fatal("Please specify --nodeKey") @@ -87,7 +113,7 @@ func runQueryServer(cmd *cobra.Command, args []string) { // Start the HTTP server go func() { - s := NewHTTPServer(*listenAddr, p2p.topic, pendingResponses) + s := NewHTTPServer(*listenAddr, p2p.topic_req, pendingResponses) logger.Sugar().Infof("Server listening on %s", *listenAddr) err := s.ListenAndServe() if err != nil && err != http.ErrServerClosed { @@ -101,8 +127,11 @@ func runQueryServer(cmd *cobra.Command, args []string) { // Cleanly shutdown // Without this the same host won't properly discover peers until some timeout p2p.sub.Cancel() - if err := p2p.topic.Close(); err != nil { - logger.Error("Error closing the topic", zap.Error(err)) + if err := p2p.topic_req.Close(); err != nil { + logger.Error("Error closing the request topic", zap.Error(err)) + } + if err := p2p.topic_resp.Close(); err != nil { + logger.Error("Error closing the response topic", zap.Error(err)) } if err := p2p.host.Close(); err != nil { logger.Error("Error closing the host", zap.Error(err)) diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index d610f58c78..80c232bba6 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -207,6 +207,9 @@ var ( ccqEnabled *bool ccqAllowedRequesters *string + ccqP2pPort *uint + ccqP2pBootstrap *string + ccqAllowedPeers *string gatewayRelayerContract *string gatewayRelayerKeyPath *string @@ -375,6 +378,9 @@ func init() { ccqEnabled = NodeCmd.Flags().Bool("ccqEnabled", false, "Enable cross chain query support") ccqAllowedRequesters = NodeCmd.Flags().String("ccqAllowedRequesters", "", "Comma separated list of signers allowed to submit cross chain queries") + ccqP2pPort = NodeCmd.Flags().Uint("ccqP2pPort", 8996, "CCQ P2P UDP listener port") + ccqP2pBootstrap = NodeCmd.Flags().String("ccqP2pBootstrap", "", "CCQ P2P bootstrap peers (comma-separated)") + ccqAllowedPeers = NodeCmd.Flags().String("ccqAllowedPeers", "", "CCQ allowed P2P peers (comma-separated)") gatewayRelayerContract = NodeCmd.Flags().String("gatewayRelayerContract", "", "Address of the smart contract on wormchain to receive relayed VAAs") gatewayRelayerKeyPath = NodeCmd.Flags().String("gatewayRelayerKeyPath", "", "Path to gateway relayer private key for signing transactions") @@ -475,6 +481,7 @@ func runNode(cmd *cobra.Command, args []string) { // Use the first guardian node as bootstrap *p2pBootstrap = fmt.Sprintf("/dns4/guardian-0.guardian/udp/%d/quic/p2p/%s", *p2pPort, g0key.String()) + *ccqP2pBootstrap = fmt.Sprintf("/dns4/guardian-0.guardian/udp/%d/quic/p2p/%s", *ccqP2pPort, g0key.String()) // Deterministic ganache ETH devnet address. *ethContract = unsafeDevModeEvmContractAddress(*ethContract) @@ -1430,7 +1437,7 @@ func runNode(cmd *cobra.Command, args []string) { node.GuardianOptionGatewayRelayer(*gatewayRelayerContract, gatewayRelayerWormchainConn), node.GuardianOptionQueryHandler(*ccqEnabled, *ccqAllowedRequesters), node.GuardianOptionAdminService(*adminSocketPath, ethRPC, ethContract, rpcMap), - node.GuardianOptionP2P(p2pKey, *p2pNetworkID, *p2pBootstrap, *nodeName, *disableHeartbeatVerify, *p2pPort, ibc.GetFeatures), + node.GuardianOptionP2P(p2pKey, *p2pNetworkID, *p2pBootstrap, *nodeName, *disableHeartbeatVerify, *p2pPort, *ccqP2pBootstrap, *ccqP2pPort, *ccqAllowedPeers, ibc.GetFeatures), node.GuardianOptionStatusServer(*statusAddr), node.GuardianOptionProcessor(), } diff --git a/node/cmd/spy/spy.go b/node/cmd/spy/spy.go index c5b24492c1..49f4c09cbe 100644 --- a/node/cmd/spy/spy.go +++ b/node/cmd/spy/spy.go @@ -359,7 +359,9 @@ func runSpy(cmd *cobra.Command, args []string) { false, // ccqEnabled nil, // query requests nil, // query responses - + "", // query bootstrap peers + 0, // query port + "", // query allow list )); err != nil { return err } diff --git a/node/hack/query/querier.key b/node/hack/query/querier.key new file mode 100644 index 0000000000000000000000000000000000000000..84b06e813e88687fae3014e52c21d4722b77af42 GIT binary patch literal 68 zcmV-K0K5MP0TMuVwek_mfVk6X)=75&8_M@>gB{Bu&wN&8;<_$&lWF_>EEZlyA&!#NNKu|Rjvr6} literal 0 HcmV?d00001 diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 08518005fb..6ecb0dc5f4 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -62,8 +62,8 @@ func main() { p2pNetworkID := "/wormhole/dev" var p2pPort uint = 8998 // don't collide with spy so we can run from the same container in tilt - p2pBootstrap := "/dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" - nodeKeyPath := "/tmp/querier.key" // don't use node key so we get a new address + p2pBootstrap := "/dns4/guardian-0.guardian/udp/8996/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" + nodeKeyPath := "./querier.key" ctx := context.Background() logger, _ := zap.NewDevelopment() @@ -146,29 +146,34 @@ func main() { panic(err) } - topic := fmt.Sprintf("%s/%s", networkID, "broadcast") + topic_req := fmt.Sprintf("%s/%s", networkID, "ccq_req") + topic_resp := fmt.Sprintf("%s/%s", networkID, "ccq_resp") - logger.Info("Subscribing pubsub topic", zap.String("topic", topic)) + logger.Info("Subscribing pubsub topic", zap.String("topic_req", topic_req), zap.String("topic_resp", topic_resp)) ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { panic(err) } - th, err := ps.Join(topic) + th_req, err := ps.Join(topic_req) if err != nil { - logger.Panic("failed to join topic", zap.Error(err)) + logger.Panic("failed to join request topic", zap.String("topic_req", topic_req), zap.Error(err)) } - sub, err := th.Subscribe() + th_resp, err := ps.Join(topic_resp) if err != nil { - logger.Panic("failed to subscribe topic", zap.Error(err)) + logger.Panic("failed to join response topic", zap.String("topic_resp", topic_resp), zap.Error(err)) + } + + sub, err := th_resp.Subscribe() + if err != nil { + logger.Panic("failed to subscribe to response topic", zap.Error(err)) } logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) - // Wait for peers - for len(th.ListPeers()) < 1 { + for len(th_req.ListPeers()) < 1 { time.Sleep(time.Millisecond * 100) } @@ -183,7 +188,7 @@ func main() { methods := []string{"name", "totalSupply"} callData := []*query.EthCallData{} - to, _ := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270") + to, _ := hex.DecodeString("DDb64fE46a91D46ee29420539FC25FD07c5FEa3E") for _, method := range methods { data, err := wethAbi.Pack(method) @@ -198,7 +203,8 @@ func main() { } // Fetch the latest block number - url := "https://rpc.ankr.com/polygon" + //url := "https://localhost:8545" + url := "http://eth-devnet:8545" logger.Info("Querying for latest block height", zap.String("url", url)) blockNum, err := utils.FetchLatestBlockNumberFromUrl(ctx, url) if err != nil { @@ -221,7 +227,7 @@ func main() { // First request... logger.Info("calling sendQueryAndGetRsp for ", zap.String("blockNum", blockNum.String())) queryRequest := createQueryRequest(callRequest) - sendQueryAndGetRsp(queryRequest, sk, th, ctx, logger, sub, wethAbi, methods) + sendQueryAndGetRsp(queryRequest, sk, th_req, ctx, logger, sub, wethAbi, methods) // This is just so that when I look at the output, it is easier for me. (Paul) logger.Info("sleeping for 5 seconds") @@ -235,20 +241,23 @@ func main() { } queryRequest2 := createQueryRequest(callRequest2) logger.Info("calling sendQueryAndGetRsp for ", zap.String("blockNum", blockNum.String())) - sendQueryAndGetRsp(queryRequest2, sk, th, ctx, logger, sub, wethAbi, methods) + sendQueryAndGetRsp(queryRequest2, sk, th_req, ctx, logger, sub, wethAbi, methods) // Now, want to send a single query with multiple requests... logger.Info("Starting multiquery test in 5...") time.Sleep(time.Second * 5) multiCallRequest := []*query.EthCallQueryRequest{callRequest, callRequest2} multQueryRequest := createQueryRequestWithMultipleRequests(multiCallRequest) - sendQueryAndGetRsp(multQueryRequest, sk, th, ctx, logger, sub, wethAbi, methods) + sendQueryAndGetRsp(multQueryRequest, sk, th_req, ctx, logger, sub, wethAbi, methods) // Cleanly shutdown // Without this the same host won't properly discover peers until some timeout sub.Cancel() - if err := th.Close(); err != nil { - logger.Fatal("Error closing the topic", zap.Error(err)) + if err := th_req.Close(); err != nil { + logger.Fatal("Error closing the request topic", zap.Error(err)) + } + if err := th_resp.Close(); err != nil { + logger.Fatal("Error closing the response topic", zap.Error(err)) } if err := h.Close(); err != nil { logger.Fatal("Error closing the host", zap.Error(err)) @@ -270,7 +279,7 @@ func createQueryRequest(callRequest *query.EthCallQueryRequest) *query.QueryRequ Nonce: rand.Uint32(), PerChainQueries: []*query.PerChainQueryRequest{ { - ChainId: 5, + ChainId: 2, Query: callRequest, }, }, @@ -282,7 +291,7 @@ func createQueryRequestWithMultipleRequests(callRequests []*query.EthCallQueryRe perChainQueries := []*query.PerChainQueryRequest{} for _, req := range callRequests { perChainQueries = append(perChainQueries, &query.PerChainQueryRequest{ - ChainId: 5, + ChainId: 2, Query: req, }) } diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index e115743ce6..7789ea52f9 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -48,8 +48,8 @@ func TestCrossChainQuery(t *testing.T) { p2pNetworkID := "/wormhole/dev" var p2pPort uint = 8997 - p2pBootstrap := "/dns4/guardian-0.guardian/udp/8999/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" - nodeKeyPath := "/tmp/querier.key" + p2pBootstrap := "/dns4/guardian-0.guardian/udp/8996/quic/p2p/12D3KooWL3XJ9EMCyZvmmGXL2LMiVBtrVa2BuESsJiXkSj7333Jw" + nodeKeyPath := "../querier.key" ctx := context.Background() logger, _ := zap.NewDevelopment() @@ -148,29 +148,35 @@ func TestCrossChainQuery(t *testing.T) { panic(err) } - topic := fmt.Sprintf("%s/%s", networkID, "broadcast") + topic_req := fmt.Sprintf("%s/%s", networkID, "ccq_req") + topic_resp := fmt.Sprintf("%s/%s", networkID, "ccq_resp") - logger.Info("Subscribing pubsub topic", zap.String("topic", topic)) + logger.Info("Subscribing pubsub topic", zap.String("topic_req", topic_req), zap.String("topic_resp", topic_resp)) ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { panic(err) } - th, err := ps.Join(topic) + th_req, err := ps.Join(topic_req) if err != nil { - logger.Panic("failed to join topic", zap.Error(err)) + logger.Panic("failed to join request topic", zap.String("topic_req", topic_req), zap.Error(err)) } - sub, err := th.Subscribe() + th_resp, err := ps.Join(topic_resp) if err != nil { - logger.Panic("failed to subscribe topic", zap.Error(err)) + logger.Panic("failed to join response topic", zap.String("topic_resp", topic_resp), zap.Error(err)) + } + + sub, err := th_resp.Subscribe() + if err != nil { + logger.Panic("failed to subscribe to response topic", zap.Error(err)) } logger.Info("Node has been started", zap.String("peer_id", h.ID().String()), zap.String("addrs", fmt.Sprintf("%v", h.Addrs()))) // Wait for peers - for len(th.ListPeers()) < 1 { + for len(th_req.ListPeers()) < 1 { time.Sleep(time.Millisecond * 100) } @@ -236,7 +242,7 @@ func TestCrossChainQuery(t *testing.T) { panic(err) } - err = th.Publish(ctx, b) + err = th_req.Publish(ctx, b) if err != nil { panic(err) } @@ -336,8 +342,11 @@ func TestCrossChainQuery(t *testing.T) { // Cleanly shutdown // Without this the same host won't properly discover peers until some timeout sub.Cancel() - if err := th.Close(); err != nil { - logger.Error("Error closing the topic", zap.Error(err)) + if err := th_req.Close(); err != nil { + logger.Fatal("Error closing the request topic", zap.Error(err)) + } + if err := th_resp.Close(); err != nil { + logger.Fatal("Error closing the response topic", zap.Error(err)) } if err := h.Close(); err != nil { logger.Error("Error closing the host", zap.Error(err)) diff --git a/node/pkg/node/node_test.go b/node/pkg/node/node_test.go index 3c89e54a55..2d12c398ca 100644 --- a/node/pkg/node/node_test.go +++ b/node/pkg/node/node_test.go @@ -190,7 +190,7 @@ func mockGuardianRunnable(t testing.TB, gs []*mockGuardian, mockGuardianIndex ui GuardianOptionNoAccountant(), // disable accountant GuardianOptionGovernor(true), GuardianOptionGatewayRelayer("", nil), // disable gateway relayer - GuardianOptionP2P(gs[mockGuardianIndex].p2pKey, networkID, bootstrapPeers, nodeName, false, cfg.p2pPort, func() string { return "" }), + GuardianOptionP2P(gs[mockGuardianIndex].p2pKey, networkID, bootstrapPeers, nodeName, false, cfg.p2pPort, "", 0, "", func() string { return "" }), GuardianOptionPublicRpcSocket(cfg.publicSocket, publicRpcLogDetail), GuardianOptionPublicrpcTcpService(cfg.publicRpc, publicRpcLogDetail), GuardianOptionPublicWeb(cfg.publicWeb, cfg.publicSocket, "", false, ""), diff --git a/node/pkg/node/options.go b/node/pkg/node/options.go index db6c8c0d2d..af701bc39b 100644 --- a/node/pkg/node/options.go +++ b/node/pkg/node/options.go @@ -39,7 +39,7 @@ type GuardianOption struct { // GuardianOptionP2P configures p2p networking. // Dependencies: Accountant, Governor -func GuardianOptionP2P(p2pKey libp2p_crypto.PrivKey, networkId string, bootstrapPeers string, nodeName string, disableHeartbeatVerify bool, port uint, ibcFeaturesFunc func() string) *GuardianOption { +func GuardianOptionP2P(p2pKey libp2p_crypto.PrivKey, networkId string, bootstrapPeers string, nodeName string, disableHeartbeatVerify bool, port uint, ccqBootstrapPeers string, ccqPort uint, ccqAllowedPeers string, ibcFeaturesFunc func() string) *GuardianOption { return &GuardianOption{ name: "p2p", dependencies: []string{"accountant", "governor", "gateway-relayer"}, @@ -76,6 +76,9 @@ func GuardianOptionP2P(p2pKey libp2p_crypto.PrivKey, networkId string, bootstrap (g.queryHandler != nil), g.signedQueryReqC.writeC, g.queryResponsePublicationC.readC, + ccqBootstrapPeers, + ccqPort, + ccqAllowedPeers, ) return nil diff --git a/node/pkg/p2p/ccq_p2p.go b/node/pkg/p2p/ccq_p2p.go new file mode 100644 index 0000000000..8b0e8e0fb3 --- /dev/null +++ b/node/pkg/p2p/ccq_p2p.go @@ -0,0 +1,296 @@ +package p2p + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "strings" + + "github.com/certusone/wormhole/node/pkg/common" + "github.com/certusone/wormhole/node/pkg/query" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/protobuf/proto" + + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + + "github.com/libp2p/go-libp2p" + dht "github.com/libp2p/go-libp2p-kad-dht" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/libp2p/go-libp2p/core/routing" + libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" + libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "go.uber.org/zap" +) + +var ( + ccqP2pMessagesSent = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "wormhole_ccqp2p_broadcast_messages_sent_total", + Help: "Total number of ccq p2p pubsub broadcast messages sent", + }) + ccqP2pMessagesReceived = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "wormhole_ccqp2p_broadcast_messages_received_total", + Help: "Total number of ccq p2p pubsub broadcast messages received", + }, []string{"type"}) +) + +type ccqP2p struct { + logger *zap.Logger + + h host.Host + th_req *pubsub.Topic + th_resp *pubsub.Topic + sub *pubsub.Subscription + allowedPeers map[string]struct{} +} + +func newCcqRunP2p( + logger *zap.Logger, + allowedPeersStr string, +) *ccqP2p { + l := logger.With(zap.String("component", "ccqp2p")) + allowedPeers := make(map[string]struct{}) + for _, peerID := range strings.Split(allowedPeersStr, ",") { + if peerID != "" { + l.Info("will allow requests from peer", zap.String("peerID", peerID)) + allowedPeers[peerID] = struct{}{} + } + } + + return &ccqP2p{ + logger: l, + allowedPeers: allowedPeers, + } +} + +func (ccq *ccqP2p) run( + ctx context.Context, + priv crypto.PrivKey, + gk *ecdsa.PrivateKey, + networkID string, + bootstrapPeers string, + port uint, + signedQueryReqC chan<- *gossipv1.SignedQueryRequest, + queryResponseReadC <-chan *query.QueryResponsePublication, + errC chan error, +) error { + var err error + + components := DefaultComponents() + if components == nil { + return fmt.Errorf("components is not initialized") + } + components.Port = port + + ccq.h, err = libp2p.New( + // Use the keypair we generated + libp2p.Identity(priv), + + // Multiple listen addresses + libp2p.ListenAddrStrings( + components.ListeningAddresses()..., + ), + + // Enable TLS security as the only security protocol. + libp2p.Security(libp2ptls.ID, libp2ptls.New), + + // Enable QUIC transport as the only transport. + libp2p.Transport(libp2pquic.NewTransport), + + // Let's prevent our peer from having too many + // connections by attaching a connection manager. + libp2p.ConnectionManager(components.ConnMgr), // TODO: Can we use the same connection manager? + + // Let this host use the DHT to find other hosts + libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { + ccq.logger.Info("Connecting to bootstrap peers", zap.String("bootstrap_peers", bootstrapPeers)) + + bootstrappers, _ := bootstrapAddrs(ccq.logger, bootstrapPeers, h.ID()) + + // TODO(leo): Persistent data store (i.e. address book) + idht, err := dht.New(ctx, h, dht.Mode(dht.ModeServer), + // This intentionally makes us incompatible with the global IPFS DHT + dht.ProtocolPrefix(protocol.ID("/"+networkID)), + dht.BootstrapPeers(bootstrappers...), + ) + return idht, err + }), + ) + + if err != nil { + return fmt.Errorf("failed to create p2p: %w", err) + } + + topic_req := fmt.Sprintf("%s/%s", networkID, "ccq_req") + topic_resp := fmt.Sprintf("%s/%s", networkID, "ccq_resp") + + ccq.logger.Info("Creating pubsub topics", zap.String("request_topic", topic_req), zap.String("response_topic", topic_resp)) + ps, err := pubsub.NewGossipSub(ctx, ccq.h, + // We only want to accept subscribes from peers in the allow list. + pubsub.WithPeerFilter(func(peerID peer.ID, topic string) bool { + if len(ccq.allowedPeers) == 0 { + return true + } + if _, found := ccq.allowedPeers[peerID.String()]; found { + return true + } + ccq.logger.Debug("Dropping subscribe attempt from unknown peer", zap.String("peerID", peerID.String())) + return false + })) + if err != nil { + return fmt.Errorf("failed to create new gossip sub for req: %w", err) + } + + // We want to join and subscribe to the request topic. We will receive messages from there, but never write to it. + ccq.th_req, err = ps.Join(topic_req) + if err != nil { + return fmt.Errorf("failed to join topic_req: %w", err) + } + + // We only want to join the response topic. We will only write to it. + ccq.th_resp, err = ps.Join(topic_resp) + if err != nil { + return fmt.Errorf("failed to join topic_resp: %w", err) + } + + // We only want to accept messages from peers in the allow list. + err = ps.RegisterTopicValidator(topic_req, func(ctx context.Context, from peer.ID, msg *pubsub.Message) bool { + if len(ccq.allowedPeers) == 0 { + return true + } + if _, found := ccq.allowedPeers[from.String()]; found { + return true + } + ccq.logger.Debug("Dropping message from unknown peer", zap.String("fromPeerID", from.String())) + return false + }) + if err != nil { + return fmt.Errorf("failed to register message filter: %w", err) + } + + // Increase the buffer size to prevent failed delivery to slower subscribers + ccq.sub, err = ccq.th_req.Subscribe(pubsub.WithBufferSize(1024)) + if err != nil { + return fmt.Errorf("failed to subscribe topic_req: %w", err) + } + + common.StartRunnable(ctx, errC, false, "ccqp2p_listener", func(ctx context.Context) error { + return ccq.listener(ctx, signedQueryReqC) + }) + + common.StartRunnable(ctx, errC, false, "ccqp2p_publisher", func(ctx context.Context) error { + return ccq.publisher(ctx, gk, queryResponseReadC) + }) + + return nil +} + +func (ccq *ccqP2p) close() { + ccq.logger.Info("entering close") + + if err := ccq.th_req.Close(); err != nil && !errors.Is(err, context.Canceled) { + ccq.logger.Error("Error closing the topic_req", zap.Error(err)) + } + if err := ccq.th_resp.Close(); err != nil && !errors.Is(err, context.Canceled) { + ccq.logger.Error("Error closing the topic_req", zap.Error(err)) + } + + ccq.sub.Cancel() + + if err := ccq.h.Close(); err != nil { + ccq.logger.Error("error closing the host", zap.Error(err)) + } +} + +func (ccq *ccqP2p) listener(ctx context.Context, signedQueryReqC chan<- *gossipv1.SignedQueryRequest) error { + for { + envelope, err := ccq.sub.Next(ctx) // Note: sub.Next(ctx) will return an error once ctx is canceled + if err != nil { + return fmt.Errorf("failed to receive pubsub message: %w", err) + } + + var msg gossipv1.GossipMessage + err = proto.Unmarshal(envelope.Data, &msg) + if err != nil { + ccq.logger.Info("received invalid message", + zap.Binary("data", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + ccqP2pMessagesReceived.WithLabelValues("invalid").Inc() + continue + } + + ccq.logger.Info("received message", //TODO: Change to Debug + zap.Any("payload", msg.Message), + zap.Binary("raw", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + + switch m := msg.Message.(type) { + case *gossipv1.GossipMessage_SignedQueryRequest: + if err := query.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { + ccq.logger.Warn("failed to handle query request", zap.Error(err)) + } + default: + ccqP2pMessagesReceived.WithLabelValues("unknown").Inc() + ccq.logger.Warn("received unknown message type (running outdated software?)", + zap.Any("payload", msg.Message), + zap.Binary("raw", envelope.Data), + zap.String("from", envelope.GetFrom().String())) + } + } +} + +func (ccq *ccqP2p) publisher(ctx context.Context, gk *ecdsa.PrivateKey, queryResponseReadC <-chan *query.QueryResponsePublication) error { + for { + select { + case <-ctx.Done(): + return nil + case msg := <-queryResponseReadC: + msgBytes, err := msg.Marshal() + if err != nil { + ccq.logger.Error("failed to marshal query response", zap.Error(err)) + continue + } + digest := query.GetQueryResponseDigestFromBytes(msgBytes) + sig, err := ethcrypto.Sign(digest.Bytes(), gk) + if err != nil { + panic(err) + } + envelope := &gossipv1.GossipMessage{ + Message: &gossipv1.GossipMessage_SignedQueryResponse{ + SignedQueryResponse: &gossipv1.SignedQueryResponse{ + QueryResponse: msgBytes, + Signature: sig, + }, + }, + } + b, err := proto.Marshal(envelope) + if err != nil { + panic(err) + } + err = ccq.th_resp.Publish(ctx, b) + ccqP2pMessagesSent.Inc() + if err != nil { + ccq.logger.Error("failed to publish query response", + zap.String("requestID", msg.RequestID()), + zap.Any("query_response", msg), + zap.Any("signature", sig), + zap.Error(err), + ) + } else { + ccq.logger.Info("published signed query response", //TODO: Change to Debug + zap.String("requestID", msg.RequestID()), + zap.Any("query_response", msg), + zap.Any("signature", sig), + ) + } + } + } +} diff --git a/node/pkg/p2p/p2p.go b/node/pkg/p2p/p2p.go index 981e3f2411..64e020fdf6 100644 --- a/node/pkg/p2p/p2p.go +++ b/node/pkg/p2p/p2p.go @@ -214,6 +214,9 @@ func Run( ccqEnabled bool, signedQueryReqC chan<- *gossipv1.SignedQueryRequest, queryResponseReadC <-chan *query.QueryResponsePublication, + ccqBootstrapPeers string, + ccqPort uint, + ccqAllowedPeers string, ) func(ctx context.Context) error { if components == nil { components = DefaultComponents() @@ -341,6 +344,27 @@ func Run( bootTime := time.Now() + if ccqEnabled { + ccqErrC := make(chan error) + ccq := newCcqRunP2p(logger, ccqAllowedPeers) + if err := ccq.run(ctx, priv, gk, networkID, ccqBootstrapPeers, ccqPort, signedQueryReqC, queryResponseReadC, ccqErrC); err != nil { + return fmt.Errorf("failed to start p2p for CCQ: %w", err) + } + defer ccq.close() + go func() { + for { + select { + case <-ctx.Done(): + return + case ccqErr := <-ccqErrC: + logger.Error("ccqp2p returned an error", zap.Error(ccqErr), zap.String("component", "ccqp2p")) + rootCtxCancel() + return + } + } + }() + } + // Periodically run guardian state set cleanup. go func() { ticker := time.NewTicker(15 * time.Second) @@ -503,45 +527,6 @@ func Run( } else { logger.Info("published signed observation request", zap.Any("signed_observation_request", sReq)) } - case msg := <-queryResponseReadC: - if !ccqEnabled { - logger.Error("received a cross chain query response when the feature is disabled, dropping it", zap.String("component", "ccqp2p")) - continue - } - msgBytes, err := msg.Marshal() - if err != nil { - logger.Error("failed to marshal query response", zap.Error(err), zap.String("component", "ccqp2p")) - continue - } - digest := query.GetQueryResponseDigestFromBytes(msgBytes) - sig, err := ethcrypto.Sign(digest.Bytes(), gk) - if err != nil { - panic(err) - } - envelope := &gossipv1.GossipMessage{ - Message: &gossipv1.GossipMessage_SignedQueryResponse{ - SignedQueryResponse: &gossipv1.SignedQueryResponse{ - QueryResponse: msgBytes, - Signature: sig, - }, - }, - } - b, err := proto.Marshal(envelope) - if err != nil { - panic(err) - } - err = th.Publish(ctx, b) - p2pMessagesSent.Inc() - if err != nil { - logger.Error("failed to publish query response", zap.Error(err), zap.String("component", "ccqp2p")) - } else { - logger.Info("published signed query response", - zap.String("requestID", msg.RequestID()), - zap.Any("query_response", msg), - zap.Any("signature", sig), - zap.String("component", "ccqp2p"), - ) - } } } }() @@ -700,16 +685,6 @@ func Run( if signedGovSt != nil { signedGovSt <- m.SignedChainGovernorStatus } - case *gossipv1.GossipMessage_SignedQueryRequest: - if signedQueryReqC != nil { - if ccqEnabled { - if err := query.PostSignedQueryRequest(signedQueryReqC, m.SignedQueryRequest); err != nil { - logger.Warn("failed to handle query request", zap.Error(err), zap.String("component", "ccqp2p")) - } - } else { - logger.Debug("dropping cross chain query request because the feature is not enabled", zap.String("component", "ccqp2p")) - } - } default: p2pMessagesReceived.WithLabelValues("unknown").Inc() logger.Warn("received unknown message type (running outdated software?)", diff --git a/node/pkg/p2p/watermark_test.go b/node/pkg/p2p/watermark_test.go index a298690ac1..3e1373b282 100644 --- a/node/pkg/p2p/watermark_test.go +++ b/node/pkg/p2p/watermark_test.go @@ -188,5 +188,8 @@ func startGuardian(t *testing.T, ctx context.Context, g *G) { false, // ccqEnabled nil, // signed query request channel nil, // query response channel + "", // query bootstrap peers + 0, // query port + "", // query allowed peers )) } From 69dfd257ddf9be0c66adfe2fec2caa605a82efe1 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:40:20 -0500 Subject: [PATCH 27/37] CCQ: Server allow list (#3347) --- devnet/query-server.yaml | 2 + node/cmd/ccq/devnet.config.json | 64 ++++++++++ node/cmd/ccq/http.go | 28 ++++- node/cmd/ccq/query_server.go | 16 ++- node/cmd/ccq/utils.go | 158 +++++++++++++++++++++++++ sdk/js-query/src/query/ethCall.test.ts | 16 ++- 6 files changed, 272 insertions(+), 12 deletions(-) create mode 100644 node/cmd/ccq/devnet.config.json diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml index f5df1bdb40..cb6c36bad5 100644 --- a/devnet/query-server.yaml +++ b/devnet/query-server.yaml @@ -37,6 +37,8 @@ spec: - node/hack/query/querier.key - --listenAddr - "[::]:6069" + - --permFile + - "node/cmd/ccq/devnet.config.json" - --ethRPC - http://eth-devnet:8545 - --ethContract diff --git a/node/cmd/ccq/devnet.config.json b/node/cmd/ccq/devnet.config.json new file mode 100644 index 0000000000..50d90e61c8 --- /dev/null +++ b/node/cmd/ccq/devnet.config.json @@ -0,0 +1,64 @@ +{ + "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", + "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" + } + } + ] + } + ] +} diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index 128e8a7942..553d603cc2 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -12,10 +12,12 @@ import ( "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 { + ApiKey string `json:"api_key"` Bytes string `json:"bytes"` Signature string `json:"signature"` } @@ -27,6 +29,8 @@ type queryResponse struct { type httpServer struct { topic *pubsub.Topic + logger *zap.Logger + permissions Permissions pendingResponses *PendingResponses } @@ -38,16 +42,24 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { 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 } - // TODO: check if request signer is authorized on Wormchain - 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 } @@ -57,7 +69,11 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { Signature: signature, } - // TODO: validate request before publishing + if err := validateRequest(s.logger, s.permissions, 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{ @@ -67,6 +83,7 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { 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 } @@ -80,6 +97,7 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { 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 @@ -118,10 +136,12 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { s.pendingResponses.Remove(pendingResponse) } -func NewHTTPServer(addr string, t *pubsub.Topic, p *PendingResponses) *http.Server { +func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, p *PendingResponses, logger *zap.Logger) *http.Server { s := &httpServer{ topic: t, + permissions: permissions, pendingResponses: p, + logger: logger, } r := mux.NewRouter() r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT") diff --git a/node/cmd/ccq/query_server.go b/node/cmd/ccq/query_server.go index de26c27856..9241280a81 100644 --- a/node/cmd/ccq/query_server.go +++ b/node/cmd/ccq/query_server.go @@ -21,6 +21,7 @@ var ( p2pBootstrap *string listenAddr *string nodeKeyPath *string + permFile *string ethRPC *string ethContract *string logLevel *string @@ -30,10 +31,11 @@ var ( func init() { p2pNetworkID = QueryServerCmd.Flags().String("network", "/wormhole/dev", "P2P network identifier") - p2pPort = QueryServerCmd.Flags().Uint("port", 8996, "P2P UDP listener port") + p2pPort = QueryServerCmd.Flags().Uint("port", 8995, "P2P UDP listener port") p2pBootstrap = QueryServerCmd.Flags().String("bootstrap", "", "P2P bootstrap peers (comma-separated)") - listenAddr = QueryServerCmd.Flags().String("listenAddr", "[::]:6069", "Listen address for query server (disabled if blank)") nodeKeyPath = QueryServerCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)") + listenAddr = QueryServerCmd.Flags().String("listenAddr", "[::]:6069", "Listen address for query server (disabled if blank)") + permFile = QueryServerCmd.Flags().String("permFile", "", "JSON file containing permissions configuration") ethRPC = QueryServerCmd.Flags().String("ethRPC", "", "Ethereum RPC for fetching current guardian set") ethContract = QueryServerCmd.Flags().String("ethContract", "", "Ethereum core bridge address for fetching current guardian set") logLevel = QueryServerCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)") @@ -87,6 +89,9 @@ func runQueryServer(cmd *cobra.Command, args []string) { if *p2pBootstrap == "" { logger.Fatal("Please specify --bootstrap") } + if *permFile == "" { + logger.Fatal("Please specify --permFile") + } if *ethRPC == "" { logger.Fatal("Please specify --ethRPC") } @@ -94,6 +99,11 @@ func runQueryServer(cmd *cobra.Command, args []string) { logger.Fatal("Please specify --ethContract") } + permissions, err := parseConfig(*permFile) + if err != nil { + logger.Fatal("Failed to load permissions file", zap.String("permFile", *permFile), zap.Error(err)) + } + // Load p2p private key var priv crypto.PrivKey priv, err = common.GetOrCreateNodeKey(logger, *nodeKeyPath) @@ -113,7 +123,7 @@ func runQueryServer(cmd *cobra.Command, args []string) { // Start the HTTP server go func() { - s := NewHTTPServer(*listenAddr, p2p.topic_req, pendingResponses) + s := NewHTTPServer(*listenAddr, p2p.topic_req, permissions, pendingResponses, logger) logger.Sugar().Infof("Server listening on %s", *listenAddr) err := s.ListenAndServe() if err != nil && err != http.ErrServerClosed { diff --git a/node/cmd/ccq/utils.go b/node/cmd/ccq/utils.go index b0b1959e52..fcdeba7193 100644 --- a/node/cmd/ccq/utils.go +++ b/node/cmd/ccq/utils.go @@ -2,10 +2,20 @@ package ccq import ( "context" + "encoding/hex" + "encoding/json" "fmt" + "io" + "os" + "strings" "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" @@ -39,3 +49,151 @@ func FetchCurrentGuardianSet(rpcUrl, coreAddr string) (*common.GuardianSet, erro Index: currentIndex, }, nil } + +type Config struct { + Permissions []User `json:"Permissions"` +} + +type User struct { + UserName string `json:"userName"` + ApiKey string `json:"apiKey"` + AllowedCalls []AllowedCall `json:"allowedCalls"` +} + +type AllowedCall struct { + EthCall *EthCall `json:"ethCall"` +} + +type EthCall struct { + Chain int `json:"chain"` + ContractAddress string `json:"contractAddress"` + Call string `json:"call"` +} + +type Permissions map[string]*permissionEntry + +type permissionEntry struct { + userName string + apiKey string + allowedCalls allowedCallsForUser // Key is something like "ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03" +} + +type allowedCallsForUser map[string]struct{} + +// parseConfig parses the permissions config file into a map keyed by API key. +func parseConfig(fileName string) (Permissions, error) { + jsonFile, err := os.Open(fileName) + if err != nil { + return nil, fmt.Errorf(`failed to open permissions file "%s": %w`, fileName, err) + } + defer jsonFile.Close() + + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + return nil, fmt.Errorf(`failed to read permissions file "%s": %w`, fileName, err) + } + + var config Config + if err := json.Unmarshal(byteValue, &config); err != nil { + return nil, fmt.Errorf(`failed to unmarshal json from permissions file "%s": %w`, fileName, err) + } + + ret := make(Permissions) + for _, user := range config.Permissions { + apiKey := strings.ToLower(user.ApiKey) + if _, exists := ret[apiKey]; exists { + return nil, fmt.Errorf(`API key "%s" in permissions file "%s" is a duplicate`, apiKey, fileName) + } + + // Build the list of allowed calls for this API key. + allowedCalls := make(allowedCallsForUser) + for _, ac := range user.AllowedCalls { + var callKey string + if ac.EthCall != nil { + // Convert the contract address into a standard format like "000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6". + contractAddress, err := vaa.StringToAddress(ac.EthCall.ContractAddress) + if err != nil { + return nil, fmt.Errorf(`invalid contract address "%s" for API key "%s" in permissions file "%s"`, ac.EthCall.ContractAddress, apiKey, fileName) + } + + // The call should be the ABI four byte hex hash of the function signature. Parse it into a standard form of "06fdde03". + call, err := hex.DecodeString(strings.TrimPrefix(ac.EthCall.Call, "0x")) + if err != nil { + return nil, fmt.Errorf(`invalid eth call "%s" for API key "%s" in permissions file "%s"`, ac.EthCall.Call, apiKey, fileName) + } + if len(call) != 4 { + return nil, fmt.Errorf(`eth call "%s" for API key "%s" in permissions file "%s" has an invalid length, must be four bytes`, ac.EthCall.Call, apiKey, fileName) + } + + // The permission key is the chain, contract address and call formatted as a colon separated string. + callKey = fmt.Sprintf("ethCall:%d:%s:%s", ac.EthCall.Chain, contractAddress, hex.EncodeToString(call)) + } else { + return nil, fmt.Errorf(`unsupported call type for API key "%s" in permissions file "%s"`, apiKey, fileName) + } + + if _, exists := allowedCalls[callKey]; exists { + return nil, fmt.Errorf(`"%s" is a duplicate allowed call for API key "%s" in permissions file "%s"`, callKey, apiKey, fileName) + } + + allowedCalls[callKey] = struct{}{} + } + + pe := &permissionEntry{ + userName: user.UserName, + apiKey: apiKey, + allowedCalls: allowedCalls, + } + + ret[apiKey] = pe + } + + return ret, nil +} + +// validateRequest verifies that this API key is allowed to do all of the calls in this request. +func validateRequest(logger *zap.Logger, perms Permissions, apiKey string, qr *gossipv1.SignedQueryRequest) error { + apiKey = strings.ToLower(apiKey) + permsForUser, exists := perms[strings.ToLower(apiKey)] + if !exists { + return fmt.Errorf("invalid api key") + } + + // TODO: Should we verify the signatures? + + var queryRequest query.QueryRequest + err := queryRequest.Unmarshal(qr.QueryRequest) + if err != nil { + return fmt.Errorf("failed to unmarshal request: %w", err) + } + + // Make sure the overall query request is sane. + if err := queryRequest.Validate(); err != nil { + return 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 { + switch q := pcq.Query.(type) { + case *query.EthCallQueryRequest: + for _, callData := range q.CallData { + contractAddress, err := vaa.BytesToAddress(callData.To) + if err != nil { + return fmt.Errorf("failed to parse contract address: %w", err) + } + if len(callData.Data) < 4 { + return fmt.Errorf("eth call data must be at least four bytes") + } + call := hex.EncodeToString(callData.Data) + callKey := fmt.Sprintf("ethCall:%d:%s:%s", int(pcq.ChainId), contractAddress, call) + if _, exists := permsForUser.allowedCalls[callKey]; !exists { + logger.Debug(`api key "%s" has requested an unauthorized call "%s"`) + return fmt.Errorf(`call "%s" not authorized`, callKey) + } + } + default: + return fmt.Errorf("unsupported query type") + } + } + + return nil +} diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts index e95060694e..cdfd90d851 100644 --- a/sdk/js-query/src/query/ethCall.test.ts +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -21,6 +21,7 @@ import { jest.setTimeout(60000); const CI = false; +const ENV = "DEVNET"; const ETH_NODE_URL = CI ? "ws://eth-devnet:8545" : "ws://localhost:8545"; const QUERY_SERVER_URL = "http://localhost:6069/v1/query"; @@ -99,12 +100,17 @@ describe("eth call", () => { const nonce = 1; const request = new QueryRequest(nonce, [ethQuery]); const serialized = request.serialize(); - const digest = QueryRequest.digest("DEVNET", serialized); + const digest = QueryRequest.digest(ENV, serialized); const signature = sign(PRIVATE_KEY, digest); - const response = await axios.put(QUERY_SERVER_URL, { - signature, - bytes: Buffer.from(serialized).toString("hex"), - }); + const api_key = "my_secret_key"; + const response = await axios.put( + QUERY_SERVER_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": api_key } } + ); expect(response.status).toBe(200); const queryResponse = QueryResponse.fromBytes( Buffer.from(response.data.bytes, "hex") From 30c475193d0a48bd9048397394337a8c7b0cb287 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Tue, 5 Sep 2023 19:17:05 -0500 Subject: [PATCH 28/37] CCQ: Add some sdk tests (#3357) --- sdk/js-query/src/query/ethCall.test.ts | 109 ++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts index cdfd90d851..9ee243af6e 100644 --- a/sdk/js-query/src/query/ethCall.test.ts +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -102,14 +102,13 @@ describe("eth call", () => { const serialized = request.serialize(); const digest = QueryRequest.digest(ENV, serialized); const signature = sign(PRIVATE_KEY, digest); - const api_key = "my_secret_key"; const response = await axios.put( QUERY_SERVER_URL, { signature, bytes: Buffer.from(serialized).toString("hex"), }, - { headers: { "X-API-Key": api_key } } + { headers: { "X-API-Key": "my_secret_key" } } ); expect(response.status).toBe(200); const queryResponse = QueryResponse.fromBytes( @@ -118,4 +117,110 @@ describe("eth call", () => { // TODO: verify signatures // TOOD: verify query response }); + test("missing api-key should fail", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + WETH_ADDRESS, + "totalSupply", + "uint256" + ); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [ + nameCallData, + totalSupplyCallData, + ]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const digest = QueryRequest.digest(ENV, serialized); + const signature = sign(PRIVATE_KEY, digest); + let err = false; + await axios + .put(QUERY_SERVER_URL, { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(400); + expect(error.response.data).toBe("api key is missing\n"); + }); + expect(err).toBe(true); + }); + test("invalid api-key should fail", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + WETH_ADDRESS, + "totalSupply", + "uint256" + ); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [ + nameCallData, + totalSupplyCallData, + ]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const digest = QueryRequest.digest(ENV, serialized); + const signature = sign(PRIVATE_KEY, digest); + let err = false; + await axios + .put( + QUERY_SERVER_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "some_junk" } } + ) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(400); + expect(error.response.data).toBe("invalid api key\n"); + }); + expect(err).toBe(true); + }); + test("unauthorized call should fail", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + WETH_ADDRESS, + "totalSupply", + "uint256" + ); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [ + nameCallData, + totalSupplyCallData, // API key "my_secret_key_2" is not authorized to do total supply. + ]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const digest = QueryRequest.digest(ENV, serialized); + const signature = sign(PRIVATE_KEY, digest); + let err = false; + await axios + .put( + QUERY_SERVER_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key_2" } } + ) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(400); + expect(error.response.data).toBe( + `call "ethCall:2:000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e:18160ddd" not authorized\n` + ); + }); + expect(err).toBe(true); + }); }); From a97a786a1aad4dfe4c49dfce4e14c560eed60e9c Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:02:52 -0500 Subject: [PATCH 29/37] CCQ: ccq_server health check (#3365) --- node/cmd/ccq/http.go | 5 +++++ sdk/js-query/src/query/ethCall.test.ts | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index 553d603cc2..697919eb67 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -136,6 +136,10 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { 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, p *PendingResponses, logger *zap.Logger) *http.Server { s := &httpServer{ topic: t, @@ -145,6 +149,7 @@ func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, p *Pen } r := mux.NewRouter() r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT") + r.HandleFunc("/v1/health", s.handleHealth).Methods("GET") return &http.Server{ Addr: addr, Handler: r, diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts index 9ee243af6e..7c8dc706c7 100644 --- a/sdk/js-query/src/query/ethCall.test.ts +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -24,7 +24,9 @@ const CI = false; const ENV = "DEVNET"; const ETH_NODE_URL = CI ? "ws://eth-devnet:8545" : "ws://localhost:8545"; -const QUERY_SERVER_URL = "http://localhost:6069/v1/query"; +const CCQ_SERVER_URL = "http://localhost:6069/v1"; +const QUERY_URL = CCQ_SERVER_URL + "/query"; +const HEALTH_URL = CCQ_SERVER_URL + "/health"; const PRIVATE_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"; @@ -83,7 +85,7 @@ describe("eth call", () => { "0100000001010005010000004600000009307832386439363330020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd" ); }); - test("parse response", async () => { + test("successful query", async () => { const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); const totalSupplyCallData = createTestEthCallData( WETH_ADDRESS, @@ -103,7 +105,7 @@ describe("eth call", () => { const digest = QueryRequest.digest(ENV, serialized); const signature = sign(PRIVATE_KEY, digest); const response = await axios.put( - QUERY_SERVER_URL, + QUERY_URL, { signature, bytes: Buffer.from(serialized).toString("hex"), @@ -138,7 +140,7 @@ describe("eth call", () => { const signature = sign(PRIVATE_KEY, digest); let err = false; await axios - .put(QUERY_SERVER_URL, { + .put(QUERY_URL, { signature, bytes: Buffer.from(serialized).toString("hex"), }) @@ -171,7 +173,7 @@ describe("eth call", () => { let err = false; await axios .put( - QUERY_SERVER_URL, + QUERY_URL, { signature, bytes: Buffer.from(serialized).toString("hex"), @@ -207,7 +209,7 @@ describe("eth call", () => { let err = false; await axios .put( - QUERY_SERVER_URL, + QUERY_URL, { signature, bytes: Buffer.from(serialized).toString("hex"), @@ -223,4 +225,8 @@ describe("eth call", () => { }); expect(err).toBe(true); }); + test("health check", async () => { + const response = await axios.get(HEALTH_URL); + expect(response.status).toBe(200); + }); }); From 328b6de5c9aca728623026a868c7ce646180d0fb Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Tue, 12 Sep 2023 11:32:50 -0500 Subject: [PATCH 30/37] CCQ: Abstract contract --- ethereum/contracts/query/QueryResponse.sol | 2 +- ethereum/forge-test/query/Query.t.sol | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index 00469750fe..bea8f935ec 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -7,7 +7,7 @@ import "../libraries/external/BytesLib.sol"; import "../interfaces/IWormhole.sol"; /// @dev QueryResponse is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses. -library QueryResponse { +abstract contract QueryResponse { using BytesLib for bytes; /// @dev ParsedQueryResponse is returned by parseAndVerifyQueryResponse(). diff --git a/ethereum/forge-test/query/Query.t.sol b/ethereum/forge-test/query/Query.t.sol index dba883d02b..27cbc8a2af 100644 --- a/ethereum/forge-test/query/Query.t.sol +++ b/ethereum/forge-test/query/Query.t.sol @@ -10,7 +10,7 @@ import "../../contracts/Setup.sol"; import "../../contracts/Wormhole.sol"; import "forge-std/Test.sol"; -contract TestQueryResponse is Test { +contract TestQueryResponse is Test, QueryResponse { bytes resp = hex"010000ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f2000000005301dd9914c6010005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd01000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"; bytes32 sigR = hex"ba36cd576a0f9a8a37ec5ea6a174857922f2f170cd7ec62edcbe74b1cc7258d3"; @@ -60,25 +60,25 @@ contract TestQueryResponse is Test { } function test_getResponseHash() public { - bytes32 hash = QueryResponse.getResponseHash(resp); + bytes32 hash = getResponseHash(resp); assertEq(hash, expectedHash); } function test_getResponseDigest() public { - bytes32 digest = QueryResponse.getResponseDigest(resp); + bytes32 digest = getResponseDigest(resp); assertEq(digest, expectedDigetst); } function test_verifyQueryResponseSignatures() public view { IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); - QueryResponse.verifyQueryResponseSignatures(address(wormhole), resp, signatures); + verifyQueryResponseSignatures(address(wormhole), resp, signatures); } function test_parseAndVerifyQueryResponse() public { IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1); signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex}); - QueryResponse.ParsedQueryResponse memory r = QueryResponse.parseAndVerifyQueryResponse(address(wormhole), resp, signatures); + ParsedQueryResponse memory r = parseAndVerifyQueryResponse(address(wormhole), resp, signatures); assertEq(r.version, 1); assertEq(r.senderChainId, 0); assertEq(r.requestId, hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200"); @@ -92,14 +92,14 @@ contract TestQueryResponse is Test { function test_parseEthCallQueryResponse() public { // Take the data extracted by the previous test and break it down even further. - QueryResponse.ParsedPerChainQueryResponse memory r = QueryResponse.ParsedPerChainQueryResponse({ + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ chainId: 5, queryType: 1, request: hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd", response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a" }); - QueryResponse.EthCallQueryResponse memory eqr = QueryResponse.parseEthCallQueryResponse(r); + EthCallQueryResponse memory eqr = parseEthCallQueryResponse(r); assertEq(eqr.requestBlockId, hex"307832613631616334"); assertEq(eqr.blockNum, 44440260); assertEq(eqr.blockHash, hex"c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d04"); From e079566500c670b4cbddfb4fca48056943baaba6 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:49:13 -0500 Subject: [PATCH 31/37] CCQ: Server allow unsigned requests (#3379) --- devnet/node.yaml | 8 +++- devnet/query-server.yaml | 2 + node/cmd/ccq/ccq.signing.key | 6 +++ node/cmd/ccq/devnet.config.json | 1 + node/cmd/ccq/http.go | 7 +++- node/cmd/ccq/query_server.go | 18 ++++++++- node/cmd/ccq/utils.go | 40 ++++++++++++++----- node/hack/query/send_req.go | 41 +------------------ node/hack/query/test/query_test.go | 41 +------------------ node/pkg/query/helpers_test.go | 52 ------------------------ node/pkg/query/query_test.go | 2 +- sdk/js-query/src/query/ethCall.test.ts | 55 ++++++++++++++++++++++++++ 12 files changed, 126 insertions(+), 147 deletions(-) create mode 100644 node/cmd/ccq/ccq.signing.key diff --git a/devnet/node.yaml b/devnet/node.yaml index 35dcabbd97..1c118435c9 100644 --- a/devnet/node.yaml +++ b/devnet/node.yaml @@ -7,6 +7,9 @@ metadata: app: guardian spec: ports: + - port: 8996 + name: ccq-p2p + protocol: UDP - port: 8999 name: p2p protocol: UDP @@ -162,7 +165,7 @@ spec: - "full" # - --chainGovernorEnabled=true - --ccqEnabled=true - - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe + - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54 - --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH # - --logLevel=debug securityContext: @@ -175,6 +178,9 @@ spec: port: 6060 path: /readyz ports: + - containerPort: 8996 + name: ccq-p2p + protocol: UDP - containerPort: 8999 name: p2p protocol: UDP diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml index cb6c36bad5..c566ac0497 100644 --- a/devnet/query-server.yaml +++ b/devnet/query-server.yaml @@ -35,6 +35,8 @@ spec: - query-server - --nodeKey - node/hack/query/querier.key + - --signerKey + - node/cmd/ccq/ccq.signing.key - --listenAddr - "[::]:6069" - --permFile diff --git a/node/cmd/ccq/ccq.signing.key b/node/cmd/ccq/ccq.signing.key new file mode 100644 index 0000000000..78e139e534 --- /dev/null +++ b/node/cmd/ccq/ccq.signing.key @@ -0,0 +1,6 @@ +-----BEGIN CCQ SERVER SIGNING KEY----- +PublicKey: 0x25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54 + +CiCWNLSaicmcA2T563fLSM0r2uFviwPdA1VV9i76DlJh3Q== +=NV1/ +-----END CCQ SERVER SIGNING KEY----- \ No newline at end of file diff --git a/node/cmd/ccq/devnet.config.json b/node/cmd/ccq/devnet.config.json index 50d90e61c8..53a4331e2c 100644 --- a/node/cmd/ccq/devnet.config.json +++ b/node/cmd/ccq/devnet.config.json @@ -41,6 +41,7 @@ { "userName": "Test User Two", "apiKey": "my_secret_key_2", + "allowUnsigned": true, "allowedCalls": [ { "ethCall": { diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index 697919eb67..aa119978c4 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -1,6 +1,7 @@ package ccq import ( + "crypto/ecdsa" "encoding/hex" "encoding/json" "fmt" @@ -31,6 +32,7 @@ type httpServer struct { topic *pubsub.Topic logger *zap.Logger permissions Permissions + signerKey *ecdsa.PrivateKey pendingResponses *PendingResponses } @@ -69,7 +71,7 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { Signature: signature, } - if err := validateRequest(s.logger, s.permissions, apiKey[0], signedQueryRequest); err != nil { + if err := validateRequest(s.logger, 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 @@ -140,10 +142,11 @@ func (s *httpServer) handleHealth(w http.ResponseWriter, r *http.Request) { s.logger.Debug("health check") } -func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, p *PendingResponses, logger *zap.Logger) *http.Server { +func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, signerKey *ecdsa.PrivateKey, p *PendingResponses, logger *zap.Logger) *http.Server { s := &httpServer{ topic: t, permissions: permissions, + signerKey: signerKey, pendingResponses: p, logger: logger, } diff --git a/node/cmd/ccq/query_server.go b/node/cmd/ccq/query_server.go index 9241280a81..9d37ddff6a 100644 --- a/node/cmd/ccq/query_server.go +++ b/node/cmd/ccq/query_server.go @@ -1,7 +1,11 @@ +// Note: To generate a signer key file do: guardiand keygen --block-type "CCQ SERVER SIGNING KEY" /path/to/key/file +// You will need to add this key to ccqAllowedRequesters in the guardian configs. + package ccq import ( "context" + "crypto/ecdsa" "fmt" "net/http" "os" @@ -15,12 +19,15 @@ import ( "go.uber.org/zap" ) +const CCQ_SERVER_SIGNING_KEY = "CCQ SERVER SIGNING KEY" + var ( p2pNetworkID *string p2pPort *uint p2pBootstrap *string listenAddr *string nodeKeyPath *string + signerKeyPath *string permFile *string ethRPC *string ethContract *string @@ -34,6 +41,7 @@ func init() { p2pPort = QueryServerCmd.Flags().Uint("port", 8995, "P2P UDP listener port") p2pBootstrap = QueryServerCmd.Flags().String("bootstrap", "", "P2P bootstrap peers (comma-separated)") nodeKeyPath = QueryServerCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)") + signerKeyPath = QueryServerCmd.Flags().String("signerKey", "", "Path to key used to sign unsigned queries") listenAddr = QueryServerCmd.Flags().String("listenAddr", "[::]:6069", "Listen address for query server (disabled if blank)") permFile = QueryServerCmd.Flags().String("permFile", "", "JSON file containing permissions configuration") ethRPC = QueryServerCmd.Flags().String("ethRPC", "", "Ethereum RPC for fetching current guardian set") @@ -111,6 +119,14 @@ func runQueryServer(cmd *cobra.Command, args []string) { logger.Fatal("Failed to load node key", zap.Error(err)) } + var signerKey *ecdsa.PrivateKey + if *signerKeyPath != "" { + signerKey, err = common.LoadArmoredKey(*signerKeyPath, CCQ_SERVER_SIGNING_KEY, false) + if err != nil { + logger.Fatal("Failed to loader signer key", zap.Error(err)) + } + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -123,7 +139,7 @@ func runQueryServer(cmd *cobra.Command, args []string) { // Start the HTTP server go func() { - s := NewHTTPServer(*listenAddr, p2p.topic_req, permissions, pendingResponses, logger) + s := NewHTTPServer(*listenAddr, p2p.topic_req, permissions, signerKey, pendingResponses, logger) logger.Sugar().Infof("Server listening on %s", *listenAddr) err := s.ListenAndServe() if err != nil && err != http.ErrServerClosed { diff --git a/node/cmd/ccq/utils.go b/node/cmd/ccq/utils.go index fcdeba7193..e9e339fa1f 100644 --- a/node/cmd/ccq/utils.go +++ b/node/cmd/ccq/utils.go @@ -2,6 +2,7 @@ package ccq import ( "context" + "crypto/ecdsa" "encoding/hex" "encoding/json" "fmt" @@ -19,6 +20,7 @@ import ( 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" ) @@ -55,9 +57,10 @@ type Config struct { } type User struct { - UserName string `json:"userName"` - ApiKey string `json:"apiKey"` - AllowedCalls []AllowedCall `json:"allowedCalls"` + UserName string `json:"userName"` + ApiKey string `json:"apiKey"` + AllowUnsigned bool `json:"allowUnsigned"` + AllowedCalls []AllowedCall `json:"allowedCalls"` } type AllowedCall struct { @@ -73,9 +76,10 @@ type EthCall struct { type Permissions map[string]*permissionEntry type permissionEntry struct { - userName string - apiKey string - allowedCalls allowedCallsForUser // Key is something like "ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03" + userName string + apiKey string + allowUnsigned bool + allowedCalls allowedCallsForUser // Key is something like "ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03" } type allowedCallsForUser map[string]struct{} @@ -139,9 +143,10 @@ func parseConfig(fileName string) (Permissions, error) { } pe := &permissionEntry{ - userName: user.UserName, - apiKey: apiKey, - allowedCalls: allowedCalls, + userName: user.UserName, + apiKey: apiKey, + allowUnsigned: user.AllowUnsigned, + allowedCalls: allowedCalls, } ret[apiKey] = pe @@ -151,7 +156,7 @@ func parseConfig(fileName string) (Permissions, error) { } // validateRequest verifies that this API key is allowed to do all of the calls in this request. -func validateRequest(logger *zap.Logger, perms Permissions, apiKey string, qr *gossipv1.SignedQueryRequest) error { +func validateRequest(logger *zap.Logger, perms Permissions, signerKey *ecdsa.PrivateKey, apiKey string, qr *gossipv1.SignedQueryRequest) error { apiKey = strings.ToLower(apiKey) permsForUser, exists := perms[strings.ToLower(apiKey)] if !exists { @@ -160,6 +165,21 @@ func validateRequest(logger *zap.Logger, perms Permissions, apiKey string, qr *g // TODO: Should we verify the signatures? + if len(qr.Signature) == 0 { + if !permsForUser.allowUnsigned || signerKey == nil { + return fmt.Errorf("request not signed") + } + + // Sign the request using our key. + var err error + digest := query.QueryRequestDigest(common.UnsafeDevNet, qr.QueryRequest) + qr.Signature, err = ethCrypto.Sign(digest.Bytes(), signerKey) + if err != nil { + logger.Error("failed to sign request", zap.Error(err)) + return fmt.Errorf("failed to sign request") + } + } + var queryRequest query.QueryRequest err := queryRequest.Unmarshal(qr.QueryRequest) if err != nil { diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 6ecb0dc5f4..a0ab2cfb48 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -9,9 +9,7 @@ import ( "crypto/ecdsa" "encoding/hex" "fmt" - "io" "math/big" - "os" "strings" "time" @@ -19,7 +17,6 @@ import ( "github.com/certusone/wormhole/node/pkg/common" "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/certusone/wormhole/node/pkg/query" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common/hexutil" @@ -37,7 +34,6 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/tendermint/tendermint/libs/rand" "go.uber.org/zap" - "golang.org/x/crypto/openpgp/armor" //nolint "google.golang.org/protobuf/proto" ) @@ -71,7 +67,7 @@ func main() { signingKeyPath := string("./dev.guardian.key") logger.Info("Loading signing key", zap.String("signingKeyPath", signingKeyPath)) - sk, err := loadGuardianKey(signingKeyPath) + sk, err := common.LoadGuardianKey(signingKeyPath, true) if err != nil { logger.Fatal("failed to load guardian key", zap.Error(err)) } @@ -419,38 +415,3 @@ func sendQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, } } } - -// loadGuardianKey loads a serialized guardian key from disk. -func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { - f, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) - } - - p, err := armor.Decode(f) - if err != nil { - return nil, fmt.Errorf("failed to read armored file: %w", err) - } - - if p.Type != GuardianKeyArmoredBlock { - return nil, fmt.Errorf("invalid block type: %s", p.Type) - } - - b, err := io.ReadAll(p.Body) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - var m nodev1.GuardianKey - err = proto.Unmarshal(b, &m) - if err != nil { - return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) - } - - gk, err := ethCrypto.ToECDSA(m.Data) - if err != nil { - return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) - } - - return gk, nil -} diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index 7789ea52f9..31edb14e1b 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -3,10 +3,8 @@ package query_test import ( "bytes" "context" - "crypto/ecdsa" "encoding/hex" "fmt" - "io" "os" "strings" "testing" @@ -19,7 +17,6 @@ import ( "github.com/certusone/wormhole/node/pkg/common" "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/certusone/wormhole/node/pkg/query" "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" @@ -37,7 +34,6 @@ import ( libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" "github.com/multiformats/go-multiaddr" "go.uber.org/zap" - "golang.org/x/crypto/openpgp/armor" //nolint "google.golang.org/protobuf/proto" ) @@ -57,7 +53,7 @@ func TestCrossChainQuery(t *testing.T) { signingKeyPath := string("../dev.guardian.key") logger.Info("Loading signing key", zap.String("signingKeyPath", signingKeyPath)) - sk, err := loadGuardianKey(signingKeyPath) + sk, err := common.LoadGuardianKey(signingKeyPath, true) if err != nil { logger.Fatal("failed to load guardian key", zap.Error(err)) } @@ -356,38 +352,3 @@ func TestCrossChainQuery(t *testing.T) { const ( GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" ) - -// loadGuardianKey loads a serialized guardian key from disk. -func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { - f, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) - } - - p, err := armor.Decode(f) - if err != nil { - return nil, fmt.Errorf("failed to read armored file: %w", err) - } - - if p.Type != GuardianKeyArmoredBlock { - return nil, fmt.Errorf("invalid block type: %s", p.Type) - } - - b, err := io.ReadAll(p.Body) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - var m nodev1.GuardianKey - err = proto.Unmarshal(b, &m) - if err != nil { - return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) - } - - gk, err := ethCrypto.ToECDSA(m.Data) - if err != nil { - return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) - } - - return gk, nil -} diff --git a/node/pkg/query/helpers_test.go b/node/pkg/query/helpers_test.go index 16ebc9152c..25d7eb47a4 100644 --- a/node/pkg/query/helpers_test.go +++ b/node/pkg/query/helpers_test.go @@ -1,57 +1,5 @@ package query -import ( - "crypto/ecdsa" - "fmt" - "io" - "os" - - nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1" - - ethCrypto "github.com/ethereum/go-ethereum/crypto" - "golang.org/x/crypto/openpgp/armor" //nolint - "google.golang.org/protobuf/proto" -) - -const ( - GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY" -) - -// loadGuardianKey loads a serialized guardian key from disk. -func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) { - f, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) - } - - p, err := armor.Decode(f) - if err != nil { - return nil, fmt.Errorf("failed to read armored file: %w", err) - } - - if p.Type != GuardianKeyArmoredBlock { - return nil, fmt.Errorf("invalid block type: %s", p.Type) - } - - b, err := io.ReadAll(p.Body) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - var m nodev1.GuardianKey - err = proto.Unmarshal(b, &m) - if err != nil { - return nil, fmt.Errorf("failed to deserialize protobuf: %w", err) - } - - gk, err := ethCrypto.ToECDSA(m.Data) - if err != nil { - return nil, fmt.Errorf("failed to deserialize raw key data: %w", err) - } - - return gk, nil -} - func makeChannelPair[T any](cap int) (<-chan T, chan<- T) { out := make(chan T, cap) return out, out diff --git a/node/pkg/query/query_test.go b/node/pkg/query/query_test.go index 2fed9c51f4..d544e57ad9 100644 --- a/node/pkg/query/query_test.go +++ b/node/pkg/query/query_test.go @@ -306,7 +306,7 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context md := mockData{} var err error - md.sk, err = loadGuardianKey("../../hack/query/dev.guardian.key") + md.sk, err = common.LoadGuardianKey("../../hack/query/dev.guardian.key", true) require.NoError(t, err) require.NotNil(t, md.sk) diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts index 7c8dc706c7..b2235c2169 100644 --- a/sdk/js-query/src/query/ethCall.test.ts +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -225,6 +225,61 @@ describe("eth call", () => { }); expect(err).toBe(true); }); + test("unsigned query should fail if not allowed", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const totalSupplyCallData = createTestEthCallData( + WETH_ADDRESS, + "totalSupply", + "uint256" + ); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [ + nameCallData, + totalSupplyCallData, + ]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const signature = ""; + let err = false; + await axios + .put( + QUERY_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key" } } + ) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(400); + expect(error.response.data).toBe(`request not signed\n`); + }); + expect(err).toBe(true); + }); + test("unsigned query should succeed if allowed", async () => { + const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string"); + const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT); + const ethCall = new EthCallQueryRequest(blockNumber, [nameCallData]); + const chainId = 2; + const ethQuery = new PerChainQueryRequest(chainId, ethCall); + const nonce = 1; + const request = new QueryRequest(nonce, [ethQuery]); + const serialized = request.serialize(); + const signature = ""; + const response = await axios.put( + QUERY_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key_2" } } // This API key allows unsigned queries. + ); + expect(response.status).toBe(200); + }); test("health check", async () => { const response = await axios.get(HEALTH_URL); expect(response.status).toBe(200); From 81f102d7a1c2965c3588be2b435cf344106d9a8e Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Sun, 1 Oct 2023 10:31:02 -0500 Subject: [PATCH 32/37] CCQ: Query demo contract (#3373) * CCQ: Add enable flag and feature flag (#2986) * CCQ: Add enable flag and feature flag * Fix build error * CCQ: Add ccqAllowedRequesters parameter (#2990) * CCQ: Query demo contract * More stuff * More stuff * More changes * remove bad check * timestamp bugfix * Code review rework --------- Co-authored-by: Evan Gray --- ethereum/contracts/query/QueryDemo.sol | 103 +++++++++++++++++++++++++ ethereum/forge-test/query/Query.t.sol | 29 +++++++ ethereum/scripts/deploy_ccq_demo.js | 26 +++++++ 3 files changed, 158 insertions(+) create mode 100644 ethereum/contracts/query/QueryDemo.sol create mode 100644 ethereum/scripts/deploy_ccq_demo.js diff --git a/ethereum/contracts/query/QueryDemo.sol b/ethereum/contracts/query/QueryDemo.sol new file mode 100644 index 0000000000..f2396f5a5b --- /dev/null +++ b/ethereum/contracts/query/QueryDemo.sol @@ -0,0 +1,103 @@ +// contracts/query/QueryDemo.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../libraries/external/BytesLib.sol"; +import "../interfaces/IWormhole.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "./QueryResponse.sol"; + +/// @dev QueryDemo is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses. +contract QueryDemo is Context, QueryResponse { + using BytesLib for bytes; + + struct ChainEntry { + uint16 chainID; + address contractAddress; + uint256 counter; + uint256 blockNum; + uint256 blockTime; + } + + address private immutable owner; + address private immutable wormhole; + uint16 private immutable myChainID; + mapping(uint16 => ChainEntry) private counters; + uint16[] private foreignChainIDs; + + bytes4 GetMyCounter = bytes4(hex"916d5743"); + + constructor(address _owner, address _wormhole, uint16 _myChainID) { + owner = _owner; + wormhole = _wormhole; + myChainID = _myChainID; + counters[_myChainID] = ChainEntry(_myChainID, address(this), 0, 0, 0); + } + + // updateRegistration should be used to add the other chains and to set / update contract addresses. + function updateRegistration(uint16 _chainID, address _contractAddress) public onlyOwner { + if (counters[_chainID].chainID == 0) { + foreignChainIDs.push(_chainID); + counters[_chainID].chainID = _chainID; + } + + counters[_chainID].contractAddress = _contractAddress; + } + + // getMyCounter (call signature 916d5743) returns the counter value for this chain. It is meant to be used in a cross chain query. + function getMyCounter() public view returns (uint256) { + return counters[myChainID].counter; + } + + // getState() returns this chain's view of all the counters. It is meant to be used in the front end. + function getState() public view returns (ChainEntry[] memory) { + ChainEntry[] memory ret = new ChainEntry[](foreignChainIDs.length + 1); + ret[0] = counters[myChainID]; + for (uint idx=0; idx counters[r.responses[idx].chainId].blockNum, "update is obsolete"); + // wormhole time is in microseconds, timestamp is in seconds + adjustedBlockTime = eqr.blockTime / 1_000_000; + require(adjustedBlockTime > block.timestamp - 300, "update is stale"); + require(eqr.result.length == 1, "result mismatch"); + require(eqr.result[0].contractAddress == counters[r.responses[idx].chainId].contractAddress, "contract address is wrong"); + + // TODO: Is there an easier way to verify that the call data is correct! + bytes memory callData = eqr.result[0].callData; + bytes4 result; + assembly { + result := mload(add(callData, 32)) + } + require(result == GetMyCounter, "unexpected callData"); + + require(eqr.result[0].result.length == 32, "result is not a uint256"); + counters[r.responses[idx].chainId].blockNum = eqr.blockNum; + counters[r.responses[idx].chainId].blockTime = adjustedBlockTime; + counters[r.responses[idx].chainId].counter = abi.decode(eqr.result[0].result, (uint256)); + } + + counters[myChainID].blockNum = block.number; + counters[myChainID].blockTime = block.timestamp; + counters[myChainID].counter += 1; + } + + modifier onlyOwner() { + require(owner == _msgSender(), "caller is not the owner"); + _; + } +} diff --git a/ethereum/forge-test/query/Query.t.sol b/ethereum/forge-test/query/Query.t.sol index 27cbc8a2af..f036d332d2 100644 --- a/ethereum/forge-test/query/Query.t.sol +++ b/ethereum/forge-test/query/Query.t.sol @@ -114,4 +114,33 @@ contract TestQueryResponse is Test, QueryResponse { assertEq(eqr.result[1].callData, hex"18160ddd"); assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000007ae5649beabeddf889364a"); } + + function test_parseEthCallQueryResponseComparison() public { + ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({ + chainId: 23, + queryType: 1, + request: hex"00000009307832376433333433013ce792601c936b1c81f73ea2fa77208c0a478bae00000004916d5743", + response: hex"00000000027d3343b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c90006056cda47a84001000000200000000000000000000000000000000000000000000000000000000000000004" + }); + + EthCallQueryResponse memory eqr = parseEthCallQueryResponse(r); + assertEq(eqr.requestBlockId, "0x27d3343"); + assertEq(eqr.blockNum, 0x27d3343); + assertEq(eqr.blockHash, hex"b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c9"); + vm.warp(1694814937); + assertEq(eqr.blockTime / 1_000_000, block.timestamp); + assertEq(eqr.result.length, 1); + + assertEq(eqr.result[0].contractAddress, address(0x3ce792601c936b1c81f73Ea2fa77208C0A478BaE)); + assertEq(eqr.result[0].callData, hex"916d5743"); + bytes memory callData = eqr.result[0].callData; + bytes4 callSignature; + assembly { + callSignature := mload(add(callData, 32)) + } + assertEq(callSignature, bytes4(keccak256("getMyCounter()"))); + assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000004"); + assertEq(abi.decode(eqr.result[0].result, (uint256)), 4); + + } } diff --git a/ethereum/scripts/deploy_ccq_demo.js b/ethereum/scripts/deploy_ccq_demo.js new file mode 100644 index 0000000000..f7ce4e9e98 --- /dev/null +++ b/ethereum/scripts/deploy_ccq_demo.js @@ -0,0 +1,26 @@ +const QueryDemo = artifacts.require("QueryDemo"); +module.exports = async function(callback) { + const accounts = await web3.eth.getAccounts(); + try { + // const ccqDemo = await QueryDemo.new( + // accounts[0], + // "0x0CBE91CF822c73C2315FB05100C2F714765d5c20", + // 5 + // ); + // const ccqDemo = await QueryDemo.new( + // accounts[0], + // "0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e", + // 23 + // ); + const ccqDemo = await QueryDemo.new( + accounts[0], + "0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35", + 24 + ); + console.log("tx: " + ccqDemo.transactionHash); + console.log("QueryDemo address: " + ccqDemo.address); + callback(); + } catch (e) { + callback(e); + } +}; From 5c926896b59d4f6e1facba1959f3cf116928ce15 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Sun, 1 Oct 2023 18:46:26 -0500 Subject: [PATCH 33/37] CCQ: Fix tilt CI tests (#3410) * CCQ: Fix tilt CI tests * Add logging * Try adding second guardian to bootstrap peers * More debugging * Another tweak * Try again * Undo a tweak * Sigh * Shrug * Give query-server a unique p2p key --- devnet/query-server.yaml | 2 +- node/cmd/ccq/ccq.p2p.key | 1 + node/hack/query/test/query_test.go | 3 +++ node/pkg/p2p/ccq_p2p.go | 1 + wormchain/contracts/tools/__tests__/test_accountant.ts | 2 +- 5 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 node/cmd/ccq/ccq.p2p.key diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml index c566ac0497..5e69009d63 100644 --- a/devnet/query-server.yaml +++ b/devnet/query-server.yaml @@ -34,7 +34,7 @@ spec: - /guardiand - query-server - --nodeKey - - node/hack/query/querier.key + - node/cmd/ccq/ccq.p2p.key - --signerKey - node/cmd/ccq/ccq.signing.key - --listenAddr diff --git a/node/cmd/ccq/ccq.p2p.key b/node/cmd/ccq/ccq.p2p.key new file mode 100644 index 0000000000..bebd888964 --- /dev/null +++ b/node/cmd/ccq/ccq.p2p.key @@ -0,0 +1 @@ +@~D&ðo%†[‰_jäÖszf=5´¨÷šfæC„d ¸Ó¥§“§91)„¬¡U£=˜Ö¿@JxÓ¼l ]aŒÀf? \ No newline at end of file diff --git a/node/hack/query/test/query_test.go b/node/hack/query/test/query_test.go index 31edb14e1b..907bdd28d3 100644 --- a/node/hack/query/test/query_test.go +++ b/node/hack/query/test/query_test.go @@ -64,6 +64,7 @@ func TestCrossChainQuery(t *testing.T) { if err != nil { logger.Fatal("Failed to fetch current guardian set", zap.Error(err)) } + logger.Info("Fetched guardian set", zap.Any("keys", sgs.Keys)) gs := common.GuardianSet{ Keys: sgs.Keys, Index: idx, @@ -176,6 +177,8 @@ func TestCrossChainQuery(t *testing.T) { time.Sleep(time.Millisecond * 100) } + logger.Info("Detected peers") + 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) diff --git a/node/pkg/p2p/ccq_p2p.go b/node/pkg/p2p/ccq_p2p.go index 8b0e8e0fb3..6926312e4c 100644 --- a/node/pkg/p2p/ccq_p2p.go +++ b/node/pkg/p2p/ccq_p2p.go @@ -190,6 +190,7 @@ func (ccq *ccqP2p) run( return ccq.publisher(ctx, gk, queryResponseReadC) }) + ccq.logger.Info("Node has been started", zap.String("peer_id", ccq.h.ID().String()), zap.String("addrs", fmt.Sprintf("%v", ccq.h.Addrs()))) return nil } diff --git a/wormchain/contracts/tools/__tests__/test_accountant.ts b/wormchain/contracts/tools/__tests__/test_accountant.ts index 8c0d0eeb57..50196040c6 100644 --- a/wormchain/contracts/tools/__tests__/test_accountant.ts +++ b/wormchain/contracts/tools/__tests__/test_accountant.ts @@ -30,7 +30,7 @@ import * as devnetConsts from "../devnet-consts.json"; import { parseUnits } from "ethers/lib/utils"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -jest.setTimeout(60000); +jest.setTimeout(120000); if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") { let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`; From 2067e47d0d95e9df4953022cff417e7defff2fd0 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:27:29 -0500 Subject: [PATCH 34/37] CCQ: Query server CORS change (#3415) --- node/cmd/ccq/http.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index aa119978c4..233ae26c8d 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -37,6 +37,18 @@ type httpServer struct { } 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 { @@ -151,7 +163,7 @@ func NewHTTPServer(addr string, t *pubsub.Topic, permissions Permissions, signer logger: logger, } r := mux.NewRouter() - r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT") + r.HandleFunc("/v1/query", s.handleQuery).Methods("PUT", "OPTIONS") r.HandleFunc("/v1/health", s.handleHealth).Methods("GET") return &http.Server{ Addr: addr, From e5efea2459c49cbb8307e4b90f8dc6d9e5004a95 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:48:09 -0500 Subject: [PATCH 35/37] CCQ: Query server env is hard coded to devnet (#3417) --- devnet/query-server.yaml | 2 ++ node/cmd/ccq/http.go | 7 +++-- node/cmd/ccq/query_server.go | 15 ++++++++++- node/cmd/ccq/utils.go | 22 ++++++++++++---- node/pkg/common/mode.go | 26 ++++++++++++++++++ node/pkg/common/mode_test.go | 51 ++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 node/pkg/common/mode_test.go diff --git a/devnet/query-server.yaml b/devnet/query-server.yaml index 5e69009d63..283aa01f1f 100644 --- a/devnet/query-server.yaml +++ b/devnet/query-server.yaml @@ -33,6 +33,8 @@ spec: command: - /guardiand - query-server + - --env + - dev - --nodeKey - node/cmd/ccq/ccq.p2p.key - --signerKey diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index 233ae26c8d..b9334a0c40 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -9,6 +9,7 @@ import ( "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" @@ -31,6 +32,7 @@ type queryResponse struct { type httpServer struct { topic *pubsub.Topic logger *zap.Logger + env common.Environment permissions Permissions signerKey *ecdsa.PrivateKey pendingResponses *PendingResponses @@ -83,7 +85,7 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { Signature: signature, } - if err := validateRequest(s.logger, s.permissions, s.signerKey, apiKey[0], signedQueryRequest); err != nil { + 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 @@ -154,13 +156,14 @@ 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) *http.Server { +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") diff --git a/node/cmd/ccq/query_server.go b/node/cmd/ccq/query_server.go index 9d37ddff6a..e0773b3166 100644 --- a/node/cmd/ccq/query_server.go +++ b/node/cmd/ccq/query_server.go @@ -13,6 +13,7 @@ import ( "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/telemetry" "github.com/certusone/wormhole/node/pkg/version" + ethCrypto "github.com/ethereum/go-ethereum/crypto" ipfslog "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/crypto" "github.com/spf13/cobra" @@ -22,6 +23,7 @@ import ( const CCQ_SERVER_SIGNING_KEY = "CCQ SERVER SIGNING KEY" var ( + envStr *string p2pNetworkID *string p2pPort *uint p2pBootstrap *string @@ -37,6 +39,7 @@ var ( ) func init() { + envStr = QueryServerCmd.Flags().String("env", "", "environment (dev, test, prod)") p2pNetworkID = QueryServerCmd.Flags().String("network", "/wormhole/dev", "P2P network identifier") p2pPort = QueryServerCmd.Flags().Uint("port", 8995, "P2P UDP listener port") p2pBootstrap = QueryServerCmd.Flags().String("bootstrap", "", "P2P bootstrap peers (comma-separated)") @@ -90,6 +93,14 @@ func runQueryServer(cmd *cobra.Command, args []string) { logger = tm.WrapLogger(logger) // Wrap logger with telemetry logger } + env, err := common.ParseEnvironment(*envStr) + if err != nil || (env != common.UnsafeDevNet && env != common.TestNet && env != common.MainNet) { + if *envStr == "" { + logger.Fatal("Please specify --env") + } + logger.Fatal("Invalid value for --env, must be dev, test or prod", zap.String("val", *envStr)) + } + // Verify flags if *nodeKeyPath == "" { logger.Fatal("Please specify --nodeKey") @@ -125,6 +136,8 @@ func runQueryServer(cmd *cobra.Command, args []string) { if err != nil { logger.Fatal("Failed to loader signer key", zap.Error(err)) } + + logger.Info("will sign unsigned requests if api key supports it", zap.Stringer("signingKey", ethCrypto.PubkeyToAddress(signerKey.PublicKey))) } ctx, cancel := context.WithCancel(context.Background()) @@ -139,7 +152,7 @@ func runQueryServer(cmd *cobra.Command, args []string) { // Start the HTTP server go func() { - s := NewHTTPServer(*listenAddr, p2p.topic_req, permissions, signerKey, pendingResponses, logger) + s := NewHTTPServer(*listenAddr, p2p.topic_req, permissions, signerKey, pendingResponses, logger, env) logger.Sugar().Infof("Server listening on %s", *listenAddr) err := s.ListenAndServe() if err != nil && err != http.ErrServerClosed { diff --git a/node/cmd/ccq/utils.go b/node/cmd/ccq/utils.go index e9e339fa1f..745b2b35a0 100644 --- a/node/cmd/ccq/utils.go +++ b/node/cmd/ccq/utils.go @@ -156,10 +156,11 @@ func parseConfig(fileName string) (Permissions, error) { } // validateRequest verifies that this API key is allowed to do all of the calls in this request. -func validateRequest(logger *zap.Logger, perms Permissions, signerKey *ecdsa.PrivateKey, apiKey string, qr *gossipv1.SignedQueryRequest) error { +func validateRequest(logger *zap.Logger, env common.Environment, perms Permissions, signerKey *ecdsa.PrivateKey, apiKey string, qr *gossipv1.SignedQueryRequest) error { apiKey = strings.ToLower(apiKey) permsForUser, exists := perms[strings.ToLower(apiKey)] if !exists { + logger.Debug("invalid api key", zap.String("apiKey", apiKey)) return fmt.Errorf("invalid api key") } @@ -167,27 +168,34 @@ func validateRequest(logger *zap.Logger, perms Permissions, signerKey *ecdsa.Pri if len(qr.Signature) == 0 { if !permsForUser.allowUnsigned || signerKey == nil { + logger.Debug("request not signed and unsigned requests not supported for apiKey", + zap.String("apiKey", apiKey), + zap.Bool("allowUnsigned", permsForUser.allowUnsigned), + zap.Bool("signerKeyConfigured", signerKey != nil), + ) return fmt.Errorf("request not signed") } // Sign the request using our key. var err error - digest := query.QueryRequestDigest(common.UnsafeDevNet, qr.QueryRequest) + digest := query.QueryRequestDigest(env, qr.QueryRequest) qr.Signature, err = ethCrypto.Sign(digest.Bytes(), signerKey) if err != nil { - logger.Error("failed to sign request", zap.Error(err)) - return fmt.Errorf("failed to sign request") + logger.Debug("failed to sign request", zap.String("apiKey", apiKey), zap.Error(err)) + return 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("apiKey", apiKey), zap.Error(err)) return 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("apiKey", apiKey), zap.Error(err)) return fmt.Errorf("failed to validate request: %w", err) } @@ -198,22 +206,26 @@ func validateRequest(logger *zap.Logger, perms Permissions, signerKey *ecdsa.Pri for _, callData := range q.CallData { contractAddress, err := vaa.BytesToAddress(callData.To) if err != nil { + logger.Debug("failed to parse contract address", zap.String("apiKey", apiKey), zap.String("contract", hex.EncodeToString(callData.To)), zap.Error(err)) return fmt.Errorf("failed to parse contract address: %w", err) } if len(callData.Data) < 4 { + logger.Debug("eth call data must be at least four bytes", zap.String("apiKey", apiKey), zap.String("data", hex.EncodeToString(callData.Data))) return fmt.Errorf("eth call data must be at least four bytes") } call := hex.EncodeToString(callData.Data) callKey := fmt.Sprintf("ethCall:%d:%s:%s", int(pcq.ChainId), contractAddress, call) if _, exists := permsForUser.allowedCalls[callKey]; !exists { - logger.Debug(`api key "%s" has requested an unauthorized call "%s"`) + logger.Debug("requested call not authorized", zap.String("apiKey", apiKey), zap.String("callKey", callKey)) return fmt.Errorf(`call "%s" not authorized`, callKey) } } default: + logger.Debug("unsupported query type", zap.String("apiKey", apiKey), zap.Any("type", pcq.Query)) return fmt.Errorf("unsupported query type") } } + logger.Debug("submitting query request", zap.String("apiKey", apiKey)) return nil } diff --git a/node/pkg/common/mode.go b/node/pkg/common/mode.go index b1d3f9eef0..823f213517 100644 --- a/node/pkg/common/mode.go +++ b/node/pkg/common/mode.go @@ -1,5 +1,10 @@ package common +import ( + "fmt" + "strings" +) + type Environment string const ( @@ -9,3 +14,24 @@ const ( GoTest Environment = "unit-test" AccountantMock Environment = "accountant-mock" // Used for mocking accountant with a Wormchain connection ) + +// ParseEnvironment parses a string into the corresponding Environment value, allowing various reasonable variations. +func ParseEnvironment(str string) (Environment, error) { + str = strings.ToLower(str) + if str == "prod" || str == "mainnet" { + return MainNet, nil + } + if str == "test" || str == "testnet" { + return TestNet, nil + } + if str == "dev" || str == "devnet" || str == "unsafedevnet" { + return UnsafeDevNet, nil + } + if str == "unit-test" || str == "gotest" { + return GoTest, nil + } + if str == "accountant-mock" || str == "accountantmock" { + return AccountantMock, nil + } + return UnsafeDevNet, fmt.Errorf("invalid environment string: %s", str) +} diff --git a/node/pkg/common/mode_test.go b/node/pkg/common/mode_test.go new file mode 100644 index 0000000000..9f5d2cd4fd --- /dev/null +++ b/node/pkg/common/mode_test.go @@ -0,0 +1,51 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseEnvironment(t *testing.T) { + type test struct { + input string + output Environment + err bool + } + + tests := []test{ + {input: "MainNet", output: MainNet, err: false}, + {input: "Prod", output: MainNet, err: false}, + + {input: "TestNet", output: TestNet, err: false}, + {input: "test", output: TestNet, err: false}, + + {input: "UnsafeDevNet", output: UnsafeDevNet, err: false}, + {input: "devnet", output: UnsafeDevNet, err: false}, + {input: "dev", output: UnsafeDevNet, err: false}, + + {input: "GoTest", output: GoTest, err: false}, + {input: "unit-test", output: GoTest, err: false}, + + {input: "AccountantMock", output: AccountantMock, err: false}, + {input: "accountant-mock", output: AccountantMock, err: false}, + + {input: "junk", output: UnsafeDevNet, err: true}, + {input: "", output: UnsafeDevNet, err: true}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + output, err := ParseEnvironment(tc.input) + if err != nil { + if tc.err == false { + assert.NoError(t, err) + } + } else if tc.err { + assert.Error(t, err) + } else { + assert.Equal(t, tc.output, output) + } + }) + } +} From 7dfe9c4c37cdf4600f4f2688ed24d4937d7049ec Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:34:26 -0500 Subject: [PATCH 36/37] CCQ: Ethereum parsing library rework (#3404) * CCQ: Eth parsing library rework * Use BytesParsing for unchecked handling * Add checkLength() * Use a const instead of a magic number --- ethereum/contracts/query/QueryResponse.sol | 164 ++++++++++++--------- ethereum/truffle-config.js | 2 +- 2 files changed, 96 insertions(+), 70 deletions(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index bea8f935ec..c94ad2fbbd 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -3,12 +3,22 @@ pragma solidity ^0.8.0; -import "../libraries/external/BytesLib.sol"; +import {BytesParsing} from "../relayer/libraries/BytesParsing.sol"; import "../interfaces/IWormhole.sol"; /// @dev QueryResponse is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses. abstract contract QueryResponse { - using BytesLib for bytes; + using BytesParsing for bytes; + + // Custom errors + error InvalidResponseVersion(); + error VersionMismatch(); + error NumberOfResponsesMismatch(); + error ChainIdMismatch(); + error RequestTypeMismatch(); + error UnsupportedQueryType(); + error UnexpectedNumberOfResults(); + error InvalidPayloadLength(uint256 received, uint256 expected); /// @dev ParsedQueryResponse is returned by parseAndVerifyQueryResponse(). struct ParsedQueryResponse { @@ -44,6 +54,7 @@ abstract contract QueryResponse { } bytes public constant responsePrefix = bytes("query_response_0000000000000000000|"); + uint8 public constant QT_ETH_CALL = 1; /// @dev getResponseHash computes the hash of the specified query response. function getResponseHash(bytes memory response) public pure returns (bytes32) { @@ -61,121 +72,129 @@ abstract contract QueryResponse { uint index = 0; - r.version = response.toUint8(index); - require(r.version == 1, "invalid response version"); - index += 1; + (r.version, index) = response.asUint8Unchecked(index); + if (r.version != 1) { + revert InvalidResponseVersion(); + } - r.senderChainId = response.toUint16(index); - index += 2; + (r.senderChainId, index) = response.asUint16Unchecked(index); if (r.senderChainId == 0) { - r.requestId = response.slice(index, 65); - index += 65; + (r.requestId, index) = response.sliceUnchecked(index, 65); } else { - r.requestId = response.slice(index, 32); - index += 32; + (r.requestId, index) = response.sliceUnchecked(index, 32); } - uint32 len = response.toUint32(index); // query_request_len - index += 4; + uint32 len; + (len, index) = response.asUint32Unchecked(index); // query_request_len uint reqIdx = index; - require(response.toUint8(reqIdx) == r.version, "version mismatch between request and response"); - reqIdx += 1; + uint8 version; + (version, reqIdx) = response.asUint8Unchecked(reqIdx); + if (version != r.version) { + revert VersionMismatch(); + } - r.nonce = response.toUint32(reqIdx); - reqIdx += 4; + (r.nonce, reqIdx) = response.asUint32Unchecked(reqIdx); - uint8 numPerChainQueries = response.toUint8(reqIdx); - reqIdx += 1; + uint8 numPerChainQueries; + (numPerChainQueries, reqIdx) = response.asUint8Unchecked(reqIdx); // The response starts after the request. uint respIdx = index + len; - require(response.toUint8(respIdx) == numPerChainQueries, "num_per_chain_responses does not match num_per_chain_queries"); - respIdx += 1; + uint8 respNumPerChainQueries; + (respNumPerChainQueries, respIdx) = response.asUint8Unchecked(respIdx); + if (respNumPerChainQueries != numPerChainQueries) { + revert NumberOfResponsesMismatch(); + } r.responses = new ParsedPerChainQueryResponse[](numPerChainQueries); // Walk through the requests and responses in lock step. - for (uint idx = 0; idx < numPerChainQueries; idx++) { - r.responses[idx].chainId = response.toUint16(reqIdx); - require(response.toUint16(respIdx) == r.responses[idx].chainId, "reqChainId does not match respChainId"); - reqIdx += 2; - respIdx += 2; - - r.responses[idx].queryType = response.toUint8(reqIdx); - require(response.toUint8(respIdx) == r.responses[idx].queryType, "reqQueryType does not match respQueryType"); - reqIdx += 1; - respIdx += 1; + for (uint idx = 0; idx < numPerChainQueries;) { + (r.responses[idx].chainId, reqIdx) = response.asUint16Unchecked(reqIdx); + uint16 respChainId; + (respChainId, respIdx) = response.asUint16Unchecked(respIdx); + if (respChainId != r.responses[idx].chainId) { + revert ChainIdMismatch(); + } + + (r.responses[idx].queryType, reqIdx) = response.asUint8Unchecked(reqIdx); + uint8 respQueryType; + (respQueryType, respIdx) = response.asUint8Unchecked(respIdx); + if (respQueryType != r.responses[idx].queryType) { + revert RequestTypeMismatch(); + } - require(r.responses[idx].queryType == 1, "EthCall is the only supported query type"); + if (r.responses[idx].queryType != QT_ETH_CALL) { + revert UnsupportedQueryType(); + } - len = response.toUint32(reqIdx); - reqIdx += 4; - r.responses[idx].request = response.slice(reqIdx, len); - reqIdx += len; + (len, reqIdx) = response.asUint32Unchecked(reqIdx); + (r.responses[idx].request, reqIdx) = response.sliceUnchecked(reqIdx, len); - len = response.toUint32(respIdx); - respIdx += 4; - r.responses[idx].response = response.slice(respIdx, len); - respIdx += len; + (len, respIdx) = response.asUint32Unchecked(respIdx); + (r.responses[idx].response, respIdx) = response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } } + checkLength(response, respIdx); return r; } /// @dev parseEthCallQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query. function parseEthCallQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallQueryResponse memory r) { - require(pcr.queryType == 1, "query type must be EthCall"); + if (pcr.queryType != QT_ETH_CALL) { + revert UnsupportedQueryType(); + } uint reqIdx = 0; uint respIdx = 0; - uint32 len = pcr.request.toUint32(reqIdx); // block_id_len - reqIdx += 4; + uint32 len; + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // block_id_len - r.requestBlockId = pcr.request.slice(reqIdx, len); - reqIdx += len; + (r.requestBlockId, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); - uint8 numBatchCallData = pcr.request.toUint8(reqIdx); - reqIdx += 1; + uint8 numBatchCallData; + (numBatchCallData, reqIdx) = pcr.request.asUint8Unchecked(reqIdx); - r.blockNum = pcr.response.toUint64(respIdx); - respIdx += 8; + (r.blockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx); - r.blockHash = pcr.response.toBytes32(respIdx); - respIdx += 32; + (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx); - r.blockTime = pcr.response.toUint64(respIdx); - respIdx += 8; + (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx); - require(pcr.response.toUint8(respIdx) == numBatchCallData, "num results doesn't match num call datas"); - respIdx += 1; + uint8 respNumResults; + (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx); + if (respNumResults != numBatchCallData) { + revert UnexpectedNumberOfResults(); + } r.result = new EthCallData[](numBatchCallData); // Walk through the call data and results in lock step. - for (uint idx = 0; idx < numBatchCallData; idx++) { - r.result[idx].contractAddress = pcr.request.toAddress(reqIdx); - reqIdx += 20; - - len = pcr.request.toUint32(reqIdx); // call_data_len - reqIdx += 4; - r.result[idx].callData = pcr.request.slice(reqIdx, len); - reqIdx += len; - - len = pcr.response.toUint32(respIdx); // result_len - respIdx += 4; - r.result[idx].result = pcr.response.slice(respIdx, len); - respIdx += len; + for (uint idx = 0; idx < numBatchCallData;) { + (r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx); + + (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len + (r.result[idx].callData, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len); + + (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len + (r.result[idx].result, respIdx) = pcr.response.sliceUnchecked(respIdx, len); + + unchecked { ++idx; } } + checkLength(pcr.request, reqIdx); + checkLength(pcr.response, respIdx); return r; } /** - * @dev verifyQueryResponseSignatures serves to + * @dev verifyQueryResponseSignatures verifies the signatures on a query response. It calls into the Wormhole contract. * IWormhole.Signature expects the last byte to be bumped by 27 * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174 */ @@ -216,5 +235,12 @@ abstract contract QueryResponse { /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet. } + + /// @dev checkLength verifies that the message was fully consumed. + function checkLength(bytes memory encoded, uint256 expected) private pure { + if (encoded.length != expected) { + revert InvalidPayloadLength(encoded.length, expected); + } + } } diff --git a/ethereum/truffle-config.js b/ethereum/truffle-config.js index 8032896fcd..0534e9096a 100644 --- a/ethereum/truffle-config.js +++ b/ethereum/truffle-config.js @@ -4,7 +4,7 @@ const KLAYHDWalletProvider = require("truffle-hdwallet-provider-klaytn"); module.exports = { contracts_directory: - "contracts/{*.sol,bridge/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol},interfaces/IWormhole.sol,mock/*.sol,nft/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol},query/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol}}", + "contracts/{*.sol,bridge/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol},interfaces/IWormhole.sol,mock/*.sol,nft/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol}}", networks: { development: { host: "127.0.0.1", From 33823dcaba01c4cd4a05596fe8cfbced094801e0 Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:21:09 -0500 Subject: [PATCH 37/37] CCQ: Eth library gas tweaks (#3420) --- ethereum/contracts/query/QueryResponse.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethereum/contracts/query/QueryResponse.sol b/ethereum/contracts/query/QueryResponse.sol index c94ad2fbbd..423413aa47 100644 --- a/ethereum/contracts/query/QueryResponse.sol +++ b/ethereum/contracts/query/QueryResponse.sol @@ -24,8 +24,8 @@ abstract contract QueryResponse { struct ParsedQueryResponse { uint8 version; uint16 senderChainId; - bytes requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain uint32 nonce; + bytes requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain ParsedPerChainQueryResponse [] responses; } @@ -41,8 +41,8 @@ abstract contract QueryResponse { struct EthCallQueryResponse { bytes requestBlockId; uint64 blockNum; - bytes32 blockHash; uint64 blockTime; + bytes32 blockHash; EthCallData [] result; }