diff --git a/common/common.go b/common/common.go index 7de9046..1d0a5a6 100644 --- a/common/common.go +++ b/common/common.go @@ -117,7 +117,11 @@ func GetSaplingInfo() (int, int, string, string) { func getBlockFromRPC(height int) (*walletrpc.CompactBlock, error) { params := make([]json.RawMessage, 2) - params[0] = json.RawMessage("\"" + strconv.Itoa(height) + "\"") + heightJSON, err := json.Marshal(strconv.Itoa(height)) + if err != nil { + return nil, errors.Wrap(err, "error marshaling height") + } + params[0] = heightJSON params[1] = json.RawMessage("0") // non-verbose (raw hex) result, rpcErr := RawRequest("getblock", params) @@ -131,7 +135,7 @@ func getBlockFromRPC(height int) (*walletrpc.CompactBlock, error) { } var blockDataHex string - err := json.Unmarshal(result, &blockDataHex) + err = json.Unmarshal(result, &blockDataHex) if err != nil { return nil, errors.Wrap(err, "error reading JSON response") } @@ -314,13 +318,16 @@ func GetBlockRange(cache *BlockCache, blockOut chan<- *walletrpc.CompactBlock, e errOut <- nil } -func displayHash(hash []byte) string { - rhash := make([]byte, len(hash)) - copy(rhash, hash) - // Reverse byte order - for i := 0; i < len(rhash)/2; i++ { - j := len(rhash) - 1 - i - rhash[i], rhash[j] = rhash[j], rhash[i] +// Reverse the given byte slice, returning a slice pointing to new data; +// the input slice is unchanged. +func Reverse(a []byte) []byte { + r := make([]byte, len(a), len(a)) + for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 { + r[left], r[right] = a[right], a[left] } - return hex.EncodeToString(rhash) + return r +} + +func displayHash(hash []byte) string { + return hex.EncodeToString(Reverse(hash)) } diff --git a/common/common_test.go b/common/common_test.go index db1f981..5b1b700 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -66,11 +66,8 @@ func TestMain(m *testing.M) { } scan := bufio.NewScanner(testBlocks) for scan.Scan() { // each line (block) - block := scan.Bytes() - // Enclose the hex string in quotes (to make it json, to match what's - // returned by the RPC) - block = []byte("\"" + string(block) + "\"") - blocks = append(blocks, block) + blockJSON, _ := json.Marshal(scan.Text()) + blocks = append(blocks, blockJSON) } // Setup is done; run all tests. diff --git a/common/darkside.go b/common/darkside.go index 9f157c7..0d5852d 100644 --- a/common/darkside.go +++ b/common/darkside.go @@ -404,7 +404,7 @@ func darksideRawRequest(method string, params []json.RawMessage) (json.RawMessag if index >= len(state.activeBlocks) { return nil, errors.New(notFoundErr) } - return []byte("\"" + hex.EncodeToString(state.activeBlocks[index]) + "\""), nil + return json.Marshal(hex.EncodeToString(state.activeBlocks[index])) case "getaddresstxids": // Not required for minimal reorg testing. @@ -435,6 +435,26 @@ func darksideRawRequest(method string, params []json.RawMessage) (json.RawMessag state.incomingTransactions = append(state.incomingTransactions, txBytes) return []byte(hex.EncodeToString(tx.GetDisplayHash())), nil + + case "getrawmempool": + reply := make([]string, 0) + addTxToReply := func(txBytes []byte) { + ctx := parser.NewTransaction() + ctx.ParseFromSlice(txBytes) + reply = append(reply, hex.EncodeToString(ctx.GetDisplayHash())) + } + for _, blockBytes := range state.stagedBlocks { + block := parser.NewBlock() + block.ParseFromSlice(blockBytes) + for _, tx := range block.Transactions() { + addTxToReply(tx.Bytes()) + } + } + for _, tx := range state.stagedTransactions { + addTxToReply(tx.bytes) + } + return json.Marshal(reply) + default: return nil, errors.New("there was an attempt to call an unsupported RPC") } @@ -444,33 +464,63 @@ func darksideGetRawTransaction(params []json.RawMessage) (json.RawMessage, error if !state.resetted { return nil, errors.New("please call Reset first") } - // remove the double-quotes from the beginning and end of the hex txid string - txbytes, err := hex.DecodeString(string(params[0][1 : 1+64])) + var rawtx string + err := json.Unmarshal(params[0], &rawtx) + if err != nil { + return nil, errors.New("failed to parse getrawtransaction JSON") + } + txid, err := hex.DecodeString(rawtx) if err != nil { return nil, errors.New("-9: " + err.Error()) } + marshalReply := func(tx *parser.Transaction, height int) []byte { + switch string(params[1]) { + case "0": + txJSON, _ := json.Marshal(hex.EncodeToString(tx.Bytes())) + return txJSON + case "1": + reply := struct { + Hex string + Height int + }{hex.EncodeToString(tx.Bytes()), height} + txVerboseJSON, _ := json.Marshal(reply) + return txVerboseJSON + default: + Log.Fatal("darkside only recognizes verbose 0 or 1") + return nil + + } + } // Linear search for the tx, somewhat inefficient but this is test code // and there aren't many blocks. If this becomes a performance problem, // we can maintain a map of transactions indexed by txid. - for _, b := range state.activeBlocks { - block := parser.NewBlock() - rest, err := block.ParseFromSlice(b) - if err != nil { - // this would be strange; we've already parsed this block - return nil, errors.New("-9: " + err.Error()) - } - if len(rest) != 0 { - return nil, errors.New("-9: block serialization is too long") - } - for _, tx := range block.Transactions() { - if bytes.Equal(tx.GetDisplayHash(), txbytes) { - reply := struct { - Hex string `json:"hex"` - Height int `json:"height"` - }{hex.EncodeToString(tx.Bytes()), block.GetHeight()} - return json.Marshal(reply) + findTxInBlocks := func(blocks [][]byte) json.RawMessage { + for _, b := range blocks { + block := parser.NewBlock() + _, _ = block.ParseFromSlice(b) + for _, tx := range block.Transactions() { + if bytes.Equal(tx.GetDisplayHash(), txid) { + return marshalReply(tx, block.GetHeight()) + } } } + return nil + } + // Search for the transaction (by txid) in the 3 places it could be. + reply := findTxInBlocks(state.activeBlocks) + if reply != nil { + return reply, nil + } + reply = findTxInBlocks(state.stagedBlocks) + if reply != nil { + return reply, nil + } + for _, stx := range state.stagedTransactions { + tx := parser.NewTransaction() + _, _ = tx.ParseFromSlice(stx.bytes) + if bytes.Equal(tx.GetDisplayHash(), txid) { + return marshalReply(tx, 0), nil + } } return nil, errors.New("-5: No information available about transaction") } diff --git a/docs/darksidewalletd.md b/docs/darksidewalletd.md index d65a253..dbbe5e8 100644 --- a/docs/darksidewalletd.md +++ b/docs/darksidewalletd.md @@ -339,6 +339,16 @@ block files. The sochain block explorer makes it easy to obtain the raw transaction hex, by viewing the transaction (example), clicking “Raw Data”, then copying the “tx_hex” field. +### Simulating the mempool + +The `GetMempoolTx` gRPC will return staged transactions that are either within +staged blocks or that have been staged separately. Here is an example: +``` +grpcurl -plaintext -d '{"saplingActivation": 663150,"branchID": "bad", "chainName":"x"}' localhost:9067 cash.z.wallet.sdk.rpc.DarksideStreamer/Reset +grpcurl -plaintext -d '{"url": "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-incoming/blocks.txt"}' localhost:9067 cash.z.wallet.sdk.rpc.DarksideStreamer/StageBlocks +grpcurl -plaintext -d '{"txid":["qg=="]}' localhost:9067 cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx +``` + ## Use cases Check out some of the potential security test cases here: [wallet <-> diff --git a/frontend/frontend_test.go b/frontend/frontend_test.go index f2119c0..d39d174 100644 --- a/frontend/frontend_test.go +++ b/frontend/frontend_test.go @@ -67,11 +67,8 @@ func TestMain(m *testing.M) { defer testBlocks.Close() scan := bufio.NewScanner(testBlocks) for scan.Scan() { // each line (block) - block := scan.Bytes() - // Enclose the hex string in quotes (to make it json, to match what's - // returned by the RPC) - block = []byte("\"" + string(block) + "\"") - blocks = append(blocks, block) + blockJSON, _ := json.Marshal(scan.Text()) + blocks = append(blocks, blockJSON) } testData, err := os.Open("../testdata/zip243_raw_tx") @@ -242,8 +239,14 @@ func zcashdrpcStub(method string, params []json.RawMessage) (json.RawMessage, er case "getrawtransaction": switch step { case 2: - txstr := hex.EncodeToString(rawTxData[0]) - return []byte("{\"hex\": \"" + txstr + "\", \"height\": 1234567}"), nil + tx := &struct { + Hex string `json:"hex"` + Height int `json:"height"` + }{ + Hex: hex.EncodeToString(rawTxData[0]), + Height: 1234567, + } + return json.Marshal(tx) case 4: // empty return value, should be okay return []byte(""), errors.New("-5: test getrawtransaction error") @@ -500,3 +503,58 @@ func TestNewZRPCFromConf(t *testing.T) { t.Fatal("NewZRPCFromClient unexpected success") } } + +func TestMempoolFilter(t *testing.T) { + txidlist := []string{ + "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", + "29e594c312eee49bc2c9ad37367ba58f857c4a7387ec9715", + "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", + "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", + "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", + "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", + } + exclude := []string{ + "98aa", // common prefix (98) but no match + "19", // no match + "29", // one match (should not appear) + "d4", // 2 matches (both should appear in result) + "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", // exact match + "ce5a28854a509ab309faa433542e73414fef6e903a3d52f500", // extra stuff ignored + } + expected := []string{ + "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", + "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", + "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", + "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", + } + actual := MempoolFilter(txidlist, exclude) + if len(actual) != len(expected) { + t.Fatal("mempool: wrong number of filter results") + } + for i := 0; i < len(actual); i++ { + if actual[i] != expected[i] { + t.Fatal(fmt.Sprintf("mempool: expected: %s actual: %s", + expected[i], actual[i])) + } + } + // If the exclude list is empty, return the entire mempool. + actual = MempoolFilter(txidlist, []string{}) + expected = []string{ + "29e594c312eee49bc2c9ad37367ba58f857c4a7387ec9715", + "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", + "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", + "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", + "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", + "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", + } + if len(actual) != len(expected) { + t.Fatal("mempool: wrong number of filter results") + } + for i := 0; i < len(actual); i++ { + if actual[i] != expected[i] { + t.Fatal(fmt.Sprintf("mempool: expected: %s actual: %s", + expected[i], actual[i])) + } + } + +} diff --git a/frontend/service.go b/frontend/service.go index dd0bbc4..8399318 100644 --- a/frontend/service.go +++ b/frontend/service.go @@ -12,12 +12,14 @@ import ( "errors" "io" "regexp" + "sort" "strconv" "strings" "sync/atomic" "time" "github.com/zcash/lightwalletd/common" + "github.com/zcash/lightwalletd/parser" "github.com/zcash/lightwalletd/walletrpc" ) @@ -63,11 +65,16 @@ func (s *lwdStreamer) GetTaddressTxids(addressBlockFilter *walletrpc.Transparent } params := make([]json.RawMessage, 1) - st := "{\"addresses\": [\"" + addressBlockFilter.Address + "\"]," + - "\"start\": " + strconv.FormatUint(addressBlockFilter.Range.Start.Height, 10) + - ", \"end\": " + strconv.FormatUint(addressBlockFilter.Range.End.Height, 10) + "}" - - params[0] = json.RawMessage(st) + request := &struct { + Addresses []string `json:"addresses"` + Start uint64 `json:"start"` + End uint64 `json:"end"` + }{ + Addresses: []string{addressBlockFilter.Address}, + Start: addressBlockFilter.Range.Start.Height, + End: addressBlockFilter.Range.End.Height, + } + params[0], _ = json.Marshal(request) result, rpcErr := common.RawRequest("getaddresstxids", params) @@ -91,10 +98,7 @@ func (s *lwdStreamer) GetTaddressTxids(addressBlockFilter *walletrpc.Transparent txid, _ := hex.DecodeString(txidstr) // Txid is read as a string, which is in big-endian order. But when converting // to bytes, it should be little-endian - for left, right := 0, len(txid)-1; left < right; left, right = left+1, right-1 { - txid[left], txid[right] = txid[right], txid[left] - } - tx, err := s.GetTransaction(timeout, &walletrpc.TxFilter{Hash: txid}) + tx, err := s.GetTransaction(timeout, &walletrpc.TxFilter{Hash: common.Reverse(txid)}) if err != nil { common.Log.Errorf("GetTransaction error: %s", err.Error()) return err @@ -154,17 +158,15 @@ func (s *lwdStreamer) GetBlockRange(span *walletrpc.BlockRange, resp walletrpc.C // by the zcashd 'getrawtransaction' RPC. func (s *lwdStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilter) (*walletrpc.RawTransaction, error) { if txf.Hash != nil { - txid := txf.Hash - for left, right := 0, len(txid)-1; left < right; left, right = left+1, right-1 { - txid[left], txid[right] = txid[right], txid[left] + leHashStringJSON, err := json.Marshal(hex.EncodeToString(txf.Hash)) + if err != nil { + common.Log.Errorf("GetTransaction: cannot encode txid: %s", err.Error()) + return nil, err } - leHashString := hex.EncodeToString(txid) - params := []json.RawMessage{ - json.RawMessage("\"" + leHashString + "\""), + leHashStringJSON, json.RawMessage("1"), } - result, rpcErr := common.RawRequest("getrawtransaction", params) // For some reason, the error responses are not JSON @@ -172,11 +174,12 @@ func (s *lwdStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte common.Log.Errorf("GetTransaction error: %s", rpcErr.Error()) return nil, errors.New((strings.Split(rpcErr.Error(), ":"))[0]) } + // Many other fields are returned, but we need only these two. var txinfo struct { Hex string Height int } - err := json.Unmarshal(result, &txinfo) + err = json.Unmarshal(result, &txinfo) if err != nil { return nil, err } @@ -229,8 +232,8 @@ func (s *lwdStreamer) SendTransaction(ctx context.Context, rawtx *walletrpc.RawT // Construct raw JSON-RPC params params := make([]json.RawMessage, 1) - txHexString := hex.EncodeToString(rawtx.Data) - params[0] = json.RawMessage("\"" + txHexString + "\"") + txJSON, _ := json.Marshal(hex.EncodeToString(rawtx.Data)) + params[0] = txJSON result, rpcErr := common.RawRequest("sendrawtransaction", params) var err error @@ -261,18 +264,13 @@ func (s *lwdStreamer) SendTransaction(ctx context.Context, rawtx *walletrpc.RawT func getTaddressBalanceZcashdRpc(addressList []string) (*walletrpc.Balance, error) { params := make([]json.RawMessage, 1) - addrList := "{\"addresses\":[" - notFirst := false - for _, addr := range addressList { - if notFirst { - addrList += "," - } - addrList += "\"" + addr + "\"" - notFirst = true + addrList := &struct { + Addresses []string `json:"addresses"` + }{ + Addresses: addressList, } - addrList += "]}" + params[0], _ = json.Marshal(addrList) - params[0] = json.RawMessage(addrList) result, rpcErr := common.RawRequest("getaddressbalance", params) if rpcErr != nil { return &walletrpc.Balance{}, rpcErr @@ -313,6 +311,132 @@ func (s *lwdStreamer) GetTaddressBalanceStream(addresses walletrpc.CompactTxStre return nil } +// Key is 32-byte txid (as a 64-character string), data is pointer to compact tx. +var mempoolMap *map[string]*walletrpc.CompactTx +var mempoolList []string + +// Last time we pulled a copy of the mempool from zcashd. +var lastMempool time.Time + +func (s *lwdStreamer) GetMempoolTx(exclude *walletrpc.Exclude, resp walletrpc.CompactTxStreamer_GetMempoolTxServer) error { + if time.Now().Sub(lastMempool).Seconds() >= 2 { + lastMempool = time.Now() + // Refresh our copy of the mempool. + newmempoolMap := make(map[string]*walletrpc.CompactTx) + params := make([]json.RawMessage, 0) + result, rpcErr := common.RawRequest("getrawmempool", params) + if rpcErr != nil { + return rpcErr + } + err := json.Unmarshal(result, &mempoolList) + if err != nil { + return err + } + if mempoolMap == nil { + mempoolMap = &newmempoolMap + } + for _, txidstr := range mempoolList { + if ctx, ok := (*mempoolMap)[txidstr]; ok { + // This ctx has already been fetched, copy pointer to it. + newmempoolMap[txidstr] = ctx + continue + } + txidJSON, _ := json.Marshal(txidstr) + // The "0" is because we only need the raw hex, which is returned as + // just a hex string, and not even a json string (with quotes). + params := []json.RawMessage{txidJSON, json.RawMessage("0")} + result, rpcErr := common.RawRequest("getrawtransaction", params) + if rpcErr != nil { + // Not an error; mempool transactions can disappear + common.Log.Errorf("GetTransaction error: %s", rpcErr.Error()) + continue + } + // strip the quotes + var txStr string + err := json.Unmarshal(result, &txStr) + if err != nil { + return err + } + + // conver to binary + txBytes, err := hex.DecodeString(txStr) + if err != nil { + return err + } + tx := parser.NewTransaction() + txdata, err := tx.ParseFromSlice(txBytes) + if len(txdata) > 0 { + return errors.New("extra data deserializing transaction") + } + newmempoolMap[txidstr] = &walletrpc.CompactTx{} + if tx.HasSaplingElements() { + newmempoolMap[txidstr] = tx.ToCompact( /* height */ 0) + } + } + mempoolMap = &newmempoolMap + } + excludeHex := make([]string, len(exclude.Txid)) + for i := 0; i < len(exclude.Txid); i++ { + excludeHex[i] = hex.EncodeToString(common.Reverse(exclude.Txid[i])) + } + for _, txid := range MempoolFilter(mempoolList, excludeHex) { + tx := (*mempoolMap)[txid] + if len(tx.Hash) > 0 { + err := resp.Send(tx) + if err != nil { + return err + } + } + } + return nil +} + +// Return the subset of items that aren't excluded, but +// if more than one item matches an exclude entry, return +// all those items. +func MempoolFilter(items, exclude []string) []string { + sort.Slice(items, func(i, j int) bool { + return items[i] < items[j] + }) + sort.Slice(exclude, func(i, j int) bool { + return exclude[i] < exclude[j] + }) + // Determine how many items match each exclude item. + nmatches := make([]int, len(exclude)) + // is the exclude string less than the item string? + lessthan := func(e, i string) bool { + l := len(e) + if l > len(i) { + l = len(i) + } + return e < i[0:l] + } + ei := 0 + for _, item := range items { + for ei < len(exclude) && lessthan(exclude[ei], item) { + ei++ + } + match := ei < len(exclude) && strings.HasPrefix(item, exclude[ei]) + if match { + nmatches[ei]++ + } + } + + // Add each item that isn't uniquely excluded to the results. + tosend := make([]string, 0) + ei = 0 + for _, item := range items { + for ei < len(exclude) && lessthan(exclude[ei], item) { + ei++ + } + match := ei < len(exclude) && strings.HasPrefix(item, exclude[ei]) + if !match || nmatches[ei] > 1 { + tosend = append(tosend, item) + } + } + return tosend +} + // This rpc is used only for testing. var concurrent int64 @@ -339,6 +463,8 @@ func (s *DarksideStreamer) Reset(ctx context.Context, ms *walletrpc.DarksideMeta if err != nil { return nil, err } + mempoolMap = nil + mempoolList = nil return &walletrpc.Empty{}, nil } diff --git a/walletrpc/darkside.pb.go b/walletrpc/darkside.pb.go index 2969b87..b44b4a9 100644 --- a/walletrpc/darkside.pb.go +++ b/walletrpc/darkside.pb.go @@ -4,8 +4,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.22.0 -// protoc v3.11.4 +// protoc-gen-go v1.25.0 +// protoc v3.11.0 // source: darkside.proto package walletrpc diff --git a/walletrpc/service.pb.go b/walletrpc/service.pb.go index 2c34c56..54358e0 100644 --- a/walletrpc/service.pb.go +++ b/walletrpc/service.pb.go @@ -809,6 +809,53 @@ func (x *Balance) GetValueZat() int64 { return 0 } +type Exclude struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Txid [][]byte `protobuf:"bytes,1,rep,name=txid,proto3" json:"txid,omitempty"` +} + +func (x *Exclude) Reset() { + *x = Exclude{} + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Exclude) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Exclude) ProtoMessage() {} + +func (x *Exclude) ProtoReflect() protoreflect.Message { + mi := &file_service_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 Exclude.ProtoReflect.Descriptor instead. +func (*Exclude) Descriptor() ([]byte, []int) { + return file_service_proto_rawDescGZIP(), []int{14} +} + +func (x *Exclude) GetTxid() [][]byte { + if x != nil { + return x.Txid + } + return nil +} + var File_service_proto protoreflect.FileDescriptor var file_service_proto_rawDesc = []byte{ @@ -881,68 +928,75 @@ var file_service_proto_rawDesc = []byte{ 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x07, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5a, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5a, 0x61, 0x74, 0x32, 0xab, 0x07, 0x0a, - 0x11, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x54, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x65, 0x72, 0x12, 0x54, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x20, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, - 0x69, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x44, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x49, 0x44, 0x1a, 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6d, - 0x70, 0x61, 0x63, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0d, 0x47, - 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x63, - 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, - 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, - 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x63, 0x61, 0x73, - 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, - 0x70, 0x63, 0x2e, 0x54, 0x78, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x1a, 0x25, 0x2e, 0x63, 0x61, + 0x28, 0x03, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5a, 0x61, 0x74, 0x22, 0x1d, 0x0a, 0x07, + 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x32, 0x81, 0x08, 0x0a, 0x11, + 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x54, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x65, + 0x72, 0x12, 0x54, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x12, 0x20, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x69, + 0x6e, 0x53, 0x70, 0x65, 0x63, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x49, 0x44, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x6f, 0x63, + 0x6b, 0x49, 0x44, 0x1a, 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6d, 0x70, + 0x61, 0x63, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0d, 0x47, 0x65, + 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, - 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x5f, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x23, + 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, - 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x73, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x54, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x54, 0x78, 0x69, 0x64, 0x73, 0x12, 0x34, 0x2e, 0x63, 0x61, 0x73, 0x68, + 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, - 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x1a, - 0x25, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x12, 0x47, 0x65, - 0x74, 0x54, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x12, 0x22, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x22, 0x00, 0x12, 0x5e, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x54, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x12, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x67, - 0x68, 0x74, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, + 0x63, 0x2e, 0x54, 0x78, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x1a, 0x25, 0x2e, 0x63, 0x61, 0x73, + 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x00, 0x12, 0x5f, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x23, 0x2e, + 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, + 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x73, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x54, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x54, 0x78, 0x69, 0x64, 0x73, 0x12, 0x34, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, + 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x1a, 0x25, + 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, + 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x12, 0x47, 0x65, 0x74, + 0x54, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, + 0x22, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, + 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4c, + 0x69, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x22, 0x00, 0x12, 0x5e, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x54, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x12, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x1a, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x54, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x70, + 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x12, 0x1e, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x1a, 0x20, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x61, 0x63, 0x74, 0x54, 0x78, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x0d, 0x47, + 0x65, 0x74, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x63, + 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, + 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x63, 0x61, 0x73, + 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x12, + 0x4e, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, - 0x67, 0x68, 0x74, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x04, 0x50, 0x69, - 0x6e, 0x67, 0x12, 0x1f, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x1a, 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, 0x7a, 0x2e, 0x77, 0x61, 0x6c, - 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x10, 0x5a, 0x0b, 0x2e, 0x3b, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0xba, 0x02, 0x00, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x23, 0x2e, 0x63, 0x61, 0x73, 0x68, 0x2e, + 0x7a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, + 0x10, 0x5a, 0x0b, 0x2e, 0x3b, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0xba, 0x02, + 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -957,7 +1011,7 @@ func file_service_proto_rawDescGZIP() []byte { return file_service_proto_rawDescData } -var file_service_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_service_proto_goTypes = []interface{}{ (*BlockID)(nil), // 0: cash.z.wallet.sdk.rpc.BlockID (*BlockRange)(nil), // 1: cash.z.wallet.sdk.rpc.BlockRange @@ -973,7 +1027,9 @@ var file_service_proto_goTypes = []interface{}{ (*Address)(nil), // 11: cash.z.wallet.sdk.rpc.Address (*AddressList)(nil), // 12: cash.z.wallet.sdk.rpc.AddressList (*Balance)(nil), // 13: cash.z.wallet.sdk.rpc.Balance - (*CompactBlock)(nil), // 14: cash.z.wallet.sdk.rpc.CompactBlock + (*Exclude)(nil), // 14: cash.z.wallet.sdk.rpc.Exclude + (*CompactBlock)(nil), // 15: cash.z.wallet.sdk.rpc.CompactBlock + (*CompactTx)(nil), // 16: cash.z.wallet.sdk.rpc.CompactTx } var file_service_proto_depIdxs = []int32{ 0, // 0: cash.z.wallet.sdk.rpc.BlockRange.start:type_name -> cash.z.wallet.sdk.rpc.BlockID @@ -988,20 +1044,22 @@ var file_service_proto_depIdxs = []int32{ 8, // 9: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressTxids:input_type -> cash.z.wallet.sdk.rpc.TransparentAddressBlockFilter 12, // 10: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalance:input_type -> cash.z.wallet.sdk.rpc.AddressList 11, // 11: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalanceStream:input_type -> cash.z.wallet.sdk.rpc.Address - 6, // 12: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLightdInfo:input_type -> cash.z.wallet.sdk.rpc.Empty - 9, // 13: cash.z.wallet.sdk.rpc.CompactTxStreamer.Ping:input_type -> cash.z.wallet.sdk.rpc.Duration - 0, // 14: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLatestBlock:output_type -> cash.z.wallet.sdk.rpc.BlockID - 14, // 15: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetBlock:output_type -> cash.z.wallet.sdk.rpc.CompactBlock - 14, // 16: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetBlockRange:output_type -> cash.z.wallet.sdk.rpc.CompactBlock - 3, // 17: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTransaction:output_type -> cash.z.wallet.sdk.rpc.RawTransaction - 4, // 18: cash.z.wallet.sdk.rpc.CompactTxStreamer.SendTransaction:output_type -> cash.z.wallet.sdk.rpc.SendResponse - 3, // 19: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressTxids:output_type -> cash.z.wallet.sdk.rpc.RawTransaction - 13, // 20: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalance:output_type -> cash.z.wallet.sdk.rpc.Balance - 13, // 21: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalanceStream:output_type -> cash.z.wallet.sdk.rpc.Balance - 7, // 22: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLightdInfo:output_type -> cash.z.wallet.sdk.rpc.LightdInfo - 10, // 23: cash.z.wallet.sdk.rpc.CompactTxStreamer.Ping:output_type -> cash.z.wallet.sdk.rpc.PingResponse - 14, // [14:24] is the sub-list for method output_type - 4, // [4:14] is the sub-list for method input_type + 14, // 12: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetMempoolTx:input_type -> cash.z.wallet.sdk.rpc.Exclude + 6, // 13: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLightdInfo:input_type -> cash.z.wallet.sdk.rpc.Empty + 9, // 14: cash.z.wallet.sdk.rpc.CompactTxStreamer.Ping:input_type -> cash.z.wallet.sdk.rpc.Duration + 0, // 15: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLatestBlock:output_type -> cash.z.wallet.sdk.rpc.BlockID + 15, // 16: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetBlock:output_type -> cash.z.wallet.sdk.rpc.CompactBlock + 15, // 17: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetBlockRange:output_type -> cash.z.wallet.sdk.rpc.CompactBlock + 3, // 18: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTransaction:output_type -> cash.z.wallet.sdk.rpc.RawTransaction + 4, // 19: cash.z.wallet.sdk.rpc.CompactTxStreamer.SendTransaction:output_type -> cash.z.wallet.sdk.rpc.SendResponse + 3, // 20: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressTxids:output_type -> cash.z.wallet.sdk.rpc.RawTransaction + 13, // 21: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalance:output_type -> cash.z.wallet.sdk.rpc.Balance + 13, // 22: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetTaddressBalanceStream:output_type -> cash.z.wallet.sdk.rpc.Balance + 16, // 23: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetMempoolTx:output_type -> cash.z.wallet.sdk.rpc.CompactTx + 7, // 24: cash.z.wallet.sdk.rpc.CompactTxStreamer.GetLightdInfo:output_type -> cash.z.wallet.sdk.rpc.LightdInfo + 10, // 25: cash.z.wallet.sdk.rpc.CompactTxStreamer.Ping:output_type -> cash.z.wallet.sdk.rpc.PingResponse + 15, // [15:26] is the sub-list for method output_type + 4, // [4:15] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name 4, // [4:4] is the sub-list for extension extendee 0, // [0:4] is the sub-list for field type_name @@ -1182,6 +1240,18 @@ func file_service_proto_init() { return nil } } + file_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Exclude); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1189,7 +1259,7 @@ func file_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_service_proto_rawDesc, NumEnums: 0, - NumMessages: 14, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, @@ -1229,6 +1299,7 @@ type CompactTxStreamerClient interface { GetTaddressTxids(ctx context.Context, in *TransparentAddressBlockFilter, opts ...grpc.CallOption) (CompactTxStreamer_GetTaddressTxidsClient, error) GetTaddressBalance(ctx context.Context, in *AddressList, opts ...grpc.CallOption) (*Balance, error) GetTaddressBalanceStream(ctx context.Context, opts ...grpc.CallOption) (CompactTxStreamer_GetTaddressBalanceStreamClient, error) + GetMempoolTx(ctx context.Context, in *Exclude, opts ...grpc.CallOption) (CompactTxStreamer_GetMempoolTxClient, error) // Return information about this lightwalletd instance and the blockchain GetLightdInfo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*LightdInfo, error) // Testing-only @@ -1386,6 +1457,38 @@ func (x *compactTxStreamerGetTaddressBalanceStreamClient) CloseAndRecv() (*Balan return m, nil } +func (c *compactTxStreamerClient) GetMempoolTx(ctx context.Context, in *Exclude, opts ...grpc.CallOption) (CompactTxStreamer_GetMempoolTxClient, error) { + stream, err := c.cc.NewStream(ctx, &_CompactTxStreamer_serviceDesc.Streams[3], "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx", opts...) + if err != nil { + return nil, err + } + x := &compactTxStreamerGetMempoolTxClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type CompactTxStreamer_GetMempoolTxClient interface { + Recv() (*CompactTx, error) + grpc.ClientStream +} + +type compactTxStreamerGetMempoolTxClient struct { + grpc.ClientStream +} + +func (x *compactTxStreamerGetMempoolTxClient) Recv() (*CompactTx, error) { + m := new(CompactTx) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + func (c *compactTxStreamerClient) GetLightdInfo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*LightdInfo, error) { out := new(LightdInfo) err := c.cc.Invoke(ctx, "/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo", in, out, opts...) @@ -1420,6 +1523,7 @@ type CompactTxStreamerServer interface { GetTaddressTxids(*TransparentAddressBlockFilter, CompactTxStreamer_GetTaddressTxidsServer) error GetTaddressBalance(context.Context, *AddressList) (*Balance, error) GetTaddressBalanceStream(CompactTxStreamer_GetTaddressBalanceStreamServer) error + GetMempoolTx(*Exclude, CompactTxStreamer_GetMempoolTxServer) error // Return information about this lightwalletd instance and the blockchain GetLightdInfo(context.Context, *Empty) (*LightdInfo, error) // Testing-only @@ -1454,6 +1558,9 @@ func (*UnimplementedCompactTxStreamerServer) GetTaddressBalance(context.Context, func (*UnimplementedCompactTxStreamerServer) GetTaddressBalanceStream(CompactTxStreamer_GetTaddressBalanceStreamServer) error { return status.Errorf(codes.Unimplemented, "method GetTaddressBalanceStream not implemented") } +func (*UnimplementedCompactTxStreamerServer) GetMempoolTx(*Exclude, CompactTxStreamer_GetMempoolTxServer) error { + return status.Errorf(codes.Unimplemented, "method GetMempoolTx not implemented") +} func (*UnimplementedCompactTxStreamerServer) GetLightdInfo(context.Context, *Empty) (*LightdInfo, error) { return nil, status.Errorf(codes.Unimplemented, "method GetLightdInfo not implemented") } @@ -1623,6 +1730,27 @@ func (x *compactTxStreamerGetTaddressBalanceStreamServer) Recv() (*Address, erro return m, nil } +func _CompactTxStreamer_GetMempoolTx_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Exclude) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(CompactTxStreamerServer).GetMempoolTx(m, &compactTxStreamerGetMempoolTxServer{stream}) +} + +type CompactTxStreamer_GetMempoolTxServer interface { + Send(*CompactTx) error + grpc.ServerStream +} + +type compactTxStreamerGetMempoolTxServer struct { + grpc.ServerStream +} + +func (x *compactTxStreamerGetMempoolTxServer) Send(m *CompactTx) error { + return x.ServerStream.SendMsg(m) +} + func _CompactTxStreamer_GetLightdInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Empty) if err := dec(in); err != nil { @@ -1708,6 +1836,11 @@ var _CompactTxStreamer_serviceDesc = grpc.ServiceDesc{ Handler: _CompactTxStreamer_GetTaddressBalanceStream_Handler, ClientStreams: true, }, + { + StreamName: "GetMempoolTx", + Handler: _CompactTxStreamer_GetMempoolTx_Handler, + ServerStreams: true, + }, }, Metadata: "service.proto", } diff --git a/walletrpc/service.proto b/walletrpc/service.proto index 416e8c3..a32b889 100644 --- a/walletrpc/service.proto +++ b/walletrpc/service.proto @@ -96,6 +96,10 @@ message Balance { int64 valueZat = 1; } +message Exclude { + repeated bytes txid = 1; +} + service CompactTxStreamer { // Return the height of the tip of the best chain rpc GetLatestBlock(ChainSpec) returns (BlockID) {} @@ -114,6 +118,17 @@ service CompactTxStreamer { rpc GetTaddressBalance(AddressList) returns (Balance) {} rpc GetTaddressBalanceStream(stream Address) returns (Balance) {} + // Return the compact transactions currently in the mempool; the results + // can be a few seconds out of date. If the Exclude list is empty, return + // all transactions; otherwise return all *except* those in the Exclude list + // (if any); this allows the client to avoid receiving transactions that it + // already has (from an earlier call to this rpc). The transaction IDs in the + // Exclude list can be shortened to any number of bytes to make the request + // more bandwidth-efficient; if two or more transactions in the mempool + // match a shortened txid, they are all sent (none is excluded). Transactions + // in the exclude list that don't exist in the mempool are ignored. + rpc GetMempoolTx(Exclude) returns (stream CompactTx) {} + // Return information about this lightwalletd instance and the blockchain rpc GetLightdInfo(Empty) returns (LightdInfo) {} // Testing-only