From d35568b3b02eacfbdb3fdc50ff825bc7672eff65 Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Sat, 24 Jan 2026 19:47:57 -0500 Subject: [PATCH 01/10] feat(sharding): add label-driven predicate routing (DGR-157) Add @label(name) directive to predicates, allowing them to be pinned to specific alpha groups that share the same label. This creates implicit sub-clusters for data isolation. Key changes: - Add label field to Member, Tablet, and SchemaUpdate protos - Parse @label(X) directive in schema (schema/parse.go) - Route labeled predicates to matching labeled groups in Zero - Require explicit label parameter in Tablet(), BelongsTo(), ForceTablet() - Block moveTablet for labeled predicates - Prevent unlabeled predicates from being assigned to labeled groups Label routing flow: 1. Alpha with --label=X registers with Zero, Member.Label is set 2. Schema with @label(X) is parsed, SchemaUpdate.Label is set 3. Zero routes tablet to group where Member.Label matches 4. Rebalancer skips tablets with non-empty Label All tablet lookup functions now require explicit label parameter to ensure correct routing for new predicates during schema mutations. --- .gitignore | 4 + dgraph/cmd/alpha/run.go | 4 + dgraph/cmd/zero/raft.go | 7 + dgraph/cmd/zero/tablet.go | 14 +- dgraph/cmd/zero/zero.go | 113 ++- go.mod | 2 +- go.sum | 4 +- protos/pb.proto | 8 +- protos/pb/pb.pb.go | 1608 +++++++++++++++--------------- schema/parse.go | 26 + schema/schema.go | 8 + systest/label/docker-compose.yml | 73 ++ systest/label/label_test.go | 392 ++++++++ testutil/zero.go | 2 + worker/groups.go | 58 +- worker/mutation.go | 29 +- worker/online_restore.go | 4 +- worker/predicate_move.go | 4 +- worker/proposal.go | 33 +- x/config.go | 2 + 20 files changed, 1568 insertions(+), 827 deletions(-) create mode 100644 systest/label/docker-compose.yml create mode 100644 systest/label/label_test.go diff --git a/.gitignore b/.gitignore index e1740a52822..8dace988b71 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ systest/bulk_live/live/**/*.rdf systest/bulk_live/live/**/*.txt x/log_test/*.enc *.buf + +## +.claude/ +CLAUDE.md diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 1d696c516d6..c541d3c2748 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -114,6 +114,9 @@ they form a Raft group and provide synchronous replication. flag.Bool("mcp", false, "run MCP server along with alpha.") + flag.String("label", "", + "Label for this alpha. Labeled alphas only serve predicates with matching @label directive.") + // By default Go GRPC traces all requests. grpc.EnableTracing = false @@ -750,6 +753,7 @@ func run() { AclPublicKey: keys.AclPublicKey, Audit: opts.Audit != nil, Badger: bopts, + Label: Alpha.Conf.GetString("label"), } x.WorkerConfig.Parse(Alpha.Conf) diff --git a/dgraph/cmd/zero/raft.go b/dgraph/cmd/zero/raft.go index 2c0cabc727b..1c82c60ea3a 100644 --- a/dgraph/cmd/zero/raft.go +++ b/dgraph/cmd/zero/raft.go @@ -335,6 +335,13 @@ func (n *node) handleTablet(tablet *pb.Tablet) error { if tablet.Force { originalGroup := state.Groups[prev.GroupId] delete(originalGroup.Tablets, tablet.Predicate) + } else if tablet.Label != "" && prev.Label != tablet.Label { + // Allow re-routing when labels differ. This happens when a schema with @label + // is applied after the predicate was created without a label. + glog.Infof("Tablet for attr: [%s] re-routing from group %d to %d due to label change (%q -> %q)", + tablet.Predicate, prev.GroupId, tablet.GroupId, prev.Label, tablet.Label) + originalGroup := state.Groups[prev.GroupId] + delete(originalGroup.Tablets, tablet.Predicate) } else if prev.GroupId != tablet.GroupId { glog.Infof( "Tablet for attr: [%s], gid: [%d] already served by group: [%d]\n", diff --git a/dgraph/cmd/zero/tablet.go b/dgraph/cmd/zero/tablet.go index e8f5c67728f..78073775897 100644 --- a/dgraph/cmd/zero/tablet.go +++ b/dgraph/cmd/zero/tablet.go @@ -140,6 +140,13 @@ func (s *Server) movePredicate(predicate string, srcGroup, dstGroup uint32) erro if tab == nil { return errors.Errorf("Tablet to be moved: [%v] is not being served", predicate) } + dstGroupLabel := s.groupLabel(dstGroup) + if dstGroupLabel != tab.Label { + // Don't allow a predicate to be moved to a group that doesn't share it's label. + // (label will be empty string on either if unassigned) + return errors.Errorf("Unable to move predicate [%v] with label '%s' to group with label '%s'", + predicate, tab.Label, dstGroupLabel) + } msg := fmt.Sprintf("Going to move predicate: [%v], size: [ondisk: %v, uncompressed: %v]"+ " from group %d to %d\n", predicate, humanize.IBytes(uint64(tab.OnDiskBytes)), humanize.IBytes(uint64(tab.UncompressedBytes)), srcGroup, dstGroup) @@ -266,11 +273,14 @@ func (s *Server) chooseTablet() (predicate string, srcGroup uint32, dstGroup uin size := int64(0) group := s.state.Groups[srcGroup] for _, tab := range group.Tablets { - // Reserved predicates should always be in group 1 so do not re-balance them. if x.IsReservedPredicate(tab.Predicate) { + // Reserved predicates should always be in group 1 so do not re-balance them. + continue + } + if tab.Label != "" { + // labeled predicates are pinned and should not be re-balanced either continue } - // Finds a tablet as big a possible such that on moving it dstGroup's size is // less than or equal to srcGroup. if tab.OnDiskBytes <= sizeDiff/2 && tab.OnDiskBytes > size { diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index 8903ec4a52c..ed7f7894826 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -439,8 +439,13 @@ func (s *Server) Inform(ctx context.Context, req *pb.TabletRequest) (*pb.TabletR // Set the tablet to be served by this server's group. var proposal pb.ZeroProposal proposal.Tablets = make([]*pb.Tablet, 0) + + // Acquire read lock for label-related lookups + s.RLock() for _, t := range unknownTablets { - if x.IsReservedPredicate(t.Predicate) { + glog.Infof("Zero.Inform: routing tablet %s (label=%q, groupId=%d)", t.Predicate, t.Label, t.GroupId) + switch { + case x.IsReservedPredicate(t.Predicate): // Force all the reserved predicates to be allocated to group 1. // This is to make it easier to stream ACL updates to all alpha servers // since they only need to open one pipeline to receive updates for all @@ -448,10 +453,29 @@ func (s *Server) Inform(ctx context.Context, req *pb.TabletRequest) (*pb.TabletR // This will also make it easier to restore the reserved predicates after // a DropAll operation. t.GroupId = 1 + case t.Label != "": + { + // Labeled predicate: route to matching labeled group + gid, err := s.labelGroupId(t.Label) + if err != nil { + s.RUnlock() + return nil, err + } + glog.Infof("Zero.Inform: labeled predicate %s (label=%q) routed to group %d", t.Predicate, t.Label, gid) + t.GroupId = gid + } + case s.isLabeledGroupId(t.GroupId): + // make sure unlabeled predicates don't go an labeled group + gid, err := s.firstUnlabeledGroupId() + if err != nil { + s.RUnlock() + return nil, err + } + t.GroupId = gid } proposal.Tablets = append(proposal.Tablets, t) } - + s.RUnlock() if err := s.Node.proposeAndWait(ctx, &proposal); err != nil && err != errTabletAlreadyServed { span.AddEvent(fmt.Sprintf("Error proposing tablet: %+v. Error: %v", &proposal, err)) return nil, err @@ -682,10 +706,20 @@ func (s *Server) ShouldServe( tab := s.ServingTablet(tablet.Predicate) span.SetAttributes(attribute.String("tablet_predicate", tablet.Predicate)) if tab != nil && !tablet.Force { - // Someone is serving this tablet. Could be the caller as well. - // The caller should compare the returned group against the group it holds to check who's - // serving. - return tab, nil + // If the existing tablet has a different label than requested, we need to re-route. + // This can happen when a schema is applied with @label after the predicate was + // created without a label (e.g., during DropAll). + if tablet.Label != "" && tab.Label != tablet.Label { + glog.Infof("ShouldServe: tablet %s has label %q but request has label %q, re-routing", + tablet.Predicate, tab.Label, tablet.Label) + // Fall through to re-assign the tablet with the new label + // The handleTablet function will allow this because labels differ + } else { + // Someone is serving this tablet. Could be the caller as well. + // The caller should compare the returned group against the group it holds to check who's + // serving. + return tab, nil + } } // Read-only requests should return an empty tablet instead of asking zero @@ -697,7 +731,10 @@ func (s *Server) ShouldServe( // Set the tablet to be served by this server's group. var proposal pb.ZeroProposal - if x.IsReservedPredicate(tablet.Predicate) { + // Acquire read lock for label-related lookups + s.RLock() + switch { + case x.IsReservedPredicate(tablet.Predicate): // Force all the reserved predicates to be allocated to group 1. // This is to make it easier to stream ACL updates to all alpha servers // since they only need to open one pipeline to receive updates for all @@ -705,7 +742,25 @@ func (s *Server) ShouldServe( // This will also make it easier to restore the reserved predicates after // a DropAll operation. tablet.GroupId = 1 + case tablet.Label != "": + // Labeled predicate: route to matching labeled group + gid, err := s.labelGroupId(tablet.Label) + if err != nil { + s.RUnlock() + return nil, err + } + glog.Infof("ShouldServe: labeled predicate %s (label=%q) routed to group %d", tablet.Predicate, tablet.Label, gid) + tablet.GroupId = gid + case s.isLabeledGroupId(tablet.GroupId): + // Make sure unlabeled predicates don't go to a labeled group + gid, err := s.firstUnlabeledGroupId() + if err != nil { + s.RUnlock() + return nil, err + } + tablet.GroupId = gid } + s.RUnlock() proposal.Tablet = tablet if err := s.Node.proposeAndWait(ctx, &proposal); err != nil && err != errTabletAlreadyServed { span.AddEvent(fmt.Sprintf("Error proposing tablet: %+v. Error: %v", &proposal, err)) @@ -862,3 +917,47 @@ func (s *Server) latestMembershipState(ctx context.Context) (*pb.MembershipState } return ms, nil } + +// groupLabel returns the label for a group (from first labeled member found) +func (s *Server) groupLabel(gid uint32) string { + s.AssertRLock() + group := s.state.Groups[gid] + if group == nil { + return "" + } + for _, member := range group.Members { + if member.Label != "" { + return member.Label + } + } + return "" +} + +// labelGroupId the group ID that has the given label, or 0 if none +func (s *Server) labelGroupId(label string) (uint32, error) { + s.AssertRLock() + for gid, group := range s.state.Groups { + for _, member := range group.Members { + if member.Label == label { + return gid, nil + } + } + } + return 0, errors.Errorf("No alpha group with label '%s' found", label) +} + +// isLabeledGroupId returns true if any member in the group has a label +func (s *Server) isLabeledGroupId(gid uint32) bool { + s.AssertRLock() + return s.groupLabel(gid) != "" +} + +func (s *Server) firstUnlabeledGroupId() (uint32, error) { + s.AssertRLock() + for gid := range s.state.Groups { + if !s.isLabeledGroupId(gid) { + return gid, nil + } + } + return 0, errors.Errorf("No unlabeled alpha groups exist.") +} diff --git a/go.mod b/go.mod index f599a76fb1a..c847110f01b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/IBM/sarama v1.46.3 github.com/Masterminds/semver/v3 v3.4.0 github.com/blevesearch/bleve/v2 v2.5.4 - github.com/dgraph-io/badger/v4 v4.8.0 + github.com/dgraph-io/badger/v4 v4.9.0 github.com/dgraph-io/dgo/v250 v250.0.0 github.com/dgraph-io/gqlgen v0.13.2 github.com/dgraph-io/gqlparser/v2 v2.2.2 diff --git a/go.sum b/go.sum index 74f2869cc10..eb2b7ba609d 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= -github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= +github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc= +github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/dgo/v250 v250.0.0 h1:zkVj8EOgNOK3s5XFEK7CJKRdftWqg5K6qGs4HEH5TcY= github.com/dgraph-io/dgo/v250 v250.0.0/go.mod h1:OVSaapUnuqaY4beLe98CajukINwbVm0JRNp0SRBCz/w= github.com/dgraph-io/gqlgen v0.13.2 h1:TNhndk+eHKj5qE7BenKKSYdSIdOGhLqxR1rCiMso9KM= diff --git a/protos/pb.proto b/protos/pb.proto index 7352d77c4f4..0cc462e5bf1 100644 --- a/protos/pb.proto +++ b/protos/pb.proto @@ -11,7 +11,6 @@ syntax = "proto3"; package pb; - import "github.com/dgraph-io/dgo/v250/protos/api.proto"; import "github.com/dgraph-io/badger/v4/pb/pb.proto"; import "google/protobuf/descriptor.proto"; @@ -125,6 +124,7 @@ message Member { bool clusterInfoOnly = 13; bool forceGroupId = 14; + string label = 15; // Label for sharding affinity } message Group { @@ -200,6 +200,7 @@ message Tablet { bool readOnly = 9; // If true, do not ask zero to serve any tablets. uint64 moveTs = 10; int64 uncompressed_bytes = 11; // Estimated uncompressed size of tablet in bytes + string label = 12; // Label from predicate schema (for routing) } message DirectedEdge { @@ -485,6 +486,7 @@ message SchemaUpdate { bool lang = 9; bool unique = 14; + // Fields required for type system. bool non_nullable = 10; bool non_nullable_list = 11; @@ -495,6 +497,10 @@ message SchemaUpdate { bool no_conflict = 13; + string label = 16; // Label from @label directive (for sharding affinity) + + + // Deleted field: reserved 7; reserved "explicit"; diff --git a/protos/pb/pb.pb.go b/protos/pb/pb.pb.go index d70dd16a43c..b156e9f0492 100644 --- a/protos/pb/pb.pb.go +++ b/protos/pb/pb.pb.go @@ -1347,6 +1347,7 @@ type Member struct { Learner bool `protobuf:"varint,7,opt,name=learner,proto3" json:"learner,omitempty"` ClusterInfoOnly bool `protobuf:"varint,13,opt,name=clusterInfoOnly,proto3" json:"clusterInfoOnly,omitempty"` ForceGroupId bool `protobuf:"varint,14,opt,name=forceGroupId,proto3" json:"forceGroupId,omitempty"` + Label string `protobuf:"bytes,15,opt,name=label,proto3" json:"label,omitempty"` // Label for sharding affinity } func (x *Member) Reset() { @@ -1444,6 +1445,13 @@ func (x *Member) GetForceGroupId() bool { return false } +func (x *Member) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type Group struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1976,6 +1984,7 @@ type Tablet struct { ReadOnly bool `protobuf:"varint,9,opt,name=readOnly,proto3" json:"readOnly,omitempty"` // If true, do not ask zero to serve any tablets. MoveTs uint64 `protobuf:"varint,10,opt,name=moveTs,proto3" json:"moveTs,omitempty"` UncompressedBytes int64 `protobuf:"varint,11,opt,name=uncompressed_bytes,json=uncompressedBytes,proto3" json:"uncompressed_bytes,omitempty"` // Estimated uncompressed size of tablet in bytes + Label string `protobuf:"bytes,12,opt,name=label,proto3" json:"label,omitempty"` // Label from predicate schema (for routing) } func (x *Tablet) Reset() { @@ -2066,6 +2075,13 @@ func (x *Tablet) GetUncompressedBytes() int64 { return 0 } +func (x *Tablet) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type DirectedEdge struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3878,6 +3894,7 @@ type SchemaUpdate struct { // custom name. This field stores said name. ObjectTypeName string `protobuf:"bytes,12,opt,name=object_type_name,json=objectTypeName,proto3" json:"object_type_name,omitempty"` NoConflict bool `protobuf:"varint,13,opt,name=no_conflict,json=noConflict,proto3" json:"no_conflict,omitempty"` + Label string `protobuf:"bytes,16,opt,name=label,proto3" json:"label,omitempty"` // Label from @label directive (for sharding affinity) IndexSpecs []*VectorIndexSpec `protobuf:"bytes,15,rep,name=index_specs,json=indexSpecs,proto3" json:"index_specs,omitempty"` } @@ -4004,6 +4021,13 @@ func (x *SchemaUpdate) GetNoConflict() bool { return false } +func (x *SchemaUpdate) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + func (x *SchemaUpdate) GetIndexSpecs() []*VectorIndexSpec { if x != nil { return x.IndexSpecs @@ -6192,7 +6216,7 @@ var file_pb_proto_rawDesc = []byte{ 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x69, 0x73, 0x4c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x72, 0x22, 0xfe, 0x01, 0x0a, 0x06, 0x4d, 0x65, + 0x69, 0x73, 0x4c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x72, 0x22, 0x94, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x12, @@ -6208,803 +6232,807 @@ var file_pb_proto_rawDesc = []byte{ 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0xdd, 0x02, 0x0a, 0x05, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x12, 0x30, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x6d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x63, 0x68, - 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x73, 0x1a, 0x46, 0x0a, 0x0c, 0x4d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, - 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x46, 0x0a, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x04, 0x0a, 0x0c, 0x5a, - 0x65, 0x72, 0x6f, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x41, 0x0a, 0x0b, 0x73, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x20, 0x2e, 0x70, 0x62, 0x2e, 0x5a, 0x65, 0x72, 0x6f, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, - 0x61, 0x6c, 0x2e, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x12, 0x22, - 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x12, 0x22, 0x0a, 0x06, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x06, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x55, 0x49, 0x44, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x55, 0x49, 0x44, 0x12, 0x1a, - 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, - 0x78, 0x4e, 0x73, 0x49, 0x44, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x6d, 0x61, 0x78, - 0x4e, 0x73, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, - 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x78, 0x52, 0x61, 0x66, 0x74, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x03, 0x74, 0x78, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x52, 0x03, 0x74, 0x78, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x63, 0x69, 0x64, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x5a, - 0x65, 0x72, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x30, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, - 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x12, 0x24, 0x0a, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x1a, 0x3d, 0x0a, - 0x0f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x08, - 0x10, 0x09, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x22, 0xd0, 0x03, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, - 0x34, 0x0a, 0x05, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x2e, 0x5a, 0x65, 0x72, 0x6f, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, - 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x55, 0x49, 0x44, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x55, 0x49, 0x44, 0x12, 0x1a, 0x0a, - 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, - 0x4e, 0x73, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x4e, - 0x73, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, - 0x64, 0x12, 0x24, 0x0a, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x07, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x07, - 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x69, 0x64, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x69, 0x64, 0x1a, 0x44, 0x0a, 0x0b, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x44, 0x0a, 0x0a, 0x5a, 0x65, 0x72, 0x6f, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x81, 0x01, 0x0a, 0x0f, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x22, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x06, 0x6d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, - 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, - 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, - 0xb8, 0x02, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, - 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, - 0x75, 0x70, 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, 0x16, 0x0a, 0x06, - 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x70, - 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x63, 0x68, 0x6f, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x63, 0x68, 0x6f, - 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x08, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x07, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x65, 0x5f, 0x66, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x65, 0x46, - 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, - 0x61, 0x78, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x22, 0xf5, 0x01, 0x0a, 0x06, 0x54, - 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, + 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x22, 0xdd, 0x02, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x30, 0x0a, 0x07, 0x6d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x62, + 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x07, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, 0x73, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x08, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x12, 0x23, 0x0a, 0x0d, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x73, + 0x1a, 0x46, 0x0a, 0x0c, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x46, 0x0a, 0x0c, 0x54, 0x61, 0x62, 0x6c, + 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x8b, 0x04, 0x0a, 0x0c, 0x5a, 0x65, 0x72, 0x6f, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, + 0x6c, 0x12, 0x41, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x62, 0x2e, 0x5a, 0x65, 0x72, 0x6f, + 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x2e, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x54, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x54, 0x73, 0x12, 0x22, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x06, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, + 0x62, 0x6c, 0x65, 0x74, 0x52, 0x06, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x6d, 0x61, 0x78, 0x55, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x61, + 0x78, 0x55, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x4e, 0x73, 0x49, 0x44, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x4e, 0x73, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, + 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, + 0x61, 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x03, 0x74, 0x78, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x78, 0x6e, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x74, 0x78, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x63, + 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x69, 0x64, 0x12, 0x2c, 0x0a, + 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x5a, 0x65, 0x72, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x30, 0x0a, 0x09, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x08, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x12, 0x24, 0x0a, + 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, + 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x74, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x54, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x22, 0xd0, + 0x03, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x06, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, + 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x34, 0x0a, 0x05, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x5a, 0x65, 0x72, 0x6f, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, + 0x61, 0x78, 0x55, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x61, 0x78, + 0x55, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x6e, 0x54, 0x73, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x4e, 0x73, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x07, 0x6d, 0x61, 0x78, 0x4e, 0x73, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x78, + 0x52, 0x61, 0x66, 0x74, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, + 0x78, 0x52, 0x61, 0x66, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x64, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x52, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x63, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x69, 0x64, 0x1a, + 0x44, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x1f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x44, 0x0a, 0x0a, 0x5a, 0x65, 0x72, 0x6f, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x09, 0x10, + 0x0a, 0x22, 0x81, 0x01, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x22, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x50, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xb8, 0x02, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 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, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x06, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, + 0x73, 0x74, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, + 0x73, 0x74, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, + 0x67, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67, + 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, + 0x65, 0x65, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x65, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x21, 0x0a, + 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x22, 0x8b, 0x02, 0x0a, 0x06, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x5f, + 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x6b, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, + 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, + 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x6e, 0x63, + 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0xe5, + 0x02, 0x0a, 0x0c, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x64, 0x67, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x06, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x23, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x64, + 0x67, 0x65, 0x2e, 0x4f, 0x70, 0x52, 0x02, 0x6f, 0x70, 0x12, 0x22, 0x0a, 0x06, 0x66, 0x61, 0x63, + 0x65, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x12, 0x22, 0x0a, + 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x50, 0x72, 0x65, 0x64, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x50, 0x72, 0x65, 0x64, + 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, + 0x1f, 0x0a, 0x02, 0x4f, 0x70, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x44, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x56, 0x52, 0x10, 0x02, + 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xf1, 0x02, 0x0a, 0x09, 0x4d, 0x75, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, - 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x62, - 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6f, 0x6e, 0x44, 0x69, - 0x73, 0x6b, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6d, - 0x6f, 0x76, 0x65, 0x54, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x6f, 0x76, - 0x65, 0x54, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, - 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x11, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x22, 0xe5, 0x02, 0x0a, 0x0c, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, - 0x64, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x06, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, - 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x50, - 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x06, 0x52, 0x07, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x23, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x45, 0x64, 0x67, 0x65, 0x2e, 0x4f, 0x70, 0x52, 0x02, 0x6f, 0x70, 0x12, 0x22, 0x0a, - 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, - 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x50, 0x72, 0x65, 0x64, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x50, 0x72, 0x65, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x22, 0x1f, 0x0a, 0x02, 0x4f, 0x70, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4f, - 0x56, 0x52, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xf1, 0x02, 0x0a, 0x09, 0x4d, - 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x26, - 0x0a, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x70, 0x62, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x64, 0x67, 0x65, 0x52, - 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x12, 0x24, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6f, - 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x52, 0x06, 0x64, - 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x72, 0x6f, 0x70, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x28, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3e, - 0x0a, 0x06, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x44, - 0x41, 0x54, 0x41, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x59, 0x50, 0x45, 0x10, 0x03, 0x12, - 0x0d, 0x0a, 0x09, 0x41, 0x4c, 0x4c, 0x5f, 0x49, 0x4e, 0x5f, 0x4e, 0x53, 0x10, 0x04, 0x22, 0xca, - 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3a, 0x0a, 0x0a, 0x70, - 0x72, 0x65, 0x64, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x50, 0x72, - 0x65, 0x64, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x70, 0x72, - 0x65, 0x64, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x53, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x64, 0x48, - 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x62, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x48, 0x69, 0x6e, 0x74, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2d, 0x0a, 0x08, - 0x48, 0x69, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, - 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x22, 0x93, 0x01, 0x0a, 0x08, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x61, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, - 0x64, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, - 0x54, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, - 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, - 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x5a, 0x65, 0x72, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x73, 0x12, 0x29, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, - 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xdb, 0x05, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, - 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x2e, - 0x0a, 0x13, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x2a, 0x0a, - 0x11, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x69, 0x64, 0x5f, 0x66, 0x69, - 0x6c, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x52, - 0x6f, 0x6c, 0x65, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x76, 0x61, 0x75, - 0x6c, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x61, 0x75, - 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x76, - 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x75, 0x6c, - 0x74, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, - 0x61, 0x75, 0x6c, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x61, 0x75, - 0x6c, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x4e, 0x75, 0x6d, 0x12, 0x29, 0x0a, 0x10, 0x69, - 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, - 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x61, 0x6c, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x61, 0x72, - 0x74, 0x69, 0x61, 0x6c, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x61, - 0x72, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x24, 0x0a, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x66, 0x72, - 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x17, 0x69, - 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x77, 0x61, 0x72, 0x65, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x69, 0x73, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x22, 0xc5, 0x04, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, - 0x61, 0x6c, 0x12, 0x2b, 0x0a, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x1d, 0x0a, 0x02, 0x6b, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x62, 0x61, - 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, 0x2e, 0x4b, 0x56, 0x52, 0x02, 0x6b, 0x76, 0x12, 0x29, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6c, 0x65, - 0x61, 0x6e, 0x5f, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x12, 0x25, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, - 0x74, 0x61, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x78, 0x70, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x12, 0x2c, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x12, 0x29, 0x0a, 0x09, 0x63, 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x44, 0x43, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x08, 0x63, 0x64, 0x63, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x30, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, - 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x10, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x59, 0x0a, 0x12, - 0x65, 0x78, 0x74, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x10, 0x65, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0x23, 0x0a, - 0x08, 0x43, 0x44, 0x43, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x6e, - 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x74, - 0x54, 0x73, 0x22, 0x63, 0x0a, 0x03, 0x4b, 0x56, 0x53, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, - 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x6f, 0x6e, - 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x80, 0x04, 0x0a, 0x07, 0x50, 0x6f, 0x73, 0x74, - 0x69, 0x6e, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, - 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x76, - 0x61, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, - 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, - 0x70, 0x65, 0x52, 0x07, 0x76, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x70, - 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x50, - 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x70, 0x6f, 0x73, 0x74, - 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x5f, - 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x6e, 0x67, 0x54, - 0x61, 0x67, 0x12, 0x22, 0x0a, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x06, - 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x02, 0x6f, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, - 0x74, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, - 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x0e, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x73, 0x22, 0xa0, - 0x01, 0x0a, 0x07, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, - 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, - 0x59, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x49, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, - 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x42, 0x4f, 0x4f, 0x4c, 0x10, - 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x41, 0x54, 0x45, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x05, 0x12, - 0x07, 0x0a, 0x03, 0x47, 0x45, 0x4f, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x10, - 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x08, 0x12, - 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06, 0x4f, - 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x49, 0x47, 0x46, 0x4c, - 0x4f, 0x41, 0x54, 0x10, 0x0b, 0x12, 0x0a, 0x0a, 0x06, 0x56, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, - 0x0c, 0x22, 0x31, 0x0a, 0x0b, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x07, 0x0a, 0x03, 0x52, 0x45, 0x46, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x4c, 0x41, - 0x4e, 0x47, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x51, 0x0a, 0x08, 0x55, 0x69, - 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, - 0x6c, 0x74, 0x61, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x74, - 0x61, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x55, 0x69, 0x64, 0x73, 0x22, 0x6b, 0x0a, - 0x07, 0x55, 0x69, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, - 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x24, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x69, 0x64, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x1b, 0x0a, - 0x09, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x17, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x08, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x52, 0x65, 0x66, 0x22, 0x8c, 0x01, 0x0a, 0x0b, 0x50, - 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x04, 0x70, 0x61, - 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x69, - 0x64, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x04, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x08, 0x70, - 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, - 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x04, 0x52, 0x06, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x46, 0x61, 0x63, - 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x69, - 0x61, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x22, - 0x4e, 0x0a, 0x0b, 0x46, 0x61, 0x63, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x19, - 0x0a, 0x08, 0x61, 0x6c, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x61, - 0x63, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, - 0x2c, 0x0a, 0x06, 0x46, 0x61, 0x63, 0x65, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x06, 0x66, 0x61, 0x63, - 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x22, 0x39, 0x0a, - 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0b, 0x66, - 0x61, 0x63, 0x65, 0x74, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x73, 0x52, 0x0a, 0x66, 0x61, - 0x63, 0x65, 0x74, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x08, 0x46, 0x75, 0x6e, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, - 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x22, 0x6a, - 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x72, 0x65, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x6f, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x70, 0x12, 0x2a, 0x0a, 0x08, - 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x08, - 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x66, 0x75, 0x6e, 0x63, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x75, 0x6e, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x66, 0x75, 0x6e, 0x63, 0x22, 0x78, 0x0a, 0x0d, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x14, - 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, - 0x79, 0x70, 0x65, 0x73, 0x22, 0xd1, 0x02, 0x0a, 0x0a, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4e, - 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76, - 0x65, 0x72, 0x73, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, - 0x72, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, - 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, - 0x70, 0x73, 0x65, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, - 0x6e, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, - 0x69, 0x71, 0x75, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x71, - 0x75, 0x65, 0x12, 0x34, 0x0a, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x73, 0x70, 0x65, 0x63, - 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x56, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0a, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x73, 0x22, 0x3a, 0x0a, 0x0c, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x4e, 0x6f, 0x64, 0x65, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x73, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x22, 0xc1, 0x04, 0x0a, 0x0c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, - 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x70, 0x62, 0x2e, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x12, - 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x06, 0x20, + 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x26, 0x0a, 0x05, 0x65, 0x64, + 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x65, 0x64, 0x45, 0x64, 0x67, 0x65, 0x52, 0x05, 0x65, 0x64, 0x67, + 0x65, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x24, 0x0a, 0x05, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, + 0x2e, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x05, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6f, 0x70, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x52, 0x06, 0x64, 0x72, 0x6f, 0x70, 0x4f, + 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x72, 0x6f, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x28, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3e, 0x0a, 0x06, 0x44, 0x72, + 0x6f, 0x70, 0x4f, 0x70, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x41, 0x10, + 0x02, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x59, 0x50, 0x45, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x41, + 0x4c, 0x4c, 0x5f, 0x49, 0x4e, 0x5f, 0x4e, 0x53, 0x10, 0x04, 0x22, 0xca, 0x01, 0x0a, 0x08, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x5f, + 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x62, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x64, 0x48, 0x69, + 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x48, 0x69, + 0x6e, 0x74, 0x73, 0x1a, 0x53, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x64, 0x48, 0x69, 0x6e, 0x74, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x2e, 0x48, 0x69, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2d, 0x0a, 0x08, 0x48, 0x69, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, + 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x22, 0x93, 0x01, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x74, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x54, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x6f, + 0x6e, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x22, 0x74, 0x0a, + 0x0c, 0x5a, 0x65, 0x72, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x54, 0x73, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x22, 0xdb, 0x05, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x74, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x73, + 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, + 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, + 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x66, 0x69, 0x6c, + 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x61, + 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x61, 0x75, + 0x6c, 0x74, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x69, + 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x73, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x11, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x69, + 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x50, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x76, 0x61, 0x75, + 0x6c, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x4e, 0x75, 0x6d, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x46, 0x72, + 0x6f, 0x6d, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, + 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, + 0x6c, 0x12, 0x24, 0x0a, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x17, 0x69, 0x73, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x69, 0x73, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x22, 0xc5, 0x04, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x2b, + 0x0a, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x02, 0x6b, + 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, + 0x70, 0x62, 0x34, 0x2e, 0x4b, 0x56, 0x52, 0x02, 0x6b, 0x76, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x5f, 0x70, + 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x25, + 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x52, 0x05, + 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x10, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x75, 0x6d, 0x12, 0x2c, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x12, 0x29, 0x0a, 0x09, 0x63, 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x44, 0x43, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x08, 0x63, 0x64, 0x63, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x30, 0x0a, 0x09, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x08, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x59, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x5f, + 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x10, 0x65, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0x23, 0x0a, 0x08, 0x43, 0x44, 0x43, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x74, 0x54, 0x73, 0x22, 0x63, + 0x0a, 0x03, 0x4b, 0x56, 0x53, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x12, 0x1e, 0x0a, + 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x22, 0x80, 0x04, 0x0a, 0x07, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x12, + 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x03, 0x75, 0x69, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x50, + 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, + 0x76, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x6f, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, + 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x61, 0x6e, 0x67, 0x54, 0x61, 0x67, 0x12, 0x22, + 0x0a, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, + 0x74, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, + 0x6f, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x73, 0x22, 0xa0, 0x01, 0x0a, 0x07, 0x56, + 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, + 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x01, 0x12, + 0x07, 0x0a, 0x03, 0x49, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, + 0x54, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x42, 0x4f, 0x4f, 0x4c, 0x10, 0x04, 0x12, 0x0c, 0x0a, + 0x08, 0x44, 0x41, 0x54, 0x45, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x05, 0x12, 0x07, 0x0a, 0x03, 0x47, + 0x45, 0x4f, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x10, 0x07, 0x12, 0x0c, 0x0a, + 0x08, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x53, + 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, + 0x54, 0x10, 0x0a, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x49, 0x47, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, + 0x0b, 0x12, 0x0a, 0x0a, 0x06, 0x56, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x0c, 0x22, 0x31, 0x0a, + 0x0b, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, + 0x52, 0x45, 0x46, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x01, + 0x12, 0x0e, 0x0a, 0x0a, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x4c, 0x41, 0x4e, 0x47, 0x10, 0x02, + 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x51, 0x0a, 0x08, 0x55, 0x69, 0x64, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x73, 0x12, 0x19, + 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x55, 0x69, 0x64, 0x73, 0x22, 0x6b, 0x0a, 0x07, 0x55, 0x69, 0x64, + 0x50, 0x61, 0x63, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, + 0x69, 0x7a, 0x65, 0x12, 0x24, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x69, 0x64, 0x42, 0x6c, 0x6f, 0x63, + 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x6c, 0x6c, + 0x6f, 0x63, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x17, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x61, 0x6c, + 0x6c, 0x6f, 0x63, 0x52, 0x65, 0x66, 0x22, 0x8c, 0x01, 0x0a, 0x0b, 0x50, 0x6f, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x04, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x69, 0x64, 0x50, 0x61, 0x63, + 0x6b, 0x52, 0x04, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x50, + 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x04, 0x52, 0x06, 0x73, + 0x70, 0x6c, 0x69, 0x74, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x22, 0x4e, 0x0a, 0x0b, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x6c, + 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, + 0x6c, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x2c, 0x0a, 0x06, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, + 0x74, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, 0x22, 0x39, 0x0a, 0x0a, 0x46, 0x61, 0x63, + 0x65, 0x74, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0b, 0x66, 0x61, 0x63, 0x65, 0x74, + 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, + 0x62, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x73, 0x52, 0x0a, 0x66, 0x61, 0x63, 0x65, 0x74, 0x73, + 0x4c, 0x69, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x08, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x22, 0x6a, 0x0a, 0x0a, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x54, 0x72, 0x65, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x70, 0x12, 0x2a, 0x0a, 0x08, 0x63, 0x68, 0x69, 0x6c, + 0x64, 0x72, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x08, 0x63, 0x68, 0x69, 0x6c, + 0x64, 0x72, 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x66, 0x75, 0x6e, 0x63, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x04, 0x66, 0x75, 0x6e, 0x63, 0x22, 0x78, 0x0a, 0x0d, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x22, 0xd1, 0x02, 0x0a, 0x0a, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4e, 0x6f, 0x64, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x69, 0x7a, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x12, 0x21, 0x0a, - 0x0c, 0x6e, 0x6f, 0x6e, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, - 0x12, 0x2a, 0x0a, 0x11, 0x6e, 0x6f, 0x6e, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6e, 0x6f, 0x6e, - 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, - 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x6c, 0x69, 0x63, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x6f, 0x43, - 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x34, 0x0a, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, - 0x5f, 0x73, 0x70, 0x65, 0x63, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, - 0x62, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, - 0x63, 0x52, 0x0a, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x73, 0x22, 0x39, 0x0a, - 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, - 0x4e, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x01, 0x12, - 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, - 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x52, 0x08, - 0x65, 0x78, 0x70, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x22, 0x4f, 0x0a, 0x0f, 0x56, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x28, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x69, 0x72, - 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x53, 0x0a, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, - 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x06, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x73, 0x22, 0x32, 0x0a, 0x09, 0x4d, 0x61, 0x70, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, - 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xb2, 0x01, 0x0a, 0x14, 0x4d, 0x6f, 0x76, - 0x65, 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x5f, 0x63, 0x6f, 0x6e, 0x66, + 0x6c, 0x69, 0x63, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x6f, 0x43, 0x6f, + 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x12, 0x34, + 0x0a, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x73, 0x18, 0x0c, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0a, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x53, + 0x70, 0x65, 0x63, 0x73, 0x22, 0x3a, 0x0a, 0x0c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x4e, 0x6f, 0x64, 0x65, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x22, 0xd7, 0x04, 0x0a, 0x0c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x69, 0x64, 0x12, 0x19, - 0x0a, 0x08, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x67, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x07, 0x64, 0x65, 0x73, 0x74, 0x47, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x74, 0x78, 0x6e, - 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x78, 0x6e, 0x54, 0x73, - 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x65, 0x78, 0x70, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x22, 0x43, 0x0a, - 0x09, 0x54, 0x78, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x54, 0x73, 0x22, 0xe4, 0x01, 0x0a, 0x0b, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, - 0x74, 0x61, 0x12, 0x21, 0x0a, 0x04, 0x74, 0x78, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x78, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x04, 0x74, 0x78, 0x6e, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x73, 0x73, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x78, - 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, - 0x74, 0x61, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x1a, 0x41, 0x0a, 0x13, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x0d, 0x54, 0x78, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0c, 0x50, 0x65, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x5e, 0x0a, 0x09, 0x52, 0x61, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, - 0x29, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x26, 0x0a, 0x07, 0x70, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x22, 0x36, 0x0a, 0x0e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, - 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x22, 0x50, 0x0a, 0x0d, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x07, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, - 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, - 0x73, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x5d, 0x0a, 0x13, - 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x12, - 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, 0x2e, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x14, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x03, 0x6b, 0x76, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, 0x2e, 0x4b, 0x56, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x03, 0x6b, 0x76, 0x73, 0x22, 0xba, 0x01, 0x0a, 0x03, 0x4e, 0x75, 0x6d, - 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x76, - 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, - 0x1c, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x12, 0x12, 0x0a, - 0x04, 0x62, 0x75, 0x6d, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x62, 0x75, 0x6d, - 0x70, 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x4e, 0x75, 0x6d, 0x2e, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2b, 0x0a, 0x09, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x53, 0x5f, 0x49, 0x44, 0x10, 0x00, - 0x12, 0x07, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x58, 0x4e, - 0x5f, 0x54, 0x53, 0x10, 0x02, 0x22, 0x5a, 0x0a, 0x0b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x49, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x49, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x49, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, - 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, - 0x79, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x18, - 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x65, 0x0a, 0x11, 0x4d, 0x6f, 0x76, 0x65, - 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x22, - 0x46, 0x0a, 0x0c, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x12, - 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x73, 0x12, 0x19, 0x0a, 0x08, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x2e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0xd9, 0x02, 0x0a, 0x0d, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, - 0x64, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, - 0x54, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x12, 0x19, 0x0a, - 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x6e, 0x69, 0x78, - 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x78, 0x54, - 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, - 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, - 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, - 0x6f, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, - 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x66, 0x75, - 0x6c, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x46, - 0x75, 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x0e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x0e, 0x64, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x22, 0x90, 0x01, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x07, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6f, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x52, 0x06, - 0x64, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x72, 0x6f, 0x70, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2d, 0x0a, 0x06, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x12, - 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x41, - 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x54, 0x54, 0x52, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, - 0x4e, 0x53, 0x10, 0x03, 0x22, 0xb5, 0x02, 0x0a, 0x0d, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, - 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x54, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x6e, - 0x69, 0x78, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x6e, 0x69, - 0x78, 0x54, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, - 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x1c, - 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x4c, 0x0a, 0x0e, - 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, - 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6d, 0x73, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xab, 0x02, 0x0a, 0x09, 0x42, - 0x61, 0x63, 0x6b, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x4b, 0x65, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x55, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x68, - 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x41, 0x10, 0x01, - 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, - 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4f, 0x55, 0x4e, - 0x54, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x56, - 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x10, 0x06, 0x12, 0x08, - 0x0a, 0x04, 0x54, 0x59, 0x50, 0x45, 0x10, 0x07, 0x22, 0xa2, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x04, 0x75, 0x69, - 0x64, 0x73, 0x12, 0x27, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, - 0x67, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, - 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, - 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x70, 0x6c, 0x69, - 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x04, 0x52, 0x06, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x08, 0x75, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0xc6, 0x01, - 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, + 0x32, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, + 0x2e, 0x56, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x04, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c, 0x61, 0x6e, + 0x67, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x6f, 0x6e, + 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x6e, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x2a, 0x0a, 0x11, + 0x6e, 0x6f, 0x6e, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, + 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6e, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, + 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, + 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x6c, + 0x69, 0x63, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x34, 0x0a, 0x0b, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x62, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, + 0x70, 0x65, 0x63, 0x52, 0x0a, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x73, 0x22, + 0x39, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x08, 0x0a, 0x04, + 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x10, 0x02, 0x12, 0x0a, + 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, + 0x52, 0x08, 0x65, 0x78, 0x70, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x22, 0x4f, 0x0a, 0x0f, 0x56, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x28, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, + 0x69, 0x72, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x53, 0x0a, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, + 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0x32, 0x0a, 0x09, 0x4d, 0x61, 0x70, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x70, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xb2, 0x01, 0x0a, 0x14, 0x4d, + 0x6f, 0x76, 0x65, 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x69, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x67, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x64, 0x65, 0x73, 0x74, 0x47, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x74, + 0x78, 0x6e, 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x78, 0x6e, + 0x54, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x65, + 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x22, + 0x43, 0x0a, 0x09, 0x54, 0x78, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x70, 0x68, - 0x71, 0x6c, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x67, 0x72, 0x61, 0x70, 0x68, 0x71, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x33, - 0x0a, 0x0c, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, 0x5f, 0x70, 0x72, 0x65, 0x64, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, 0x50, 0x72, - 0x65, 0x64, 0x73, 0x12, 0x31, 0x0a, 0x0c, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x54, - 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x64, 0x67, 0x72, 0x61, 0x70, - 0x68, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x2f, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0xdb, 0x01, 0x0a, 0x08, 0x42, 0x75, 0x6c, 0x6b, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x64, 0x67, 0x65, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x6d, 0x61, - 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x75, 0x6c, - 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d, 0x61, 0x70, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d, 0x61, 0x70, 0x12, - 0x24, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x05, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x4e, 0x0a, 0x0e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d, - 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x54, 0x73, 0x22, 0xe4, 0x01, 0x0a, 0x0b, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, + 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x04, 0x74, 0x78, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x78, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x04, 0x74, 0x78, 0x6e, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, + 0x61, 0x78, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x0f, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, + 0x65, 0x6c, 0x74, 0x61, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x75, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x1a, 0x41, 0x0a, 0x13, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x0d, 0x54, + 0x78, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x0e, 0x0a, 0x02, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0c, + 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x5e, 0x0a, 0x09, 0x52, 0x61, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x26, 0x0a, 0x07, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x07, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x36, 0x0a, 0x0e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x22, 0x50, 0x0a, 0x0d, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, + 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, + 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x5d, + 0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, + 0x73, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, 0x2e, 0x4d, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0x3b, 0x0a, + 0x14, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x03, 0x6b, 0x76, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, 0x2e, 0x4b, + 0x56, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x03, 0x6b, 0x76, 0x73, 0x22, 0xba, 0x01, 0x0a, 0x03, 0x4e, + 0x75, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x03, 0x76, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, + 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x62, 0x75, 0x6d, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x62, + 0x75, 0x6d, 0x70, 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x4e, 0x75, 0x6d, 0x2e, 0x6c, 0x65, 0x61, 0x73, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2b, 0x0a, 0x09, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x53, 0x5f, 0x49, 0x44, + 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x54, + 0x58, 0x4e, 0x5f, 0x54, 0x53, 0x10, 0x02, 0x22, 0x5a, 0x0a, 0x0b, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x49, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x49, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x49, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x05, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, + 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, + 0x6e, 0x6c, 0x79, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65, + 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, + 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x65, 0x0a, 0x11, 0x4d, 0x6f, + 0x76, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x22, 0x46, 0x0a, 0x0c, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x65, 0x74, + 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x73, 0x12, 0x19, + 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x2e, 0x0a, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0xd9, 0x02, 0x0a, 0x0d, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, + 0x65, 0x61, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, + 0x61, 0x64, 0x54, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x12, + 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x6e, + 0x69, 0x78, 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x6e, 0x69, + 0x78, 0x54, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, + 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, + 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x64, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x64, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, + 0x66, 0x75, 0x6c, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x46, 0x75, 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x0e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x72, 0x6f, 0x70, 0x5f, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x64, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x90, 0x01, 0x0a, 0x0d, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x07, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6f, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x72, 0x6f, 0x70, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x72, 0x6f, 0x70, 0x4f, 0x70, + 0x52, 0x06, 0x64, 0x72, 0x6f, 0x70, 0x4f, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x72, 0x6f, 0x70, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x72, + 0x6f, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2d, 0x0a, 0x06, 0x44, 0x72, 0x6f, 0x70, 0x4f, + 0x70, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, + 0x54, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x54, 0x54, 0x52, 0x10, 0x02, 0x12, 0x06, + 0x0a, 0x02, 0x4e, 0x53, 0x10, 0x03, 0x22, 0xb5, 0x02, 0x0a, 0x0d, 0x45, 0x78, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x22, 0x2c, 0x0a, 0x11, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, - 0x31, 0x0a, 0x12, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x4d, 0x65, - 0x74, 0x61, 0x32, 0xc4, 0x01, 0x0a, 0x04, 0x52, 0x61, 0x66, 0x74, 0x12, 0x2d, 0x0a, 0x09, 0x48, - 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2e, 0x0a, 0x0b, 0x52, 0x61, - 0x66, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x61, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x28, 0x01, 0x12, 0x2e, 0x0a, 0x0b, 0x4a, 0x6f, - 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x61, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x06, 0x49, 0x73, - 0x50, 0x65, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0xfd, 0x04, 0x0a, 0x04, 0x5a, 0x65, - 0x72, 0x6f, 0x12, 0x2c, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x0a, 0x2e, - 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x00, - 0x12, 0x2d, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x68, 0x69, 0x70, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x1a, - 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x12, - 0x39, 0x0a, 0x10, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, - 0x68, 0x69, 0x70, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, - 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2b, 0x0a, 0x06, 0x4f, 0x72, - 0x61, 0x63, 0x6c, 0x65, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, - 0x6c, 0x74, 0x61, 0x22, 0x00, 0x30, 0x01, 0x12, 0x27, 0x0a, 0x0b, 0x53, 0x68, 0x6f, 0x75, 0x6c, - 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x12, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, - 0x65, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x22, 0x00, - 0x12, 0x31, 0x0a, 0x06, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, - 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, - 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x27, 0x0a, 0x09, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x49, 0x64, 0x73, - 0x12, 0x07, 0x2e, 0x70, 0x62, 0x2e, 0x4e, 0x75, 0x6d, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x07, 0x2e, 0x70, 0x62, 0x2e, - 0x4e, 0x75, 0x6d, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x49, 0x64, 0x73, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x4f, 0x72, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x78, - 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, - 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, 0x12, 0x30, 0x0a, 0x08, 0x54, - 0x72, 0x79, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x78, 0x6e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, - 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x22, 0x00, 0x12, 0x34, 0x0a, - 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, + 0x70, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x54, 0x73, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, + 0x6e, 0x69, 0x78, 0x54, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x20, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, + 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x4c, + 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, + 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xab, 0x02, 0x0a, + 0x09, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x55, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x22, 0x68, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x41, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x02, 0x12, 0x0b, 0x0a, + 0x07, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4f, + 0x55, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x52, + 0x45, 0x56, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x10, 0x06, + 0x12, 0x08, 0x0a, 0x04, 0x54, 0x59, 0x50, 0x45, 0x10, 0x07, 0x22, 0xa2, 0x01, 0x0a, 0x11, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x04, + 0x75, 0x69, 0x64, 0x73, 0x12, 0x27, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, + 0x69, 0x6e, 0x67, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x70, + 0x6c, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x04, 0x52, 0x06, 0x73, 0x70, 0x6c, 0x69, + 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x75, 0x69, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, + 0xc6, 0x01, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, + 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, + 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, + 0x70, 0x68, 0x71, 0x6c, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x70, 0x68, 0x71, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x12, 0x33, 0x0a, 0x0c, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, 0x5f, 0x70, 0x72, 0x65, 0x64, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, + 0x50, 0x72, 0x65, 0x64, 0x73, 0x12, 0x31, 0x0a, 0x0c, 0x64, 0x67, 0x72, 0x61, 0x70, 0x68, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, + 0x2e, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x64, 0x67, 0x72, + 0x61, 0x70, 0x68, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x2f, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0xdb, 0x01, 0x0a, 0x08, 0x42, 0x75, + 0x6c, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x64, 0x67, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, + 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x62, 0x2e, 0x42, + 0x75, 0x6c, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d, 0x61, + 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d, 0x61, + 0x70, 0x12, 0x24, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x1a, 0x4e, 0x0a, 0x0e, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x22, 0x2c, 0x0a, 0x11, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, + 0x64, 0x22, 0x31, 0x0a, 0x12, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, + 0x4d, 0x65, 0x74, 0x61, 0x32, 0xc4, 0x01, 0x0a, 0x04, 0x52, 0x61, 0x66, 0x74, 0x12, 0x2d, 0x0a, + 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2e, 0x0a, 0x0b, + 0x52, 0x61, 0x66, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0d, 0x2e, 0x70, 0x62, + 0x2e, 0x52, 0x61, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x28, 0x01, 0x12, 0x2e, 0x0a, 0x0b, + 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x70, 0x62, + 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x0c, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x06, + 0x49, 0x73, 0x50, 0x65, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x61, 0x66, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0xfd, 0x04, 0x0a, 0x04, + 0x5a, 0x65, 0x72, 0x6f, 0x12, 0x2c, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, + 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x1a, 0x13, 0x2e, 0x70, 0x62, + 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, + 0x00, 0x12, 0x39, 0x0a, 0x10, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x1a, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2b, 0x0a, 0x06, + 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x12, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, + 0x44, 0x65, 0x6c, 0x74, 0x61, 0x22, 0x00, 0x30, 0x01, 0x12, 0x27, 0x0a, 0x0b, 0x53, 0x68, 0x6f, + 0x75, 0x6c, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x12, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, + 0x62, 0x6c, 0x65, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, + 0x22, 0x00, 0x12, 0x31, 0x0a, 0x06, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x11, 0x2e, 0x70, + 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x27, 0x0a, 0x09, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x49, + 0x64, 0x73, 0x12, 0x07, 0x2e, 0x70, 0x62, 0x2e, 0x4e, 0x75, 0x6d, 0x1a, 0x0f, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x73, 0x22, 0x00, 0x12, 0x28, + 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x07, 0x2e, 0x70, + 0x62, 0x2e, 0x4e, 0x75, 0x6d, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x49, 0x64, 0x73, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x4f, 0x72, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x1a, 0x0f, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, 0x12, 0x30, 0x0a, + 0x08, 0x54, 0x72, 0x79, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x54, + 0x78, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x1a, 0x0f, 0x2e, 0x70, + 0x62, 0x2e, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x22, 0x00, 0x12, + 0x34, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x4d, 0x6f, 0x76, 0x65, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x6f, 0x76, 0x65, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, + 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x32, 0xa6, 0x07, 0x0a, 0x06, + 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x0d, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, + 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, + 0x22, 0x00, 0x12, 0x24, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x12, + 0x09, 0x2e, 0x70, 0x62, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, + 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x0c, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x1a, 0x07, 0x2e, 0x70, 0x62, 0x2e, 0x4b, 0x56, + 0x53, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x29, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, + 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x0e, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x11, 0x2e, 0x70, + 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x11, 0x2e, + 0x70, 0x62, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x12, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, - 0x65, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x4d, 0x6f, 0x76, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x32, 0xa6, 0x07, 0x0a, 0x06, 0x57, 0x6f, - 0x72, 0x6b, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0d, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x0f, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, - 0x12, 0x24, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x09, 0x2e, - 0x70, 0x62, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x1a, 0x07, 0x2e, 0x70, 0x62, 0x2e, 0x4b, 0x56, 0x53, 0x22, - 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x29, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x0f, 0x2e, - 0x70, 0x62, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0e, - 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, - 0x12, 0x2f, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x70, 0x62, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, - 0x00, 0x12, 0x31, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x11, 0x2e, 0x70, 0x62, - 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, - 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, - 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, - 0x00, 0x12, 0x31, 0x0a, 0x06, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, - 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x50, - 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x07, 0x2e, 0x70, 0x62, 0x2e, 0x4b, 0x56, - 0x53, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, - 0x00, 0x28, 0x01, 0x12, 0x39, 0x0a, 0x0d, 0x4d, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x65, 0x64, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x12, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x50, 0x72, - 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x0c, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, 0x12, 0x3b, - 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x17, 0x2e, 0x70, 0x62, - 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, 0x62, 0x34, - 0x2e, 0x4b, 0x56, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x58, 0x0a, 0x13, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x12, 0x1e, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, - 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, - 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, - 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0a, 0x54, - 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x54, - 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x1f, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, 0x62, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x11, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x1d, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, - 0x30, 0x01, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x06, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x11, 0x2e, + 0x70, 0x62, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, + 0x65, 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x07, 0x2e, 0x70, 0x62, 0x2e, + 0x4b, 0x56, 0x53, 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x22, 0x00, 0x28, 0x01, 0x12, 0x39, 0x0a, 0x0d, 0x4d, 0x6f, 0x76, 0x65, 0x50, 0x72, 0x65, + 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x18, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x6f, 0x76, 0x65, + 0x50, 0x72, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x1a, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x00, + 0x12, 0x3b, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x17, 0x2e, + 0x70, 0x62, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x62, 0x61, 0x64, 0x67, 0x65, 0x72, 0x70, + 0x62, 0x34, 0x2e, 0x4b, 0x56, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x58, 0x0a, + 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x12, 0x1e, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x47, 0x72, 0x61, 0x70, 0x68, 0x51, 0x4c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, + 0x0a, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x70, 0x62, + 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x1f, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x2b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x78, 0x74, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x70, + 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x11, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, + 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x74, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x28, 0x01, 0x30, 0x01, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/schema/parse.go b/schema/parse.go index a18f7fddadb..ccaf0a91339 100644 --- a/schema/parse.go +++ b/schema/parse.go @@ -73,6 +73,12 @@ func parseDirective(it *lex.ItemIterator, schema *pb.SchemaUpdate, t types.TypeI " Got: [%v] for attr: [%v]", t.Name(), schema.Predicate) } schema.Lang = true + case "label": + labelName, err := parseLabelDirective(it) + if err != nil { + return err + } + schema.Label = labelName default: return next.Errorf("Invalid index specification") } @@ -81,6 +87,26 @@ func parseDirective(it *lex.ItemIterator, schema *pb.SchemaUpdate, t types.TypeI return nil } +func parseLabelDirective(it *lex.ItemIterator) (string, error) { + it.Next() + next := it.Item() + if next.Typ != itemLeftRound { + return "", next.Errorf("Missing '(' after @label") + } + it.Next() + next = it.Item() + if next.Typ != itemText { + return "", next.Errorf("Missing name inside @label()") + } + labelName := next.Val + it.Next() + next = it.Item() + if next.Typ != itemRightRound { + return "", next.Errorf("Missing ')' after @label(name") + } + return labelName, nil +} + func parseScalarPair(it *lex.ItemIterator, predicate string, ns uint64) (*pb.SchemaUpdate, error) { it.Next() next := it.Item() diff --git a/schema/schema.go b/schema/schema.go index 3ce0da8ea74..f651946baf2 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -240,6 +240,14 @@ func (s *state) SetType(typeName string, typ *pb.TypeUpdate) { s.elog.Printf(logTypeUpdate(typ, typeName)) } +// Gets the label for the given predicate +func (s *state) GetLabel(ctx context.Context, pred string) (string, bool) { + if sch, ok := s.Get(ctx, pred); ok { + return sch.Label, true + } + return "", false +} + // Get gets the schema for the given predicate. func (s *state) Get(ctx context.Context, pred string) (pb.SchemaUpdate, bool) { isWrite, _ := ctx.Value(IsWrite).(bool) diff --git a/systest/label/docker-compose.yml b/systest/label/docker-compose.yml new file mode 100644 index 00000000000..fe5a605616d --- /dev/null +++ b/systest/label/docker-compose.yml @@ -0,0 +1,73 @@ +# Docker compose for label-based sharding integration tests +# Setup: 3 alphas in different groups with different labels +# - alpha1: group=1, no label (serves unlabeled predicates) +# - alpha2: group=2, --label=secret (serves @label(secret) predicates) +# - alpha3: group=3, --label=top_secret (serves @label(top_secret) predicates) +# +version: "3.5" +services: + alpha1: + image: dgraph/dgraph:local + working_dir: /data/alpha1 + labels: + cluster: test + ports: + - "8080" + - "9080" + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: + /gobin/dgraph ${COVERAGE_OUTPUT} alpha --my=alpha1:7080 --zero=zero1:5080 --logtostderr -v=2 + --raft "group=1" --security "whitelist=0.0.0.0/0;" + alpha2: + image: dgraph/dgraph:local + working_dir: /data/alpha2 + labels: + cluster: test + ports: + - "8080" + - "9080" + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: + /gobin/dgraph ${COVERAGE_OUTPUT} alpha --my=alpha2:7080 --zero=zero1:5080 --logtostderr -v=2 + --raft "group=2" --label=secret --security "whitelist=0.0.0.0/0;" + alpha3: + image: dgraph/dgraph:local + working_dir: /data/alpha3 + labels: + cluster: test + ports: + - "8080" + - "9080" + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: + /gobin/dgraph ${COVERAGE_OUTPUT} alpha --my=alpha3:7080 --zero=zero1:5080 --logtostderr -v=2 + --raft "group=3" --label=top_secret --security "whitelist=0.0.0.0/0;" + zero1: + image: dgraph/dgraph:local + working_dir: /data/zero1 + labels: + cluster: test + ports: + - "5080" + - "6080" + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: + /gobin/dgraph ${COVERAGE_OUTPUT} zero --telemetry "reports=false;" --raft="idx=1;" + --my=zero1:5080 --replicas=1 --logtostderr -v=2 --bindall +volumes: {} \ No newline at end of file diff --git a/systest/label/label_test.go b/systest/label/label_test.go new file mode 100644 index 00000000000..050cfa6317f --- /dev/null +++ b/systest/label/label_test.go @@ -0,0 +1,392 @@ +//go:build integration + +/* + * SPDX-FileCopyrightText: © 2017-2025 Istari Digital, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "context" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/dgraph-io/dgo/v250" + "github.com/dgraph-io/dgo/v250/protos/api" + "github.com/dgraph-io/dgraph/v25/testutil" +) + +// waitForCluster waits for all alpha nodes to be ready and connected to Zero. +func waitForCluster(t *testing.T) *dgo.Dgraph { + t.Log("Waiting for cluster to be ready...") + var dg *dgo.Dgraph + var err error + + // Retry getting connection to group 1 (unlabeled alpha) + t.Log(" Connecting to group 1 (unlabeled alpha)...") + for i := 0; i < 30; i++ { + dg, err = testutil.GetClientToGroup("1") + if err == nil { + t.Log(" Connected to group 1") + break + } + if i%5 == 0 { + t.Logf(" Retry %d/30: %v", i+1, err) + } + time.Sleep(time.Second) + } + require.NoError(t, err, "error while getting connection to group 1") + + // Wait for all groups to appear in state + t.Log(" Waiting for all 3 groups to appear in state...") + for i := 0; i < 30; i++ { + state, err := testutil.GetState() + if err == nil && len(state.Groups) >= 3 { + t.Logf(" All %d groups ready", len(state.Groups)) + return dg + } + if i%5 == 0 { + groupCount := 0 + if state != nil { + groupCount = len(state.Groups) + } + t.Logf(" Retry %d/30: %d groups found", i+1, groupCount) + } + time.Sleep(time.Second) + } + + t.Fatal("timeout waiting for all 3 groups to be ready") + return nil +} + +// TestLabeledAlphaRegistration verifies that alphas register with their labels. +func TestLabeledAlphaRegistration(t *testing.T) { + t.Log("=== TestLabeledAlphaRegistration: Verifying alphas register with their labels ===") + waitForCluster(t) + + t.Log("Fetching cluster state...") + state, err := testutil.GetState() + require.NoError(t, err) + + // Verify we have 3 groups + t.Logf("Found %d groups", len(state.Groups)) + require.Len(t, state.Groups, 3, "expected 3 groups") + + // Track which labels we found + foundLabels := make(map[string]uint32) // label -> groupId + + for groupID, group := range state.Groups { + for _, member := range group.Members { + if member.Label != "" { + foundLabels[member.Label] = uint32(member.GroupID) + t.Logf("Group %s has member with label: %s", groupID, member.Label) + } else { + t.Logf("Group %s has unlabeled member", groupID) + } + } + } + + // Verify we have the expected labels + require.Contains(t, foundLabels, "secret", "expected 'secret' label to be registered") + require.Contains(t, foundLabels, "top_secret", "expected 'top_secret' label to be registered") +} + +// TestLabeledPredicateRouting verifies that predicates with @label are routed to the correct group. +func TestLabeledPredicateRouting(t *testing.T) { + t.Log("=== TestLabeledPredicateRouting: Verifying @label predicates route to correct groups ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Drop all data first + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + t.Log("Drop complete") + + // Apply schema with labeled predicates + t.Log("Applying schema with labeled predicates:") + t.Log(" - name: unlabeled") + t.Log(" - codename: @label(secret)") + t.Log(" - alias: @label(top_secret)") + schema := ` + name: string @index(term) . + codename: string @index(term) @label(secret) . + alias: string @index(term) @label(top_secret) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Schema applied successfully") + + // Give Zero time to process tablet assignments + t.Log("Waiting 3s for Zero to process tablet assignments...") + time.Sleep(3 * time.Second) + + // Get state and verify tablet assignments + t.Log("Fetching cluster state to verify tablet assignments...") + state, err := testutil.GetState() + require.NoError(t, err) + + // Build a map of label -> groupID from members + labelToGroup := make(map[string]string) + for groupID, group := range state.Groups { + for _, member := range group.Members { + if member.Label != "" { + labelToGroup[member.Label] = groupID + } + } + } + + // Find which group has each predicate + predicateToGroup := make(map[string]string) + predicateToLabel := make(map[string]string) + for groupID, group := range state.Groups { + for predName, tablet := range group.Tablets { + predicateToGroup[predName] = groupID + predicateToLabel[predName] = tablet.Label + t.Logf("Predicate %s is in group %s (tablet label: %s)", predName, groupID, tablet.Label) + } + } + + // Predicates are namespaced with "0-" prefix (namespace 0 is default) + // Verify 'name' is in an unlabeled group (group 1) + t.Log("Verifying predicate routing:") + t.Logf(" Checking 'name' is in group 1 (unlabeled)... actual: %s", predicateToGroup["0-name"]) + require.Equal(t, "1", predicateToGroup["0-name"], + "'name' predicate should be in group 1 (unlabeled)") + + // Verify 'codename' is in the 'secret' labeled group + secretGroup := labelToGroup["secret"] + t.Logf(" 'secret' label maps to group: %s", secretGroup) + require.NotEmpty(t, secretGroup, "should have a 'secret' labeled group") + t.Logf(" Checking 'codename' is in secret group... actual: %s", predicateToGroup["0-codename"]) + require.Equal(t, secretGroup, predicateToGroup["0-codename"], + "'codename' predicate should be in the 'secret' labeled group") + + // Verify 'alias' is in the 'top_secret' labeled group + topSecretGroup := labelToGroup["top_secret"] + t.Logf(" 'top_secret' label maps to group: %s", topSecretGroup) + require.NotEmpty(t, topSecretGroup, "should have a 'top_secret' labeled group") + t.Logf(" Checking 'alias' is in top_secret group... actual: %s", predicateToGroup["0-alias"]) + require.Equal(t, topSecretGroup, predicateToGroup["0-alias"], + "'alias' predicate should be in the 'top_secret' labeled group") + t.Log("All predicate routing verified successfully!") +} + +// TestLabeledPredicateDataIsolation verifies data can be written/read for labeled predicates. +func TestLabeledPredicateDataIsolation(t *testing.T) { + t.Log("=== TestLabeledPredicateDataIsolation: Verifying data read/write across labeled predicates ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Drop all and set up schema + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + t.Log("Applying schema with labeled predicates...") + schema := ` + name: string @index(term) . + codename: string @index(term) @label(secret) . + alias: string @index(term) @label(top_secret) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Waiting 2s for schema to propagate...") + time.Sleep(2 * time.Second) + + // Insert data using all predicates + t.Log("Inserting test data (James Bond agent)...") + _, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:agent "James Bond" . + _:agent "007" . + _:agent "The spy who loved me" . + `), + }) + require.NoError(t, err) + t.Log("Data inserted successfully") + + // Query to verify data is accessible + t.Log("Querying data to verify accessibility across groups...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Verify the response contains our data + t.Log("Verifying response matches expected data...") + testutil.CompareJSON(t, ` + { + "agents": [ + { + "name": "James Bond", + "codename": "007", + "alias": "The spy who loved me" + } + ] + } + `, string(resp.GetJson())) + t.Log("Data isolation test passed!") +} + +// TestLabeledPredicateCannotBeMoved verifies that labeled predicates cannot be moved via /moveTablet. +func TestLabeledPredicateCannotBeMoved(t *testing.T) { + t.Log("=== TestLabeledPredicateCannotBeMoved: Verifying labeled predicates cannot be moved ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Set up schema with labeled predicate + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + t.Log("Applying schema with 'codename' @label(secret)...") + schema := ` + name: string @index(term) . + codename: string @index(term) @label(secret) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Waiting 2s for schema to propagate...") + time.Sleep(2 * time.Second) + + // Get state to find the 'secret' group + t.Log("Fetching cluster state to find 'codename' predicate location...") + state, err := testutil.GetState() + require.NoError(t, err) + + // Find the group with 'codename' predicate (stored with namespace prefix "0-") + var codenameGroup string + for groupID, group := range state.Groups { + if _, ok := group.Tablets["0-codename"]; ok { + codenameGroup = groupID + break + } + } + t.Logf("'codename' predicate is currently in group: %s", codenameGroup) + require.NotEmpty(t, codenameGroup, "codename predicate should exist") + + // Try to move 'codename' to group 1 (should fail) + // Note: moveTablet API uses the namespaced predicate name + moveUrl := fmt.Sprintf("http://"+testutil.GetSockAddrZeroHttp()+"/moveTablet?tablet=%s&group=1", + url.QueryEscape("0-codename")) + t.Logf("Attempting to move 'codename' to group 1 via: %s", moveUrl) + resp, err := http.Get(moveUrl) + require.NoError(t, err) + defer resp.Body.Close() + t.Logf("Move request returned status: %s", resp.Status) + + // The move should fail because labeled predicates cannot be moved + // We expect either an error response or the predicate to remain in its original group + t.Log("Waiting 2s for any potential move to complete...") + time.Sleep(2 * time.Second) + + // Verify the predicate is still in its original group + t.Log("Verifying predicate did not move...") + state2, err := testutil.GetState() + require.NoError(t, err) + + var newCodenameGroup string + for groupID, group := range state2.Groups { + if _, ok := group.Tablets["0-codename"]; ok { + newCodenameGroup = groupID + break + } + } + t.Logf("'codename' predicate is now in group: %s", newCodenameGroup) + + require.Equal(t, codenameGroup, newCodenameGroup, + "labeled predicate 'codename' should not have moved") + t.Log("Verified: labeled predicate cannot be moved!") +} + +// TestUnlabeledPredicateNotOnLabeledGroup verifies that unlabeled predicates +// are not assigned to labeled groups. +func TestUnlabeledPredicateNotOnLabeledGroup(t *testing.T) { + t.Log("=== TestUnlabeledPredicateNotOnLabeledGroup: Verifying unlabeled predicates stay on unlabeled groups ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Set up schema with mix of labeled and unlabeled predicates + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + t.Log("Applying schema with mix of labeled and unlabeled predicates...") + schema := ` + name: string @index(term) . + email: string @index(exact) . + phone: string . + codename: string @index(term) @label(secret) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Waiting 3s for schema to propagate...") + time.Sleep(3 * time.Second) + + // Get state + t.Log("Fetching cluster state...") + state, err := testutil.GetState() + require.NoError(t, err) + + // Find labeled groups + t.Log("Identifying labeled groups...") + labeledGroups := make(map[string]bool) + for groupID, group := range state.Groups { + for _, member := range group.Members { + if member.Label != "" { + labeledGroups[groupID] = true + t.Logf(" Group %s is labeled (label: %s)", groupID, member.Label) + } + } + } + + // Verify unlabeled predicates are not in labeled groups + t.Log("Verifying unlabeled predicates are not in labeled groups...") + unlabeledPreds := []string{"name", "email", "phone"} + for _, pred := range unlabeledPreds { + for groupID, group := range state.Groups { + if _, ok := group.Tablets[pred]; ok { + t.Logf(" Predicate '%s' is in group %s (labeled: %v)", pred, groupID, labeledGroups[groupID]) + require.False(t, labeledGroups[groupID], + "unlabeled predicate '%s' should not be in labeled group %s", pred, groupID) + } + } + } + t.Log("Verified: unlabeled predicates are not on labeled groups!") +} + +// TestMissingLabelGroupError tests that applying a schema with a label that has no matching +// alpha group results in an error. +func TestMissingLabelGroupError(t *testing.T) { + t.Log("=== TestMissingLabelGroupError: Verifying error when using non-existent label ===") + dg := waitForCluster(t) + ctx := context.Background() + + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + // Try to apply schema with a label that doesn't exist + t.Log("Attempting to apply schema with @label(nonexistent_label)...") + schema := ` + secret_data: string @label(nonexistent_label) . + ` + err := dg.Alter(ctx, &api.Operation{Schema: schema}) + + // This should fail because there's no alpha with --label=nonexistent_label + if err != nil { + t.Logf("Got expected error: %v", err) + } else { + t.Log("WARNING: No error returned (expected an error)") + } + require.Error(t, err, "should error when no alpha has the required label") + require.Contains(t, err.Error(), "nonexistent_label", + "error should mention the missing label") + t.Log("Verified: non-existent label produces correct error!") +} \ No newline at end of file diff --git a/testutil/zero.go b/testutil/zero.go index 5ea567cfbac..3b43f2d7a28 100644 --- a/testutil/zero.go +++ b/testutil/zero.go @@ -28,6 +28,7 @@ type Member struct { ID string `json:"id"` LastUpdate string `json:"lastUpdate"` Leader bool `json:"leader"` + Label string `json:"label,omitempty"` } // StateResponse represents the structure of the JSON object returned by calling @@ -41,6 +42,7 @@ type StateResponse struct { Tablets map[string]struct { GroupID int `json:"groupId"` Predicate string `json:"predicate"` + Label string `json:"label,omitempty"` } `json:"tablets"` } `json:"groups"` Removed []struct { diff --git a/worker/groups.go b/worker/groups.go index 5c5984e68da..7252be5dbc2 100644 --- a/worker/groups.go +++ b/worker/groups.go @@ -98,6 +98,7 @@ func StartRaftNodes(walStore *raftwal.DiskStorage, bindall bool) { GroupId: x.WorkerConfig.ProposedGroupId, Addr: x.WorkerConfig.MyAddr, Learner: x.WorkerConfig.Raft.GetBool("learner"), + Label: x.WorkerConfig.Label, } if m.GroupId > 0 { m.ForceGroupId = true @@ -173,7 +174,6 @@ func (g *groupi) informZeroAboutTablets() { // this node is the leader or the follower, because this early on, we might not have // figured that out. ticker := time.Tick(time.Second) - for range ticker { preds := schema.State().Predicates() if _, err := g.Inform(preds); err != nil { @@ -388,8 +388,11 @@ func (g *groupi) ChecksumsMatch(ctx context.Context) error { } } -func (g *groupi) BelongsTo(key string) (uint32, error) { - if tablet, err := g.Tablet(key); err != nil { +// BelongsTo returns the group ID that serves the given predicate with the specified label. +// For new predicates (schema mutations), the label should be passed from the SchemaUpdate. +// For existing predicates (data mutations), get the label from schema.State().GetLabel(). +func (g *groupi) BelongsTo(key string, label string) (uint32, error) { + if tablet, err := g.Tablet(key, label); err != nil { return 0, err } else if tablet != nil { return tablet.GroupId, nil @@ -442,8 +445,11 @@ func (g *groupi) BelongsToReadOnly(key string, ts uint64) (uint32, error) { return out.GetGroupId(), nil } +// ServesTablet checks if this group serves the given predicate. +// Uses stored schema to get the label for existing predicates. func (g *groupi) ServesTablet(key string) (bool, error) { - if tablet, err := g.Tablet(key); err != nil { + label, _ := schema.State().GetLabel(context.Background(), key) + if tablet, err := g.Tablet(key, label); err != nil { return false, err } else if tablet != nil && tablet.GroupId == groups().groupId() { return true, nil @@ -483,19 +489,22 @@ func (g *groupi) Inform(preds []string) ([]*pb.Tablet, error) { if len(p) == 0 { continue } - if tab, ok := g.tablets[p]; !ok { - unknownPreds = append(unknownPreds, &pb.Tablet{GroupId: g.groupId(), Predicate: p}) + tablet := &pb.Tablet{GroupId: g.groupId(), Predicate: p} + // Get label from schema and set if exists + if label, ok := schema.State().GetLabel(context.Background(), p); ok { + tablet.Label = label + glog.Infof("Inform: predicate %s has label %q from schema", p, label) + } + unknownPreds = append(unknownPreds, tablet) } else { tablets = append(tablets, tab) } } g.RUnlock() - if len(unknownPreds) == 0 { return nil, nil } - pl := g.connToZeroLeader() zc := pb.NewZeroClient(pl.Get()) out, err := zc.Inform(g.Ctx(), &pb.TabletRequest{ @@ -524,24 +533,42 @@ func (g *groupi) Inform(preds []string) ([]*pb.Tablet, error) { return tablets, nil } -// Do not modify the returned Tablet -func (g *groupi) Tablet(key string) (*pb.Tablet, error) { +// Tablet returns tablet information for the given predicate with the specified label. +// The label parameter is required to ensure correct routing for labeled predicates. +// For schema mutations, pass the label from SchemaUpdate.Label. +// For data mutations, get the label from schema.State().GetLabel(). +// Do not modify the returned Tablet. +func (g *groupi) Tablet(key string, label string) (*pb.Tablet, error) { // TODO: Remove all this later, create a membership state and apply it g.RLock() tablet, ok := g.tablets[key] g.RUnlock() if ok { - return tablet, nil + // If labels match (or both empty), return cached tablet + if tablet.Label == label { + glog.V(2).Infof("Tablet: predicate %s cached (groupId=%d, label=%q)", key, tablet.GroupId, tablet.Label) + return tablet, nil + } + // Labels don't match - clear our cache and re-request from Zero + // This can happen after DropAll when tablets are re-created with different labels + glog.Infof("Tablet: predicate %s cached with label %q but need %q, clearing cache and re-requesting", + key, tablet.Label, label) + g.Lock() + delete(g.tablets, key) + g.Unlock() } - // We don't know about this tablet. + // We don't know about this tablet (or labels didn't match). // Check with dgraphzero if we can serve it. - tablet = &pb.Tablet{GroupId: g.groupId(), Predicate: key} + tablet = &pb.Tablet{GroupId: g.groupId(), Predicate: key, Label: label} + glog.V(2).Infof("Tablet: predicate %s requesting with label %q", key, label) return g.sendTablet(tablet) } -func (g *groupi) ForceTablet(key string) (*pb.Tablet, error) { - return g.sendTablet(&pb.Tablet{GroupId: g.groupId(), Predicate: key, Force: true}) +// ForceTablet forces this group to serve the given predicate, even if another +// group is currently serving it. Used during restore operations. +func (g *groupi) ForceTablet(key string, label string) (*pb.Tablet, error) { + return g.sendTablet(&pb.Tablet{GroupId: g.groupId(), Predicate: key, Label: label, Force: true}) } func (g *groupi) HasMeInState() bool { @@ -741,6 +768,7 @@ func (g *groupi) doSendMembership(tablets map[string]*pb.Tablet) error { Addr: x.WorkerConfig.MyAddr, Leader: leader, LastUpdate: uint64(time.Now().Unix()), + Label: x.WorkerConfig.Label, } group := &pb.Group{ Members: make(map[uint64]*pb.Member), diff --git a/worker/mutation.go b/worker/mutation.go index fdac2a41c1b..5f61f2bff07 100644 --- a/worker/mutation.go +++ b/worker/mutation.go @@ -217,9 +217,24 @@ func runSchemaMutation(ctx context.Context, updates []*pb.SchemaUpdate, startTs var closer *z.Closer for _, su := range updates { - if tablet, err := groups().Tablet(su.Predicate); err != nil { + // Use the label from the SchemaUpdate since the schema might not be stored yet. + if tablet, err := groups().Tablet(su.Predicate, su.Label); err != nil { return err } else if tablet.GetGroupId() != groups().groupId() { + // For labeled predicates, the tablet is intentionally served by a different group. + // We still need to record the schema metadata so queries know the predicate type, + // but we skip all index operations since we don't store the data. + if su.Label != "" { + glog.V(2).Infof("Recording schema metadata for labeled predicate %s (label: %s), served by group %d", + su.Predicate, su.Label, tablet.GetGroupId()) + if err := checkSchema(su); err != nil { + return err + } + // Just record the schema metadata, skip index operations + schema.State().Set(su.Predicate, su) + schema.State().SetMutSchema(su.Predicate, su) + continue + } return errors.Errorf("Tablet isn't being served by this group. Tablet: %+v", tablet) } @@ -690,7 +705,9 @@ func proposeOrSend(ctx context.Context, gid uint32, m *pb.Mutations, chr chan re func populateMutationMap(src *pb.Mutations) (map[uint32]*pb.Mutations, error) { mm := make(map[uint32]*pb.Mutations) for _, edge := range src.Edges { - gid, err := groups().BelongsTo(edge.Attr) + // For data mutations, get the label from stored schema + label, _ := schema.State().GetLabel(context.Background(), edge.Attr) + gid, err := groups().BelongsTo(edge.Attr, label) if err != nil { return nil, err } @@ -704,8 +721,10 @@ func populateMutationMap(src *pb.Mutations) (map[uint32]*pb.Mutations, error) { mu.Metadata = src.Metadata } - for _, schema := range src.Schema { - gid, err := groups().BelongsTo(schema.Predicate) + for _, schemaUpdate := range src.Schema { + // For schema mutations, use the label from the SchemaUpdate itself + // This is critical for new predicates where the schema isn't stored yet + gid, err := groups().BelongsTo(schemaUpdate.Predicate, schemaUpdate.Label) if err != nil { return nil, err } @@ -715,7 +734,7 @@ func populateMutationMap(src *pb.Mutations) (map[uint32]*pb.Mutations, error) { mu = &pb.Mutations{GroupId: gid} mm[gid] = mu } - mu.Schema = append(mu.Schema, schema) + mu.Schema = append(mu.Schema, schemaUpdate) } if src.DropOp > 0 { diff --git a/worker/online_restore.go b/worker/online_restore.go index 05462d0b21c..f2eb8bb8461 100644 --- a/worker/online_restore.go +++ b/worker/online_restore.go @@ -306,7 +306,9 @@ func handleRestoreProposal(ctx context.Context, req *pb.RestoreRequest, pidx uin for _, pred := range restorePreds { // Force the tablet to be moved to this group, even // if it's currently being served by another group. - tablet, err := groups().ForceTablet(pred) + // Get label from stored schema (schema is restored before data). + label, _ := schema.State().GetLabel(context.Background(), pred) + tablet, err := groups().ForceTablet(pred, label) if err != nil { return errors.Wrapf(err, "cannot create tablet for restored predicate %s", pred) } diff --git a/worker/predicate_move.go b/worker/predicate_move.go index f9b6c54f691..0943819701e 100644 --- a/worker/predicate_move.go +++ b/worker/predicate_move.go @@ -233,7 +233,9 @@ func (w *grpcWorker) MovePredicate(ctx context.Context, return &emptyPayload, errors.Errorf("While waiting for txn ts: %d. Error: %v", in.TxnTs, err) } - gid, err := groups().BelongsTo(in.Predicate) + // For predicate move, get label from stored schema + label, _ := schema.State().GetLabel(context.Background(), in.Predicate) + gid, err := groups().BelongsTo(in.Predicate, label) switch { case err != nil: return &emptyPayload, err diff --git a/worker/proposal.go b/worker/proposal.go index a7761bee175..1ee0d5b891f 100644 --- a/worker/proposal.go +++ b/worker/proposal.go @@ -145,8 +145,11 @@ func (n *node) proposeAndWait(ctx context.Context, proposal *pb.Proposal) (perr // timeout. var noTimeout bool + // checkTablet verifies that this group serves the given predicate. + // For data mutations, we get the label from stored schema. checkTablet := func(pred string) error { - tablet, err := groups().Tablet(pred) + label, _ := schema.State().GetLabel(context.Background(), pred) + tablet, err := groups().Tablet(pred, label) switch { case err != nil: return err @@ -159,6 +162,29 @@ func (n *node) proposeAndWait(ctx context.Context, proposal *pb.Proposal) (perr } } + // validateSchemaTablet validates tablet assignment for schema mutations. + // For labeled predicates, we only verify the tablet was assigned to some group - + // we don't require it to be served by this instance since labeled predicates + // are intentionally routed to a different group (the one with matching label). + validateSchemaTablet := func(pred string, label string) error { + tablet, err := groups().Tablet(pred, label) + switch { + case err != nil: + return err + case tablet == nil || tablet.GroupId == 0: + return errNonExistentTablet + case label != "": + // Labeled predicates are served by the labeled group, not this instance. + // Just verify the tablet was assigned successfully. + return nil + case tablet.GroupId != groups().groupId(): + // Unlabeled schema predicates should be served by this instance + return errUnservedTablet + default: + return nil + } + } + // Do a type check here if schema is present // In very rare cases invalid entries might pass through raft, which would // be persisted, we do best effort schema check while writing @@ -185,7 +211,10 @@ func (n *node) proposeAndWait(ctx context.Context, proposal *pb.Proposal) (perr } for _, schema := range proposal.Mutations.Schema { - if err := checkTablet(schema.Predicate); err != nil { + // Use validateSchemaTablet to pass the label from the schema update + // since the schema isn't stored yet when we're processing it. + // For labeled predicates, we don't require this instance to serve the tablet. + if err := validateSchemaTablet(schema.Predicate, schema.Label); err != nil { return err } if err := checkSchema(schema); err != nil { diff --git a/x/config.go b/x/config.go index ca0ac9a3326..c5a3c92d39d 100644 --- a/x/config.go +++ b/x/config.go @@ -135,6 +135,8 @@ type WorkerOptions struct { HardSync bool // Audit contains the audit flags that enables the audit. Audit bool + // Sharding label for alpha + Label string } // WorkerConfig stores the global instance of the worker package's options. From 60349e2d9d16441cea032518333acb08a23cc3e2 Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Sat, 24 Jan 2026 20:17:41 -0500 Subject: [PATCH 02/10] fix(ci): update trunk go runtime and format test files - Update trunk go runtime from 1.24.3 to 1.25.6 to match go.mod - Fix trailing newline formatting in label test files --- .trunk/trunk.yaml | 2 +- systest/label/docker-compose.yml | 2 +- systest/label/label_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 9452cac5cf4..b122b503c3e 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -14,7 +14,7 @@ plugins: # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) runtimes: enabled: - - go@1.24.3 + - go@1.25.6 - node@22.16.0 - python@3.10.8 diff --git a/systest/label/docker-compose.yml b/systest/label/docker-compose.yml index fe5a605616d..4bf7d90c267 100644 --- a/systest/label/docker-compose.yml +++ b/systest/label/docker-compose.yml @@ -70,4 +70,4 @@ services: command: /gobin/dgraph ${COVERAGE_OUTPUT} zero --telemetry "reports=false;" --raft="idx=1;" --my=zero1:5080 --replicas=1 --logtostderr -v=2 --bindall -volumes: {} \ No newline at end of file +volumes: {} diff --git a/systest/label/label_test.go b/systest/label/label_test.go index 050cfa6317f..30d61df228b 100644 --- a/systest/label/label_test.go +++ b/systest/label/label_test.go @@ -389,4 +389,4 @@ func TestMissingLabelGroupError(t *testing.T) { require.Contains(t, err.Error(), "nonexistent_label", "error should mention the missing label") t.Log("Verified: non-existent label produces correct error!") -} \ No newline at end of file +} From c4e4ccfd937c71c3a32c587c56980de9c496d3bf Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Sat, 24 Jan 2026 21:06:49 -0500 Subject: [PATCH 03/10] refactor(sharding): improve lock safety and code organization - Use closure with defer for RLock/RUnlock in Inform() to prevent lock leaks on error paths - Add getGroupLabel() helper that handles its own locking, keeping groupLabel() for callers that already hold the lock - Move checkSchemaTablet function closer to its usage in proposeAndWait - Add tablet_label span attribute for better observability --- dgraph/cmd/zero/tablet.go | 2 +- dgraph/cmd/zero/zero.go | 61 ++++++++++++++++++++++++--------------- worker/proposal.go | 49 +++++++++++++++---------------- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/dgraph/cmd/zero/tablet.go b/dgraph/cmd/zero/tablet.go index 78073775897..5260d458b87 100644 --- a/dgraph/cmd/zero/tablet.go +++ b/dgraph/cmd/zero/tablet.go @@ -140,7 +140,7 @@ func (s *Server) movePredicate(predicate string, srcGroup, dstGroup uint32) erro if tab == nil { return errors.Errorf("Tablet to be moved: [%v] is not being served", predicate) } - dstGroupLabel := s.groupLabel(dstGroup) + dstGroupLabel := s.getGroupLabel(dstGroup) if dstGroupLabel != tab.Label { // Don't allow a predicate to be moved to a group that doesn't share it's label. // (label will be empty string on either if unassigned) diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index ed7f7894826..89b01d9ac37 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -440,42 +440,45 @@ func (s *Server) Inform(ctx context.Context, req *pb.TabletRequest) (*pb.TabletR var proposal pb.ZeroProposal proposal.Tablets = make([]*pb.Tablet, 0) - // Acquire read lock for label-related lookups - s.RLock() for _, t := range unknownTablets { glog.Infof("Zero.Inform: routing tablet %s (label=%q, groupId=%d)", t.Predicate, t.Label, t.GroupId) - switch { - case x.IsReservedPredicate(t.Predicate): - // Force all the reserved predicates to be allocated to group 1. - // This is to make it easier to stream ACL updates to all alpha servers - // since they only need to open one pipeline to receive updates for all - // ACL predicates. - // This will also make it easier to restore the reserved predicates after - // a DropAll operation. - t.GroupId = 1 - case t.Label != "": - { + // Use closure to ensure lock is always released via defer, even on error paths. + // This pattern prevents lock leaks if new error conditions are added later. + if err := func() error { + s.RLock() + defer s.RUnlock() + switch { + case x.IsReservedPredicate(t.Predicate): + // Force all the reserved predicates to be allocated to group 1. + // This is to make it easier to stream ACL updates to all alpha servers + // since they only need to open one pipeline to receive updates for all + // ACL predicates. + // This will also make it easier to restore the reserved predicates after + // a DropAll operation. + t.GroupId = 1 + case t.Label != "": // Labeled predicate: route to matching labeled group gid, err := s.labelGroupId(t.Label) if err != nil { - s.RUnlock() - return nil, err + return err } glog.Infof("Zero.Inform: labeled predicate %s (label=%q) routed to group %d", t.Predicate, t.Label, gid) t.GroupId = gid + case s.isLabeledGroupId(t.GroupId): + // make sure unlabeled predicates don't go an labeled group + gid, err := s.firstUnlabeledGroupId() + if err != nil { + return err + } + t.GroupId = gid } - case s.isLabeledGroupId(t.GroupId): - // make sure unlabeled predicates don't go an labeled group - gid, err := s.firstUnlabeledGroupId() - if err != nil { - s.RUnlock() - return nil, err - } - t.GroupId = gid + return nil + }(); err != nil { + return nil, err } proposal.Tablets = append(proposal.Tablets, t) } - s.RUnlock() + if err := s.Node.proposeAndWait(ctx, &proposal); err != nil && err != errTabletAlreadyServed { span.AddEvent(fmt.Sprintf("Error proposing tablet: %+v. Error: %v", &proposal, err)) return nil, err @@ -705,6 +708,7 @@ func (s *Server) ShouldServe( // Check who is serving this tablet. tab := s.ServingTablet(tablet.Predicate) span.SetAttributes(attribute.String("tablet_predicate", tablet.Predicate)) + span.SetAttributes(attribute.String("tablet_label", tablet.Label)) if tab != nil && !tablet.Force { // If the existing tablet has a different label than requested, we need to re-route. // This can happen when a schema is applied with @label after the predicate was @@ -919,6 +923,7 @@ func (s *Server) latestMembershipState(ctx context.Context) (*pb.MembershipState } // groupLabel returns the label for a group (from first labeled member found) +// Caller must hold the read lock. func (s *Server) groupLabel(gid uint32) string { s.AssertRLock() group := s.state.Groups[gid] @@ -933,6 +938,14 @@ func (s *Server) groupLabel(gid uint32) string { return "" } +// getGroupLabel is like groupLabel but handles its own locking. +// Use this when calling from code that doesn't already hold the lock. +func (s *Server) getGroupLabel(gid uint32) string { + s.RLock() + defer s.RUnlock() + return s.groupLabel(gid) +} + // labelGroupId the group ID that has the given label, or 0 if none func (s *Server) labelGroupId(label string) (uint32, error) { s.AssertRLock() diff --git a/worker/proposal.go b/worker/proposal.go index 1ee0d5b891f..b3974be1cb3 100644 --- a/worker/proposal.go +++ b/worker/proposal.go @@ -162,29 +162,6 @@ func (n *node) proposeAndWait(ctx context.Context, proposal *pb.Proposal) (perr } } - // validateSchemaTablet validates tablet assignment for schema mutations. - // For labeled predicates, we only verify the tablet was assigned to some group - - // we don't require it to be served by this instance since labeled predicates - // are intentionally routed to a different group (the one with matching label). - validateSchemaTablet := func(pred string, label string) error { - tablet, err := groups().Tablet(pred, label) - switch { - case err != nil: - return err - case tablet == nil || tablet.GroupId == 0: - return errNonExistentTablet - case label != "": - // Labeled predicates are served by the labeled group, not this instance. - // Just verify the tablet was assigned successfully. - return nil - case tablet.GroupId != groups().groupId(): - // Unlabeled schema predicates should be served by this instance - return errUnservedTablet - default: - return nil - } - } - // Do a type check here if schema is present // In very rare cases invalid entries might pass through raft, which would // be persisted, we do best effort schema check while writing @@ -210,11 +187,33 @@ func (n *node) proposeAndWait(ctx context.Context, proposal *pb.Proposal) (perr } } + // checkSchemaTablet validates tablet assignment for schema mutations. + // For labeled predicates, we only verify the tablet was assigned to some group - + // we don't require it to be served by this instance since labeled predicates + // are intentionally routed to a different group (the one with matching label). + checkSchemaTablet := func(pred string, label string) error { + tablet, err := groups().Tablet(pred, label) + switch { + case err != nil: + return err + case tablet == nil || tablet.GroupId == 0: + return errNonExistentTablet + case label != "": + // Labeled predicates are served by the labeled group, not this instance. + // Just verify the tablet was assigned successfully. + return nil + case tablet.GroupId != groups().groupId(): + // Unlabeled schema predicates should be served by this instance + return errUnservedTablet + default: + return nil + } + } for _, schema := range proposal.Mutations.Schema { - // Use validateSchemaTablet to pass the label from the schema update + // Use checkSchemaTablet to pass the label from the schema update // since the schema isn't stored yet when we're processing it. // For labeled predicates, we don't require this instance to serve the tablet. - if err := validateSchemaTablet(schema.Predicate, schema.Label); err != nil { + if err := checkSchemaTablet(schema.Predicate, schema.Label); err != nil { return err } if err := checkSchema(schema); err != nil { From 115d220538c15dc1e12fd875d9cd49a8beb236d5 Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Sat, 24 Jan 2026 21:08:35 -0500 Subject: [PATCH 04/10] refactor(sharding): simplify group/label function names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename for consistency - drop redundant 'Id' suffix since return types already indicate the value type: - labelGroupId → labelGroup - isLabeledGroupId → isLabeledGroup - firstUnlabeledGroupId → firstUnlabeledGroup --- dgraph/cmd/zero/zero.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index 89b01d9ac37..c28b5f47846 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -458,15 +458,15 @@ func (s *Server) Inform(ctx context.Context, req *pb.TabletRequest) (*pb.TabletR t.GroupId = 1 case t.Label != "": // Labeled predicate: route to matching labeled group - gid, err := s.labelGroupId(t.Label) + gid, err := s.labelGroup(t.Label) if err != nil { return err } glog.Infof("Zero.Inform: labeled predicate %s (label=%q) routed to group %d", t.Predicate, t.Label, gid) t.GroupId = gid - case s.isLabeledGroupId(t.GroupId): + case s.isLabeledGroup(t.GroupId): // make sure unlabeled predicates don't go an labeled group - gid, err := s.firstUnlabeledGroupId() + gid, err := s.firstUnlabeledGroup() if err != nil { return err } @@ -748,16 +748,16 @@ func (s *Server) ShouldServe( tablet.GroupId = 1 case tablet.Label != "": // Labeled predicate: route to matching labeled group - gid, err := s.labelGroupId(tablet.Label) + gid, err := s.labelGroup(tablet.Label) if err != nil { s.RUnlock() return nil, err } glog.Infof("ShouldServe: labeled predicate %s (label=%q) routed to group %d", tablet.Predicate, tablet.Label, gid) tablet.GroupId = gid - case s.isLabeledGroupId(tablet.GroupId): + case s.isLabeledGroup(tablet.GroupId): // Make sure unlabeled predicates don't go to a labeled group - gid, err := s.firstUnlabeledGroupId() + gid, err := s.firstUnlabeledGroup() if err != nil { s.RUnlock() return nil, err @@ -946,8 +946,8 @@ func (s *Server) getGroupLabel(gid uint32) string { return s.groupLabel(gid) } -// labelGroupId the group ID that has the given label, or 0 if none -func (s *Server) labelGroupId(label string) (uint32, error) { +// labelGroup the group ID that has the given label, or 0 if none +func (s *Server) labelGroup(label string) (uint32, error) { s.AssertRLock() for gid, group := range s.state.Groups { for _, member := range group.Members { @@ -959,16 +959,16 @@ func (s *Server) labelGroupId(label string) (uint32, error) { return 0, errors.Errorf("No alpha group with label '%s' found", label) } -// isLabeledGroupId returns true if any member in the group has a label -func (s *Server) isLabeledGroupId(gid uint32) bool { +// isLabeledGroup returns true if any member in the group has a label +func (s *Server) isLabeledGroup(gid uint32) bool { s.AssertRLock() return s.groupLabel(gid) != "" } -func (s *Server) firstUnlabeledGroupId() (uint32, error) { +func (s *Server) firstUnlabeledGroup() (uint32, error) { s.AssertRLock() for gid := range s.state.Groups { - if !s.isLabeledGroupId(gid) { + if !s.isLabeledGroup(gid) { return gid, nil } } From 20d3b5f19abbc5f8f6d507d0de979e8e077e2e29 Mon Sep 17 00:00:00 2001 From: mattthew Date: Tue, 3 Feb 2026 18:24:18 -0500 Subject: [PATCH 05/10] Add first pass a enforcement of label/programs at query-time --- edgraph/access.go | 98 ++++++++-- systest/label/label_auth_test.go | 299 +++++++++++++++++++++++++++++++ worker/groups.go | 19 ++ x/auth/context.go | 61 +++++++ x/auth/context_test.go | 82 +++++++++ x/auth/extractor.go | 19 ++ x/auth/levels.go | 36 ++++ x/auth/levels_test.go | 88 +++++++++ x/auth/mock.go | 44 +++++ x/auth/programs.go | 33 ++++ x/auth/programs_test.go | 54 ++++++ x/auth/transport.go | 134 ++++++++++++++ 12 files changed, 952 insertions(+), 15 deletions(-) create mode 100644 systest/label/label_auth_test.go create mode 100644 x/auth/context.go create mode 100644 x/auth/context_test.go create mode 100644 x/auth/extractor.go create mode 100644 x/auth/levels.go create mode 100644 x/auth/levels_test.go create mode 100644 x/auth/mock.go create mode 100644 x/auth/programs.go create mode 100644 x/auth/programs_test.go create mode 100644 x/auth/transport.go diff --git a/edgraph/access.go b/edgraph/access.go index 4f39b3e4a18..0c6df46c819 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -30,6 +30,7 @@ import ( "github.com/dgraph-io/dgraph/v25/schema" "github.com/dgraph-io/dgraph/v25/worker" "github.com/dgraph-io/dgraph/v25/x" + "github.com/dgraph-io/dgraph/v25/x/auth" "github.com/dgraph-io/ristretto/v2/z" ) @@ -951,14 +952,50 @@ func shouldAllowAcls(ns uint64) bool { return !x.Config.SharedInstance || ns == x.RootNamespace } -// authorizeQuery authorizes the query using the aclCachePtr. It will silently drop all -// unauthorized predicates from query. +// authorizeLabeledPreds filters predicates based on security labels. +// This runs independently of ACL - it checks if the user's security level +// allows access to predicates with @label directives. +// Returns a map of blocked predicates. +func authorizeLabeledPreds(ctx context.Context, authCtx *auth.AuthContext, preds []string, namespace uint64) map[string]struct{} { + blocked := make(map[string]struct{}) + + glog.V(2).Infof("authorizeLabeledPreds: user level=%q, isNil=%v, preds=%v, namespace=%d", + authCtx.Level, authCtx.IsNil, preds, namespace) + + // If no auth credentials provided (nil token), skip label filtering for backward compatibility + if authCtx.IsNil { + glog.V(2).Infof("authorizeLabeledPreds: nil token, skipping label filtering") + return blocked + } + + for _, pred := range preds { + // Get the predicate's label from tablet cache (works across distributed schema) + nsPred := x.NamespaceAttr(namespace, pred) + label := worker.GetTabletLabel(nsPred) + glog.V(2).Infof(" pred=%q nsPred=%q label=%q", pred, nsPred, label) + if label == "" { + // No label on predicate = accessible to all authenticated users + continue + } + + // Check if user's level can access this label + canAccess := auth.CanAccess(authCtx.Level, label) + glog.V(2).Infof(" CanAccess(%q, %q) = %v", authCtx.Level, label, canAccess) + if authCtx.Level == "" || !canAccess { + blocked[pred] = struct{}{} + } + } + + glog.V(2).Infof("authorizeLabeledPreds: blocked=%v", blocked) + return blocked +} + +// authorizeQuery authorizes the query using the aclCachePtr and label-based security. +// It will silently drop all unauthorized predicates from query. // At this stage, namespace is not attached in the predicates. func authorizeQuery(ctx context.Context, parsedReq *dql.Result, graphql bool) error { - if worker.Config.AclSecretKey == nil { - // the user has not turned on the acl feature - return nil - } + // Extract auth context (nil token if no credentials) + authCtx := auth.ExtractOrNil(ctx) var userId string var groupIds []string @@ -974,6 +1011,27 @@ func authorizeQuery(ctx context.Context, parsedReq *dql.Result, graphql bool) er predToVarsMap[v] = k } + // 1. Label-based filtering (ALWAYS runs, ACL-independent) + // Use authCtx.Namespace for label checks + labelBlocked := authorizeLabeledPreds(ctx, authCtx, preds, authCtx.Namespace) + + // 2. ACL-based filtering (only if ACL enabled) + if worker.Config.AclSecretKey == nil { + // ACL not enabled, but still apply label filtering if any predicates were blocked + if len(labelBlocked) > 0 { + blockedVars := make(map[string]struct{}) + for predicate := range labelBlocked { + if variable, found := predToVarsMap[predicate]; found { + labelBlocked[variable] = struct{}{} + blockedVars[variable] = struct{}{} + } + } + parsedReq.Query = removePredsFromQuery(parsedReq.Query, labelBlocked) + parsedReq.QueryVars = removeVarsFromQueryVars(parsedReq.QueryVars, blockedVars) + } + return nil + } + doAuthorizeQuery := func() (map[string]struct{}, []string, error) { userData, err := extractUserAndGroups(ctx) if err != nil { @@ -996,11 +1054,21 @@ func authorizeQuery(ctx context.Context, parsedReq *dql.Result, graphql bool) er return result.blocked, result.allowed, nil } - blockedPreds, allowedPreds, err := doAuthorizeQuery() + aclBlocked, allowedPreds, err := doAuthorizeQuery() if err != nil { return err } + // 3. Merge label-blocked and ACL-blocked predicates + // Both checks must pass - if either blocks a predicate, it's blocked + allBlocked := make(map[string]struct{}) + for pred := range labelBlocked { + allBlocked[pred] = struct{}{} + } + for pred := range aclBlocked { + allBlocked[pred] = struct{}{} + } + if span := otrace.FromContext(ctx); span != nil { span.Annotatef(nil, (&accessEntry{ userId: userId, @@ -1011,32 +1079,32 @@ func authorizeQuery(ctx context.Context, parsedReq *dql.Result, graphql bool) er }).String()) } - if len(blockedPreds) != 0 { + if len(allBlocked) != 0 { // For GraphQL requests, we allow filtered access to the ACL predicates. // Filter for user_id and group_id is applied for the currently logged in user. if graphql && shouldAllowAcls(namespace) { for _, gq := range parsedReq.Query { addUserFilterToQuery(gq, userId, groupIds) } - // blockedPreds might have acl predicates which we want to allow access through + // allBlocked might have acl predicates which we want to allow access through // graphql, so deleting those from here. for _, pred := range x.AllACLPredicates() { - delete(blockedPreds, pred) + delete(allBlocked, pred) } // In query context ~predicate and predicate are considered different. - delete(blockedPreds, "~dgraph.user.group") + delete(allBlocked, "~dgraph.user.group") } blockedVars := make(map[string]struct{}) - for predicate := range blockedPreds { + for predicate := range allBlocked { if variable, found := predToVarsMap[predicate]; found { - // Add variables to blockedPreds to delete from Query - blockedPreds[variable] = struct{}{} + // Add variables to allBlocked to delete from Query + allBlocked[variable] = struct{}{} // Collect blocked Variables to remove from QueryVars blockedVars[variable] = struct{}{} } } - parsedReq.Query = removePredsFromQuery(parsedReq.Query, blockedPreds) + parsedReq.Query = removePredsFromQuery(parsedReq.Query, allBlocked) parsedReq.QueryVars = removeVarsFromQueryVars(parsedReq.QueryVars, blockedVars) } for i := range parsedReq.Query { diff --git a/systest/label/label_auth_test.go b/systest/label/label_auth_test.go new file mode 100644 index 00000000000..ac2e26173c9 --- /dev/null +++ b/systest/label/label_auth_test.go @@ -0,0 +1,299 @@ +//go:build integration + +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/dgraph-io/dgo/v250/protos/api" + "github.com/dgraph-io/dgraph/v25/x/auth" +) + +// setupLabelAuthTest sets up the schema and test data for label auth tests. +// Returns the dgraph client for querying. +func setupLabelAuthTest(t *testing.T) { + t.Log("Setting up label auth test...") + dg := waitForCluster(t) + ctx := context.Background() + + // Drop all and set up schema + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + // Schema with labeled predicates at different security levels + t.Log("Applying schema with labeled predicates...") + schema := ` + name: string @index(term) . + codename: string @index(term) @label(secret) . + alias: string @index(term) @label(top_secret) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Waiting 2s for schema to propagate...") + time.Sleep(2 * time.Second) + + // Insert test data + t.Log("Inserting test data...") + _, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:agent "James Bond" . + _:agent "007" . + _:agent "The spy who loved me" . + `), + }) + require.NoError(t, err) + t.Log("Test data inserted successfully") +} + +// TestLabelAuthNoLevel verifies that a user with no security level can only access unlabeled predicates. +func TestLabelAuthNoLevel(t *testing.T) { + t.Log("=== TestLabelAuthNoLevel: User with no level should only see unlabeled predicates ===") + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Create context with auth credentials but empty level + // This triggers label filtering (unlike nil token which skips filtering for backward compat) + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: ""}) + + // Query for all predicates + t.Log("Querying with empty security level...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Should only see 'name', not 'codename' or 'alias' + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + require.Len(t, result.Agents, 1, "should have 1 agent") + require.Equal(t, "James Bond", result.Agents[0].Name, "should see name") + require.Empty(t, result.Agents[0].Codename, "should NOT see codename (requires secret)") + require.Empty(t, result.Agents[0].Alias, "should NOT see alias (requires top_secret)") + t.Log("Test passed: user with no level only sees unlabeled predicates") +} + +// TestLabelAuthSecretLevel verifies that a user with 'secret' level can access secret and below. +func TestLabelAuthSecretLevel(t *testing.T) { + t.Log("=== TestLabelAuthSecretLevel: User with 'secret' level should see name + codename ===") + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Create context with 'secret' security level via gRPC metadata + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: "secret"}) + + // Query for all predicates + t.Log("Querying with 'secret' security level...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Should see 'name' and 'codename', but not 'alias' + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + require.Len(t, result.Agents, 1, "should have 1 agent") + require.Equal(t, "James Bond", result.Agents[0].Name, "should see name") + require.Equal(t, "007", result.Agents[0].Codename, "should see codename (has secret)") + require.Empty(t, result.Agents[0].Alias, "should NOT see alias (requires top_secret)") + t.Log("Test passed: user with 'secret' level sees name + codename") +} + +// TestLabelAuthTopSecretLevel verifies that a user with 'top_secret' level can access all predicates. +func TestLabelAuthTopSecretLevel(t *testing.T) { + t.Log("=== TestLabelAuthTopSecretLevel: User with 'top_secret' level should see all predicates ===") + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Create context with 'top_secret' security level via gRPC metadata + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: "top_secret"}) + + // Query for all predicates + t.Log("Querying with 'top_secret' security level...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Should see all predicates + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + require.Len(t, result.Agents, 1, "should have 1 agent") + require.Equal(t, "James Bond", result.Agents[0].Name, "should see name") + require.Equal(t, "007", result.Agents[0].Codename, "should see codename") + require.Equal(t, "The spy who loved me", result.Agents[0].Alias, "should see alias (has top_secret)") + t.Log("Test passed: user with 'top_secret' level sees all predicates") +} + +// TestLabelAuthUnclassifiedLevel verifies that 'unclassified' level can only access unlabeled predicates. +func TestLabelAuthUnclassifiedLevel(t *testing.T) { + t.Log("=== TestLabelAuthUnclassifiedLevel: User with 'unclassified' level should only see unlabeled ===") + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Create context with 'unclassified' security level via gRPC metadata + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: "unclassified"}) + + // Query for all predicates + t.Log("Querying with 'unclassified' security level...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Should only see 'name' (unlabeled), not 'codename' (secret) or 'alias' (top_secret) + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + require.Len(t, result.Agents, 1, "should have 1 agent") + require.Equal(t, "James Bond", result.Agents[0].Name, "should see name") + require.Empty(t, result.Agents[0].Codename, "should NOT see codename (requires secret)") + require.Empty(t, result.Agents[0].Alias, "should NOT see alias (requires top_secret)") + t.Log("Test passed: user with 'unclassified' level only sees unlabeled predicates") +} + +// TestLabelAuthClassifiedLevel verifies that 'classified' level can access classified and below but not secret/top_secret. +func TestLabelAuthClassifiedLevel(t *testing.T) { + t.Log("=== TestLabelAuthClassifiedLevel: User with 'classified' level - testing hierarchy ===") + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Create context with 'classified' security level via gRPC metadata + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: "classified"}) + + // Query for all predicates + t.Log("Querying with 'classified' security level...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // 'classified' is below 'secret' in hierarchy, so should only see 'name' (unlabeled) + // The schema uses @label(secret) and @label(top_secret), no @label(classified) + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + require.Len(t, result.Agents, 1, "should have 1 agent") + require.Equal(t, "James Bond", result.Agents[0].Name, "should see name (unlabeled)") + require.Empty(t, result.Agents[0].Codename, "should NOT see codename (requires secret)") + require.Empty(t, result.Agents[0].Alias, "should NOT see alias (requires top_secret)") + t.Log("Test passed: user with 'classified' level only sees unlabeled predicates") +} + +// TestLabelAuthWithoutACL verifies that label authorization works even when ACL is disabled. +func TestLabelAuthWithoutACL(t *testing.T) { + t.Log("=== TestLabelAuthWithoutACL: Label auth should work independently of ACL ===") + // This test runs in the label systest which does NOT have ACL enabled + // It verifies that label-based authorization is independent of the ACL system + setupLabelAuthTest(t) + dg := waitForCluster(t) + + // Even without ACL, label authorization should filter predicates + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{Level: "secret"}) + + t.Log("Querying with 'secret' level (ACL disabled)...") + resp, err := dg.NewTxn().Query(ctx, ` + { + agents(func: has(name)) { + name + codename + alias + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + var result struct { + Agents []struct { + Name string `json:"name,omitempty"` + Codename string `json:"codename,omitempty"` + Alias string `json:"alias,omitempty"` + } `json:"agents"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + + // Should see name and codename, not alias + require.Len(t, result.Agents, 1) + require.Equal(t, "James Bond", result.Agents[0].Name) + require.Equal(t, "007", result.Agents[0].Codename) + require.Empty(t, result.Agents[0].Alias) + t.Log("Test passed: label auth works without ACL enabled") +} diff --git a/worker/groups.go b/worker/groups.go index 7252be5dbc2..5aa2ee1c827 100644 --- a/worker/groups.go +++ b/worker/groups.go @@ -457,6 +457,19 @@ func (g *groupi) ServesTablet(key string) (bool, error) { return false, nil } +// GetTabletLabel returns the label for a predicate from the cached tablet info. +// This is used for authorization to get labels for predicates that may be served +// by other groups (labeled alphas). Returns empty string if tablet not cached. +func (g *groupi) GetTabletLabel(key string) string { + g.RLock() + tablet := g.tablets[key] + g.RUnlock() + if tablet != nil { + return tablet.Label + } + return "" +} + func (g *groupi) sendTablet(tablet *pb.Tablet) (*pb.Tablet, error) { pl := g.connToZeroLeader() zc := pb.NewZeroClient(pl.Get()) @@ -681,6 +694,12 @@ func KnownGroups() []uint32 { return groups().KnownGroups() } +// GetTabletLabel returns the label for a predicate from the cached tablet info. +// Used for authorization to get labels for predicates served by other groups. +func GetTabletLabel(pred string) string { + return groups().GetTabletLabel(pred) +} + // GroupId returns the group to which this worker belongs to. func GroupId() uint32 { return groups().groupId() diff --git a/x/auth/context.go b/x/auth/context.go new file mode 100644 index 00000000000..d916d3e0f04 --- /dev/null +++ b/x/auth/context.go @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import "context" + +// authContextKey is the context key for storing AuthContext. +type authContextKey struct{} + +// AuthContext contains authentication and authorization information extracted +// from credentials (JWT, API key, etc.). It is used for both ACL and label-based +// authorization decisions. +type AuthContext struct { + UserID string // User identifier + Namespace uint64 // Dgraph namespace + Groups []string // ACL groups the user belongs to + Level string // Security classification level (e.g., "secret", "top_secret") + Programs []string // NEED-TO-KNOW compartments/programs + IsNil bool // True when no credentials were provided (nil token pattern) +} + +// NilAuthContext returns a default AuthContext for unauthenticated requests. +// This implements the "nil token" pattern to avoid if/else checks throughout the codebase. +func NilAuthContext() *AuthContext { + return &AuthContext{ + UserID: "", + Namespace: 0, + Groups: nil, + Level: "", // No level = can only access unlabeled predicates + Programs: nil, // No programs = can only access non-compartmented data + IsNil: true, + } +} + +// WithAuthContext returns a new context with the given AuthContext attached. +func WithAuthContext(ctx context.Context, authCtx *AuthContext) context.Context { + return context.WithValue(ctx, authContextKey{}, authCtx) +} + +// FromContext extracts the AuthContext from the context, returning nil if not present. +func FromContext(ctx context.Context) *AuthContext { + if authCtx, ok := ctx.Value(authContextKey{}).(*AuthContext); ok { + return authCtx + } + return nil +} + +// ExtractOrNil extracts the AuthContext from the context, or returns NilAuthContext +// if no credentials were provided. This ensures callers always get a valid AuthContext. +// It checks in order: context value, transport metadata (gRPC/HTTP headers). +func ExtractOrNil(ctx context.Context) *AuthContext { + // First check if AuthContext was explicitly set in context + if authCtx := FromContext(ctx); authCtx != nil { + return authCtx + } + // Then try to extract from transport metadata + return ExtractFromMetadata(ctx) +} diff --git a/x/auth/context_test.go b/x/auth/context_test.go new file mode 100644 index 00000000000..948a4d3ed13 --- /dev/null +++ b/x/auth/context_test.go @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNilAuthContext(t *testing.T) { + ctx := NilAuthContext() + + require.NotNil(t, ctx) + require.Equal(t, "", ctx.UserID) + require.Equal(t, uint64(0), ctx.Namespace) + require.Nil(t, ctx.Groups) + require.Equal(t, "", ctx.Level) + require.Nil(t, ctx.Programs) + require.True(t, ctx.IsNil) +} + +func TestAuthContextWithValues(t *testing.T) { + ctx := &AuthContext{ + UserID: "alice", + Namespace: 1, + Groups: []string{"analysts", "readers"}, + Level: "secret", + Programs: []string{"alpha", "omega"}, + IsNil: false, + } + + require.Equal(t, "alice", ctx.UserID) + require.Equal(t, uint64(1), ctx.Namespace) + require.Equal(t, []string{"analysts", "readers"}, ctx.Groups) + require.Equal(t, "secret", ctx.Level) + require.Equal(t, []string{"alpha", "omega"}, ctx.Programs) + require.False(t, ctx.IsNil) +} + +func TestWithAuthContext(t *testing.T) { + authCtx := &AuthContext{ + UserID: "bob", + Level: "top_secret", + } + + ctx := WithAuthContext(context.Background(), authCtx) + require.NotNil(t, ctx) + + extracted := FromContext(ctx) + require.NotNil(t, extracted) + require.Equal(t, "bob", extracted.UserID) + require.Equal(t, "top_secret", extracted.Level) +} + +func TestFromContextMissing(t *testing.T) { + // When no auth context is set, should return nil + extracted := FromContext(context.Background()) + require.Nil(t, extracted) +} + +func TestExtractOrNil(t *testing.T) { + // When auth context exists, return it + authCtx := &AuthContext{ + UserID: "charlie", + Level: "classified", + } + ctx := WithAuthContext(context.Background(), authCtx) + extracted := ExtractOrNil(ctx) + require.Equal(t, "charlie", extracted.UserID) + require.False(t, extracted.IsNil) + + // When no auth context, return NilAuthContext + extracted = ExtractOrNil(context.Background()) + require.NotNil(t, extracted) + require.True(t, extracted.IsNil) + require.Equal(t, "", extracted.UserID) +} diff --git a/x/auth/extractor.go b/x/auth/extractor.go new file mode 100644 index 00000000000..796839d2997 --- /dev/null +++ b/x/auth/extractor.go @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import "context" + +// TokenExtractor defines the interface for extracting authentication context +// from various token types (JWT, API keys, etc.). +type TokenExtractor interface { + // Extract extracts authentication context from the given context. + // Returns an error if extraction fails (e.g., invalid token). + Extract(ctx context.Context) (*AuthContext, error) + + // Name returns the name of this extractor (e.g., "jwt", "apikey"). + Name() string +} diff --git a/x/auth/levels.go b/x/auth/levels.go new file mode 100644 index 00000000000..eb4bfbc87c5 --- /dev/null +++ b/x/auth/levels.go @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +// LevelHierarchy defines the security classification levels in descending order of access. +// Higher index = lower clearance. A user with a higher clearance (lower index) can access +// all levels at or below their clearance. +var LevelHierarchy = []string{"top_secret", "secret", "classified", "unclassified"} + +// CanAccess returns true if userLevel can access a predicate with the given label. +// Access is granted if the user's level is at or above the predicate's label in the hierarchy. +func CanAccess(userLevel, predicateLabel string) bool { + userIdx := indexOf(LevelHierarchy, userLevel) + predIdx := indexOf(LevelHierarchy, predicateLabel) + + // Unknown levels deny access + if userIdx == -1 || predIdx == -1 { + return false + } + + // Lower index = higher clearance, so userIdx <= predIdx means access granted + return userIdx <= predIdx +} + +// indexOf returns the index of the given level in the hierarchy, or -1 if not found. +func indexOf(hierarchy []string, level string) int { + for i, l := range hierarchy { + if l == level { + return i + } + } + return -1 +} diff --git a/x/auth/levels_test.go b/x/auth/levels_test.go new file mode 100644 index 00000000000..1a82b078e2d --- /dev/null +++ b/x/auth/levels_test.go @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCanAccess(t *testing.T) { + tests := []struct { + name string + userLevel string + predLabel string + canAccess bool + }{ + // Same level access + {"top_secret can access top_secret", "top_secret", "top_secret", true}, + {"secret can access secret", "secret", "secret", true}, + {"classified can access classified", "classified", "classified", true}, + {"unclassified can access unclassified", "unclassified", "unclassified", true}, + + // Higher level can access lower + {"top_secret can access secret", "top_secret", "secret", true}, + {"top_secret can access classified", "top_secret", "classified", true}, + {"top_secret can access unclassified", "top_secret", "unclassified", true}, + {"secret can access classified", "secret", "classified", true}, + {"secret can access unclassified", "secret", "unclassified", true}, + {"classified can access unclassified", "classified", "unclassified", true}, + + // Lower level cannot access higher + {"secret cannot access top_secret", "secret", "top_secret", false}, + {"classified cannot access top_secret", "classified", "top_secret", false}, + {"classified cannot access secret", "classified", "secret", false}, + {"unclassified cannot access top_secret", "unclassified", "top_secret", false}, + {"unclassified cannot access secret", "unclassified", "secret", false}, + {"unclassified cannot access classified", "unclassified", "classified", false}, + + // No level = no access to labeled predicates + {"empty level cannot access top_secret", "", "top_secret", false}, + {"empty level cannot access secret", "", "secret", false}, + {"empty level cannot access classified", "", "classified", false}, + {"empty level cannot access unclassified", "", "unclassified", false}, + + // Unknown levels + {"unknown user level cannot access", "unknown", "secret", false}, + {"user cannot access unknown pred level", "secret", "unknown", false}, + {"both unknown levels", "foo", "bar", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CanAccess(tt.userLevel, tt.predLabel) + require.Equal(t, tt.canAccess, result) + }) + } +} + +func TestLevelHierarchy(t *testing.T) { + // Verify the hierarchy is defined correctly + require.Equal(t, []string{"top_secret", "secret", "classified", "unclassified"}, LevelHierarchy) +} + +func TestIndexOf(t *testing.T) { + tests := []struct { + name string + level string + expected int + }{ + {"top_secret is index 0", "top_secret", 0}, + {"secret is index 1", "secret", 1}, + {"classified is index 2", "classified", 2}, + {"unclassified is index 3", "unclassified", 3}, + {"unknown returns -1", "unknown", -1}, + {"empty returns -1", "", -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := indexOf(LevelHierarchy, tt.level) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/x/auth/mock.go b/x/auth/mock.go new file mode 100644 index 00000000000..3f22d52c786 --- /dev/null +++ b/x/auth/mock.go @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import "context" + +// MockExtractor is a TokenExtractor for testing that returns a preconfigured AuthContext. +type MockExtractor struct { + AuthCtx *AuthContext + Err error +} + +// NewMockExtractor creates a new MockExtractor with the given AuthContext. +func NewMockExtractor(authCtx *AuthContext) *MockExtractor { + return &MockExtractor{AuthCtx: authCtx} +} + +// Extract returns the preconfigured AuthContext or error. +func (m *MockExtractor) Extract(ctx context.Context) (*AuthContext, error) { + if m.Err != nil { + return nil, m.Err + } + return m.AuthCtx, nil +} + +// Name returns "mock". +func (m *MockExtractor) Name() string { + return "mock" +} + +// WithMockAuth returns a context with the given AuthContext attached. +// This is a convenience function for testing. +func WithMockAuth(ctx context.Context, userID string, namespace uint64, level string, programs []string) context.Context { + return WithAuthContext(ctx, &AuthContext{ + UserID: userID, + Namespace: namespace, + Level: level, + Programs: programs, + IsNil: false, + }) +} diff --git a/x/auth/programs.go b/x/auth/programs.go new file mode 100644 index 00000000000..afc3f2f0fd6 --- /dev/null +++ b/x/auth/programs.go @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +// HasProgram returns true if the user has at least one program that matches +// the required programs on the predicate (OR logic). +// If no programs are required (nil or empty), access is granted. +// If programs are required but the user has none, access is denied. +func HasProgram(userPrograms, requiredPrograms []string) bool { + // No programs required = accessible to all + if len(requiredPrograms) == 0 { + return true + } + + // Programs required but user has none = denied + if len(userPrograms) == 0 { + return false + } + + // OR logic: user needs at least one matching program + for _, up := range userPrograms { + for _, rp := range requiredPrograms { + if up == rp { + return true + } + } + } + + return false +} diff --git a/x/auth/programs_test.go b/x/auth/programs_test.go new file mode 100644 index 00000000000..b679c4fff32 --- /dev/null +++ b/x/auth/programs_test.go @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHasProgram(t *testing.T) { + tests := []struct { + name string + userPrograms []string + predPrograms []string + hasAccess bool + }{ + // Exact match + {"single program exact match", []string{"alpha"}, []string{"alpha"}, true}, + + // User has more programs than required + {"user has multiple, pred has one", []string{"alpha", "beta"}, []string{"alpha"}, true}, + {"user has multiple, matches second", []string{"gamma", "beta"}, []string{"beta"}, true}, + + // OR logic - user needs at least one matching program + {"OR logic - one match is enough", []string{"alpha"}, []string{"alpha", "beta"}, true}, + {"OR logic - match second required", []string{"beta"}, []string{"alpha", "beta"}, true}, + {"OR logic - multiple matches", []string{"alpha", "beta"}, []string{"alpha", "beta"}, true}, + + // No match + {"no matching programs", []string{"gamma"}, []string{"alpha", "beta"}, false}, + {"completely different programs", []string{"x", "y", "z"}, []string{"a", "b", "c"}, false}, + + // Edge cases with nil/empty + {"user has no programs, pred requires some", nil, []string{"alpha"}, false}, + {"user has empty programs, pred requires some", []string{}, []string{"alpha"}, false}, + {"user has programs, pred requires none", []string{"alpha"}, nil, true}, + {"user has programs, pred has empty", []string{"alpha"}, []string{}, true}, + {"both nil - no programs required", nil, nil, true}, + {"both empty - no programs required", []string{}, []string{}, true}, + {"user nil, pred empty", nil, []string{}, true}, + {"user empty, pred nil", []string{}, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := HasProgram(tt.userPrograms, tt.predPrograms) + require.Equal(t, tt.hasAccess, result) + }) + } +} diff --git a/x/auth/transport.go b/x/auth/transport.go new file mode 100644 index 00000000000..f8c27be7f70 --- /dev/null +++ b/x/auth/transport.go @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package auth + +import ( + "context" + "strings" + + "github.com/golang/glog" + "google.golang.org/grpc/metadata" +) + +// Header keys for auth context passed via transport (gRPC metadata or HTTP headers). +// These use lowercase to be compatible with both gRPC (which lowercases) and HTTP. +const ( + // HeaderKeyActive indicates auth context is present (even if level/programs are empty) + HeaderKeyActive = "x-dgraph-auth-active" + // HeaderKeyLevel is the header key for security level + HeaderKeyLevel = "x-dgraph-auth-level" + // HeaderKeyPrograms is the header key for programs (comma-separated) + HeaderKeyPrograms = "x-dgraph-auth-programs" +) + +// ExtractFromMetadata extracts AuthContext from gRPC incoming metadata. +// Returns NilAuthContext if metadata is missing or auth-active marker not present. +func ExtractFromMetadata(ctx context.Context) *AuthContext { + md, ok := metadata.FromIncomingContext(ctx) + glog.V(2).Infof("ExtractFromMetadata: hasMetadata=%v", ok) + if !ok { + return NilAuthContext() + } + return extractFromHeaders(headerGetter(md.Get)) +} + +// ExtractFromHeaders extracts AuthContext from a generic header getter function. +// This can be used with HTTP requests, gRPC metadata, or any other transport. +// Returns NilAuthContext if headers are missing or auth-active marker not present. +func ExtractFromHeaders(getHeader func(key string) string) *AuthContext { + return extractFromHeaders(func(key string) []string { + if v := getHeader(key); v != "" { + return []string{v} + } + return nil + }) +} + +// headerGetter is a function type for getting header values +type headerGetter func(key string) []string + +// extractFromHeaders is the internal implementation that works with a header getter +func extractFromHeaders(get headerGetter) *AuthContext { + // Check if auth is active - if not present, return nil token + if active := get(HeaderKeyActive); len(active) == 0 || active[0] != "true" { + glog.V(2).Infof("extractFromHeaders: auth not active, returning nil token") + return NilAuthContext() + } + + authCtx := &AuthContext{} + + // Extract level + if levels := get(HeaderKeyLevel); len(levels) > 0 { + authCtx.Level = levels[0] + glog.V(2).Infof("extractFromHeaders: found level=%q", authCtx.Level) + } + + // Extract programs (comma-separated) + if programs := get(HeaderKeyPrograms); len(programs) > 0 && programs[0] != "" { + authCtx.Programs = splitPrograms(programs[0]) + glog.V(2).Infof("extractFromHeaders: found programs=%v", authCtx.Programs) + } + + glog.V(2).Infof("extractFromHeaders: auth active, level=%q, programs=%v", authCtx.Level, authCtx.Programs) + return authCtx +} + +// AttachToOutgoingContext attaches auth context to outgoing gRPC metadata. +// Used by clients to send auth info to the server via gRPC. +func AttachToOutgoingContext(ctx context.Context, authCtx *AuthContext) context.Context { + if authCtx == nil { + return ctx + } + + // Always include the active marker to indicate auth is present + pairs := []string{HeaderKeyActive, "true"} + if authCtx.Level != "" { + pairs = append(pairs, HeaderKeyLevel, authCtx.Level) + } + if len(authCtx.Programs) > 0 { + pairs = append(pairs, HeaderKeyPrograms, joinPrograms(authCtx.Programs)) + } + + return metadata.AppendToOutgoingContext(ctx, pairs...) +} + +// ToHeaders converts an AuthContext to a map of headers. +// Useful for HTTP clients or other transports. +func (a *AuthContext) ToHeaders() map[string]string { + if a == nil { + return nil + } + headers := map[string]string{ + HeaderKeyActive: "true", + } + if a.Level != "" { + headers[HeaderKeyLevel] = a.Level + } + if len(a.Programs) > 0 { + headers[HeaderKeyPrograms] = joinPrograms(a.Programs) + } + return headers +} + +// splitPrograms splits a comma-separated string into a slice +func splitPrograms(s string) []string { + if s == "" { + return nil + } + parts := strings.Split(s, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + if trimmed := strings.TrimSpace(p); trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + +// joinPrograms joins a slice into a comma-separated string +func joinPrograms(programs []string) string { + return strings.Join(programs, ",") +} From ee0bb3cd241194facabbf365ac1ac383c870550a Mon Sep 17 00:00:00 2001 From: mattthew Date: Tue, 3 Feb 2026 19:59:40 -0500 Subject: [PATCH 06/10] Add "Programs" auth via facets mechanism (insert phase) Programs are assigned at mutation time via declared programs in security context --- edgraph/server.go | 51 +++ systest/label/label_auth_test.go | 603 +++++++++++++++++++++++++++++++ worker/task.go | 249 +++++++++++-- x/keys.go | 10 + 4 files changed, 880 insertions(+), 33 deletions(-) diff --git a/edgraph/server.go b/edgraph/server.go index 64c3a3de762..d28490d881c 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -48,6 +48,7 @@ import ( "github.com/dgraph-io/dgraph/v25/types/facets" "github.com/dgraph-io/dgraph/v25/worker" "github.com/dgraph-io/dgraph/v25/x" + "github.com/dgraph-io/dgraph/v25/x/auth" ) const ( @@ -558,6 +559,11 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo return err } + // Inject program facets from auth context into edges + if err := injectProgramFacets(ctx, edges); err != nil { + return err + } + if len(edges) > x.Config.LimitMutationsNquad { return errors.Errorf("NQuad count in the request: %d, is more that threshold: %d", len(edges), x.Config.LimitMutationsNquad) @@ -2238,3 +2244,48 @@ func parseSubject(predSubject string) (uint64, error) { return dql.ParseUid(predSubject) } } + +// injectProgramFacets adds the dgraph.programs facet to edges based on the auth context. +// If the user has programs in their auth context, those programs are attached to each edge +// being mutated. This enables server-side filtering during queries. +func injectProgramFacets(ctx context.Context, edges []*pb.DirectedEdge) error { + authCtx := auth.ExtractOrNil(ctx) + if authCtx == nil || len(authCtx.Programs) == 0 { + return nil + } + + // Create the program facet with comma-separated programs + programsValue := strings.Join(authCtx.Programs, ",") + programFacet := &api.Facet{ + Key: x.ProgramFacetKey, + Value: []byte(programsValue), + ValType: api.Facet_STRING, + } + + // Add the program facet to each SET edge + for _, edge := range edges { + if edge.Op != pb.DirectedEdge_SET { + continue + } + + // Check if program facet already exists (user explicitly set it) + hasProgram := false + for _, f := range edge.Facets { + if f.Key == x.ProgramFacetKey { + hasProgram = true + break + } + } + + // Only add if not already present + if !hasProgram { + edge.Facets = append(edge.Facets, programFacet) + // Re-sort facets to maintain order + sort.Slice(edge.Facets, func(i, j int) bool { + return edge.Facets[i].Key < edge.Facets[j].Key + }) + } + } + + return nil +} diff --git a/systest/label/label_auth_test.go b/systest/label/label_auth_test.go index ac2e26173c9..7d5d6e0505f 100644 --- a/systest/label/label_auth_test.go +++ b/systest/label/label_auth_test.go @@ -10,6 +10,7 @@ package main import ( "context" "encoding/json" + "fmt" "testing" "time" @@ -297,3 +298,605 @@ func TestLabelAuthWithoutACL(t *testing.T) { require.Empty(t, result.Agents[0].Alias) t.Log("Test passed: label auth works without ACL enabled") } + +// setupProgramAuthTest sets up test data with program-labeled edges for program auth tests. +func setupProgramAuthTest(t *testing.T) { + t.Log("Setting up program auth test...") + dg := waitForCluster(t) + ctx := context.Background() + + // Drop all and set up schema + t.Log("Dropping all data...") + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + + // Simple schema without labels - we'll use program facets on the data + t.Log("Applying schema...") + schema := ` + name: string @index(term) . + project: string @index(term) . + location: string @index(term) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + t.Log("Waiting 2s for schema to propagate...") + time.Sleep(2 * time.Second) +} + +// TestProgramAuthMutationInjectsPrograms verifies that mutations automatically get program facets. +func TestProgramAuthMutationInjectsPrograms(t *testing.T) { + t.Log("=== TestProgramAuthMutationInjectsPrograms: Mutations should auto-inject program facets ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Create context with programs - these should be auto-injected as facets + ctx := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA", "BRAVO"}, + }) + + // Insert data with program context + t.Log("Inserting data with programs ALPHA,BRAVO...") + _, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:p1 "Project Alpha-Bravo" . + _:p1 "classified" . + `), + }) + require.NoError(t, err) + + // Query with same programs - should see the data (program filtering is server-side) + t.Log("Querying with matching programs...") + resp, err := dg.NewTxn().Query(ctx, ` + { + projects(func: has(name)) { + name + project + } + } + `) + require.NoError(t, err) + t.Logf("Query response: %s", string(resp.GetJson())) + + // Verify data is returned (program facets are enforced server-side, not via @facets directive) + var result struct { + Projects []struct { + Name string `json:"name"` + Project string `json:"project"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "should have 1 project") + require.Equal(t, "Project Alpha-Bravo", result.Projects[0].Name) + require.Equal(t, "classified", result.Projects[0].Project) + t.Log("Test passed: mutation injected programs and query returned data") +} + +// TestProgramAuthFiltersByProgram verifies that queries filter data based on user programs. +func TestProgramAuthFiltersByProgram(t *testing.T) { + t.Log("=== TestProgramAuthFiltersByProgram: Queries should filter based on user programs ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + t.Log("Inserting ALPHA project...") + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Project" .`), + }) + require.NoError(t, err) + + // Insert data with BRAVO program + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + t.Log("Inserting BRAVO project...") + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "Bravo Project" .`), + }) + require.NoError(t, err) + + // Query with ALPHA program - should only see ALPHA data + t.Log("Querying with ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: has(name)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("ALPHA query response: %s", string(resp.GetJson())) + + var alphaResult struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &alphaResult)) + require.Len(t, alphaResult.Projects, 1, "ALPHA user should see 1 project") + require.Equal(t, "Alpha Project", alphaResult.Projects[0].Name) + + // Query with BRAVO program - should only see BRAVO data + t.Log("Querying with BRAVO program...") + resp, err = dg.NewTxn().Query(ctxBravo, ` + { + projects(func: has(name)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("BRAVO query response: %s", string(resp.GetJson())) + + var bravoResult struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &bravoResult)) + require.Len(t, bravoResult.Projects, 1, "BRAVO user should see 1 project") + require.Equal(t, "Bravo Project", bravoResult.Projects[0].Name) + + t.Log("Test passed: queries correctly filter by program") +} + +// TestProgramAuthNoPrograms verifies that users without programs only see public data. +func TestProgramAuthNoPrograms(t *testing.T) { + t.Log("=== TestProgramAuthNoPrograms: Users without programs should only see public data ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program (protected) + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + t.Log("Inserting ALPHA project (protected)...") + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Project" .`), + }) + require.NoError(t, err) + + // Insert data without programs (public - accessible to everyone) + t.Log("Inserting public project (no programs)...") + _, err = dg.NewTxn().Mutate(context.Background(), &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "Public Project" .`), + }) + require.NoError(t, err) + + // Query with empty programs (auth active but no programs) - should only see public data + ctxNoPrograms := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{}, + }) + t.Log("Querying with auth active but no programs...") + resp, err := dg.NewTxn().Query(ctxNoPrograms, ` + { + projects(func: has(name)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("No-program query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + // Should only see public project (user has no programs, can't access program-protected data) + require.Len(t, result.Projects, 1, "user without programs should only see public data") + require.Equal(t, "Public Project", result.Projects[0].Name) + t.Log("Test passed: users without programs only see public data") +} + +// TestProgramAuthMultiplePrograms verifies OR logic for multiple programs. +func TestProgramAuthMultiplePrograms(t *testing.T) { + t.Log("=== TestProgramAuthMultiplePrograms: Multiple programs use OR logic ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert ALPHA project + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Only" .`), + }) + require.NoError(t, err) + + // Insert BRAVO project + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "Bravo Only" .`), + }) + require.NoError(t, err) + + // Query with both ALPHA and BRAVO programs - should see both + ctxBoth := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA", "BRAVO"}, + }) + t.Log("Querying with ALPHA and BRAVO programs...") + resp, err := dg.NewTxn().Query(ctxBoth, ` + { + projects(func: has(name)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("Multi-program query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 2, "user with both programs should see both projects") + t.Log("Test passed: multiple programs use OR logic") +} + +// TestProgramAuthEqFunction tests program auth with eq() comparison function. +func TestProgramAuthEqFunction(t *testing.T) { + t.Log("=== TestProgramAuthEqFunction: eq() should respect program auth ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert ALPHA project + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Project" .`), + }) + require.NoError(t, err) + + // Insert BRAVO project with same name pattern + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "Alpha Project" .`), + }) + require.NoError(t, err) + + // Query with eq() using ALPHA program - should only see ALPHA's data + t.Log("Querying with eq() and ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: eq(name, "Alpha Project")) { + name + } + } + `) + require.NoError(t, err) + t.Logf("eq() query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "ALPHA user should see only 1 matching project") + t.Log("Test passed: eq() respects program auth") +} + +// TestProgramAuthTermSearch tests program auth with term search functions. +func TestProgramAuthTermSearch(t *testing.T) { + t.Log("=== TestProgramAuthTermSearch: allofterms/anyofterms should respect program auth ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert ALPHA project + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "secret mission alpha" .`), + }) + require.NoError(t, err) + + // Insert BRAVO project with overlapping terms + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "secret mission bravo" .`), + }) + require.NoError(t, err) + + // Query with anyofterms using ALPHA program + t.Log("Querying with anyofterms() and ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: anyofterms(name, "secret mission")) { + name + } + } + `) + require.NoError(t, err) + t.Logf("anyofterms() query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "ALPHA user should see only their project") + require.Contains(t, result.Projects[0].Name, "alpha") + t.Log("Test passed: term search respects program auth") +} + +// TestProgramAuthRegexp tests program auth with regexp() function. +func TestProgramAuthRegexp(t *testing.T) { + t.Log("=== TestProgramAuthRegexp: regexp() should respect program auth ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Setup schema with trigram index for regexp + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + schema := ` + name: string @index(trigram) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + time.Sleep(2 * time.Second) + + // Insert ALPHA project + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Project-Alpha-001" .`), + }) + require.NoError(t, err) + + // Insert BRAVO project + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "Project-Bravo-002" .`), + }) + require.NoError(t, err) + + // Query with regexp using ALPHA program + t.Log("Querying with regexp() and ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: regexp(name, /Project-.*/)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("regexp() query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "ALPHA user should see only their project") + require.Contains(t, result.Projects[0].Name, "Alpha") + t.Log("Test passed: regexp() respects program auth") +} + +// TestProgramAuthMatch tests program auth with match() fuzzy function. +func TestProgramAuthMatch(t *testing.T) { + t.Log("=== TestProgramAuthMatch: match() should respect program auth ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Setup schema with trigram index for match + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + schema := ` + name: string @index(trigram) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + time.Sleep(2 * time.Second) + + // Insert ALPHA project + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "AlphaProject" .`), + }) + require.NoError(t, err) + + // Insert BRAVO project + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p2 "BravoProject" .`), + }) + require.NoError(t, err) + + // Query with match using ALPHA program (fuzzy match with edit distance 3) + t.Log("Querying with match() and ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: match(name, "AlphaProjct", 3)) { + name + } + } + `) + require.NoError(t, err) + t.Logf("match() query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "ALPHA user should see only their project") + require.Equal(t, "AlphaProject", result.Projects[0].Name) + t.Log("Test passed: match() respects program auth") +} + +// TestProgramAuthUidTraversal tests program auth with UID traversal. +func TestProgramAuthUidTraversal(t *testing.T) { + t.Log("=== TestProgramAuthUidTraversal: UID traversal should respect program auth ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Setup schema with edges + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + schema := ` + name: string @index(term) . + member: [uid] . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + time.Sleep(2 * time.Second) + + // Insert team and members with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:team "Alpha Team" . + _:m1 "Alice" . + _:m2 "Bob" . + _:team _:m1 . + _:team _:m2 . + `), + }) + require.NoError(t, err) + teamUID := resp.Uids["team"] + + // Insert team with BRAVO program + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:team2 "Bravo Team" . + _:m3 "Charlie" . + _:team2 _:m3 . + `), + }) + require.NoError(t, err) + + // Query team by UID with ALPHA program - should see team and members + t.Log("Querying team by UID with ALPHA program...") + queryResp, err := dg.NewTxn().Query(ctxAlpha, fmt.Sprintf(` + { + team(func: uid(%s)) { + name + member { + name + } + } + } + `, teamUID)) + require.NoError(t, err) + t.Logf("UID traversal response: %s", string(queryResp.GetJson())) + + var result struct { + Team []struct { + Name string `json:"name"` + Member []struct { + Name string `json:"name"` + } `json:"member"` + } `json:"team"` + } + require.NoError(t, json.Unmarshal(queryResp.GetJson(), &result)) + require.Len(t, result.Team, 1) + require.Equal(t, "Alpha Team", result.Team[0].Name) + require.Len(t, result.Team[0].Member, 2, "should see both members") + t.Log("Test passed: UID traversal respects program auth") +} + +// TestProgramAuthComparisonOps tests program auth with comparison operators (lt, gt, le, ge). +func TestProgramAuthComparisonOps(t *testing.T) { + t.Log("=== TestProgramAuthComparisonOps: comparison operators should respect program auth ===") + dg := waitForCluster(t) + ctx := context.Background() + + // Setup schema with int index + require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) + schema := ` + name: string @index(term) . + priority: int @index(int) . + ` + require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: schema})) + time.Sleep(2 * time.Second) + + // Insert ALPHA projects with various priorities + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + _, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:p1 "Alpha High" . + _:p1 "10" . + _:p2 "Alpha Low" . + _:p2 "2" . + `), + }) + require.NoError(t, err) + + // Insert BRAVO project with high priority + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:p3 "Bravo High" . + _:p3 "10" . + `), + }) + require.NoError(t, err) + + // Query with gt() using ALPHA program + t.Log("Querying with gt() and ALPHA program...") + resp, err := dg.NewTxn().Query(ctxAlpha, ` + { + projects(func: gt(priority, 5)) { + name + priority + } + } + `) + require.NoError(t, err) + t.Logf("gt() query response: %s", string(resp.GetJson())) + + var result struct { + Projects []struct { + Name string `json:"name"` + Priority int `json:"priority"` + } `json:"projects"` + } + require.NoError(t, json.Unmarshal(resp.GetJson(), &result)) + require.Len(t, result.Projects, 1, "ALPHA user should see only their high priority project") + require.Equal(t, "Alpha High", result.Projects[0].Name) + t.Log("Test passed: comparison operators respect program auth") +} diff --git a/worker/task.go b/worker/task.go index e5c5dd20835..16d2a2a72b8 100644 --- a/worker/task.go +++ b/worker/task.go @@ -38,6 +38,7 @@ import ( "github.com/dgraph-io/dgraph/v25/types" "github.com/dgraph-io/dgraph/v25/types/facets" "github.com/dgraph-io/dgraph/v25/x" + "github.com/dgraph-io/dgraph/v25/x/auth" ) func invokeNetworkRequest(ctx context.Context, addr string, @@ -296,6 +297,7 @@ func needsIntersect(fnName string) bool { } type funcArgs struct { + ctx context.Context q *pb.Query gid uint32 srcFn *functionContext @@ -395,8 +397,28 @@ func (qs *queryState) handleValuePostings(ctx context.Context, args funcArgs) er if err != nil && !strings.Contains(err.Error(), hnsw.EmptyHNSWTreeError+": "+badger.ErrKeyNotFound.Error()) { return err } - sort.Slice(nnUids, func(i, j int) bool { return nnUids[i] < nnUids[j] }) - args.out.UidMatrix = append(args.out.UidMatrix, &pb.List{Uids: nnUids}) + // Filter UIDs by program authorization + var authorizedUids []uint64 + for _, uid := range nnUids { + key := x.DataKey(q.Attr, uid) + pl, err := qs.cache.Get(key) + if err != nil { + continue + } + hasAuthorized := false + _ = pl.Iterate(q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if hasAuthorized { + authorizedUids = append(authorizedUids, uid) + } + } + sort.Slice(authorizedUids, func(i, j int) bool { return authorizedUids[i] < authorizedUids[j] }) + args.out.UidMatrix = append(args.out.UidMatrix, &pb.List{Uids: authorizedUids}) return nil } @@ -471,8 +493,22 @@ func (qs *queryState) handleValuePostings(ctx context.Context, args funcArgs) er &pb.ValueList{Values: []*pb.TaskValue{}}) continue } - vals = make([]types.Val, len(pl.Postings)) - for i, p := range pl.Postings { + // Filter postings by program authorization + var authorizedPostings []*pb.Posting + for _, p := range pl.Postings { + if checkProgramAuthorization(ctx, p.Facets) { + authorizedPostings = append(authorizedPostings, p) + } + } + if len(authorizedPostings) == 0 { + out.UidMatrix = append(out.UidMatrix, &pb.List{}) + out.FacetMatrix = append(out.FacetMatrix, &pb.FacetsList{}) + out.ValueMatrix = append(out.ValueMatrix, + &pb.ValueList{Values: []*pb.TaskValue{}}) + continue + } + vals = make([]types.Val, len(authorizedPostings)) + for i, p := range authorizedPostings { vals[i] = types.Val{ Tid: types.TypeID(p.ValType), Value: p.Value, @@ -667,6 +703,12 @@ func facetsFilterValuePostingList(args funcArgs, pl *posting.List, facetsTree *f if err != nil { return err } + + // Check program authorization - filter out postings user doesn't have access to + if picked && !checkProgramAuthorization(args.ctx, p.Facets) { + picked = false + } + if picked { fn(p) } @@ -715,7 +757,7 @@ func retrieveValuesAndFacets(args funcArgs, pl *posting.List, facetsTree *facets return vals, &pb.FacetsList{FacetsList: fcs}, nil } -func facetsFilterUidPostingList(pl *posting.List, facetsTree *facetsTree, opts posting.ListOptions, +func facetsFilterUidPostingList(ctx context.Context, pl *posting.List, facetsTree *facetsTree, opts posting.ListOptions, fn func(*pb.Posting)) error { return pl.Postings(opts, func(p *pb.Posting) error { @@ -724,6 +766,12 @@ func facetsFilterUidPostingList(pl *posting.List, facetsTree *facetsTree, opts p if err != nil { return err } + + // Check program authorization - filter out postings user doesn't have access to + if pick && !checkProgramAuthorization(ctx, p.Facets) { + pick = false + } + if pick { fn(p) } @@ -735,7 +783,7 @@ func countForUidPostings(args funcArgs, pl *posting.List, facetsTree *facetsTree opts posting.ListOptions) (int, error) { var filteredCount int - err := facetsFilterUidPostingList(pl, facetsTree, opts, func(p *pb.Posting) { + err := facetsFilterUidPostingList(args.ctx, pl, facetsTree, opts, func(p *pb.Posting) { filteredCount++ }) if err != nil { @@ -754,7 +802,7 @@ func retrieveUidsAndFacets(args funcArgs, pl *posting.List, facetsTree *facetsTr Uids: make([]uint64, 0, pl.ApproxLen()), // preallocate uid slice. } - err := facetsFilterUidPostingList(pl, facetsTree, opts, func(p *pb.Posting) { + err := facetsFilterUidPostingList(args.ctx, pl, facetsTree, opts, func(p *pb.Posting) { uidList.Uids = append(uidList.Uids, p.Uid) if q.FacetParam != nil { fcsList = append(fcsList, &pb.Facets{ @@ -915,8 +963,22 @@ func (qs *queryState) handleUidPostings( return err } if !empty { - tlist := &pb.List{Uids: []uint64{q.UidList.Uids[i]}} - out.UidMatrix = append(out.UidMatrix, tlist) + // Check program authorization before including this UID + hasAuthorized := false + iterErr := pl.Iterate(args.q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if iterErr != nil && iterErr != posting.ErrStopIteration { + return iterErr + } + if hasAuthorized { + tlist := &pb.List{Uids: []uint64{q.UidList.Uids[i]}} + out.UidMatrix = append(out.UidMatrix, tlist) + } } case srcFn.fnType == uidInFn: if i == 0 { @@ -934,8 +996,22 @@ func (qs *queryState) handleUidPostings( return err } if len(plist.Uids) > 0 { - tlist := &pb.List{Uids: []uint64{q.UidList.Uids[i]}} - out.UidMatrix = append(out.UidMatrix, tlist) + // Check program authorization before including this UID + hasAuthorized := false + iterErr := pl.Iterate(args.q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if iterErr != nil && iterErr != posting.ErrStopIteration { + return iterErr + } + if hasAuthorized { + tlist := &pb.List{Uids: []uint64{q.UidList.Uids[i]}} + out.UidMatrix = append(out.UidMatrix, tlist) + } } case q.FacetParam != nil || facetsTree != nil: if i == 0 { @@ -953,7 +1029,8 @@ func (qs *queryState) handleUidPostings( if i == 0 { span.AddEvent("default no facets") } - uidList, err := pl.Uids(opts) + // Use facetsFilterUidPostingList to apply program authorization even without facet params + uidList, _, err := retrieveUidsAndFacets(args, pl, nil, opts) if err != nil { return err } @@ -1119,7 +1196,7 @@ func (qs *queryState) helpProcessTask(ctx context.Context, q *pb.Query, gid uint opts.Intersect = q.UidList } - args := funcArgs{q, gid, srcFn, out} + args := funcArgs{ctx, q, gid, srcFn, out} needsValPostings, err := srcFn.needsValuePostings(typ) if err != nil { return nil, err @@ -1311,6 +1388,19 @@ func (qs *queryState) handleRegexFunction(ctx context.Context, arg funcArgs) err return err } + // Check program authorization before processing + hasAuthorized := false + _ = pl.Iterate(arg.q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if !hasAuthorized { + continue + } + vals := make([]types.Val, 1) switch { case lang != "": @@ -1393,14 +1483,27 @@ func (qs *queryState) handleCompareFunction(ctx context.Context, arg funcArgs) e var filterErr error algo.ApplyFilter(arg.out.UidMatrix[row], func(uid uint64, i int) bool { + // Check program authorization first + pl, err := qs.cache.Get(x.DataKey(attr, uid)) + if err != nil { + filterErr = err + return false + } + hasAuthorized := false + _ = pl.Iterate(arg.q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if !hasAuthorized { + return false + } + switch lang { case "": if isList { - pl, err := qs.cache.Get(x.DataKey(attr, uid)) - if err != nil { - filterErr = err - return false - } svs, err := pl.AllUntaggedValues(arg.q.ReadTs) if err != nil { if err != posting.ErrNoValue { @@ -1418,11 +1521,6 @@ func (qs *queryState) handleCompareFunction(ctx context.Context, arg funcArgs) e return false } - pl, err := qs.cache.Get(x.DataKey(attr, uid)) - if err != nil { - filterErr = err - return false - } sv, err := pl.Value(arg.q.ReadTs) if err != nil { if err != posting.ErrNoValue { @@ -1433,11 +1531,6 @@ func (qs *queryState) handleCompareFunction(ctx context.Context, arg funcArgs) e dst, err := types.Convert(sv, typ) return err == nil && compareFunc(dst) case ".": - pl, err := qs.cache.Get(x.DataKey(attr, uid)) - if err != nil { - filterErr = err - return false - } values, err := pl.AllValues(arg.q.ReadTs) // does not return ErrNoValue if err != nil { filterErr = err @@ -1451,11 +1544,6 @@ func (qs *queryState) handleCompareFunction(ctx context.Context, arg funcArgs) e } return false default: - pl, err := qs.cache.Get(x.DataKey(attr, uid)) - if err != nil { - filterErr = err - return false - } src, err := pl.ValueFor(arg.q.ReadTs, arg.q.Langs) if err != nil { if err != posting.ErrNoValue { @@ -1575,6 +1663,19 @@ func (qs *queryState) handleMatchFunction(ctx context.Context, arg funcArgs) err return err } + // Check program authorization before processing + hasAuthorized := false + _ = pl.Iterate(arg.q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if !hasAuthorized { + continue + } + vals := make([]types.Val, 1) switch { case lang != "": @@ -2631,6 +2732,23 @@ func (qs *queryState) handleHasWithOrderFunction(ctx context.Context, q *pb.Quer if err := checkInclusion(uid); err != nil { continue } + // Check program authorization for this UID + dataKey := x.DataKey(q.Attr, uid) + dataList, err := qs.cache.Get(dataKey) + if err != nil { + continue + } + hasAuthorized := false + _ = dataList.Iterate(q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorized = true + return posting.ErrStopIteration + } + return nil + }) + if !hasAuthorized { + continue + } result.Uids = append(result.Uids, uid) } @@ -2723,6 +2841,22 @@ func (qs *queryState) handleHasFunction(ctx context.Context, q *pb.Query, out *p AllVersions: true, CheckInclusion: checkInclusion, Function: func(l *posting.List, pk x.ParsedKey) error { + // Check program authorization - iterate postings to see if any are authorized + hasAuthorizedPosting := false + err := l.Iterate(q.ReadTs, 0, func(p *pb.Posting) error { + if checkProgramAuthorization(ctx, p.Facets) { + hasAuthorizedPosting = true + return posting.ErrStopIteration + } + return nil + }) + if err != nil && err != posting.ErrStopIteration { + return err + } + if !hasAuthorizedPosting { + return nil // Skip this UID - no authorized postings + } + if cnt < q.Offset { cnt++ return nil @@ -2798,3 +2932,52 @@ func parseSimilarToOptions(args []string, fc *functionContext) error { } return nil } + +// checkProgramAuthorization checks if a posting is authorized based on the user's programs. +// Returns true if the posting should be included, false if it should be filtered out. +// If the auth context is nil (no auth active), all postings are allowed (backward compat). +// If the posting has no program facet, it's accessible to everyone. +// If the posting has programs but user has none, access is denied. +// If both have programs, at least one must match (OR logic). +func checkProgramAuthorization(ctx context.Context, postingFacets []*api.Facet) bool { + authCtx := auth.ExtractOrNil(ctx) + if authCtx == nil { + // No auth context - allow all postings (backward compatibility) + return true + } + + // Find the program facet on this posting + var programFacet *api.Facet + for _, f := range postingFacets { + if f.Key == x.ProgramFacetKey { + programFacet = f + break + } + } + + if programFacet == nil { + // No program facet on posting - accessible to everyone + return true + } + + // Posting has programs - user must have at least one matching program + if len(authCtx.Programs) == 0 { + // User has no programs but posting requires them - deny access + return false + } + + // Parse the comma-separated programs from the facet + postingPrograms := strings.Split(string(programFacet.Value), ",") + + // Check if any user program matches any posting program (OR logic) + for _, userProg := range authCtx.Programs { + for _, postProg := range postingPrograms { + if strings.TrimSpace(userProg) == strings.TrimSpace(postProg) { + return true + } + } + } + + // No matching programs - filter out this posting + return false +} diff --git a/x/keys.go b/x/keys.go index 94112d07c03..8850cebd08f 100644 --- a/x/keys.go +++ b/x/keys.go @@ -782,3 +782,13 @@ func IsPreDefinedType(typ string) bool { func isReservedName(name string) bool { return strings.HasPrefix(strings.ToLower(name), "dgraph.") } + +// ProgramFacetKey is the reserved facet key used for storing program labels on edges. +// This facet is automatically added during mutations when the user has programs in their +// auth context, and is used for server-side filtering during queries. +const ProgramFacetKey = "dgraph.programs" + +// IsReservedFacetKey returns true if the facet key is reserved for internal usage. +func IsReservedFacetKey(key string) bool { + return key == ProgramFacetKey +} From 9620bb2ce6b5d800b81e493784ee3537070efbdd Mon Sep 17 00:00:00 2001 From: mattthew Date: Tue, 3 Feb 2026 20:49:51 -0500 Subject: [PATCH 07/10] Updates and Deletes protected --- edgraph/server.go | 152 +++++++++++++++++++++++++++++++ posting/lists.go | 9 ++ systest/label/label_auth_test.go | 143 +++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+) diff --git a/edgraph/server.go b/edgraph/server.go index d28490d881c..5604a2e2d6e 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -16,6 +16,7 @@ import ( "sort" "strconv" "strings" + "sync" "sync/atomic" "time" "unicode" @@ -573,6 +574,11 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo if err != nil { return errors.Wrapf(err, "While doing mutations:") } + + // Check program authorization for modifying existing data (after namespace is available) + if err := checkMutationProgramAuth(ctx, edges, ns); err != nil { + return err + } predHints := make(map[string]pb.Metadata_HintType) for _, gmu := range qc.gmuList { for pred, hint := range gmu.Metadata.GetPredHints() { @@ -660,6 +666,10 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo resp.Txn.Keys = resp.Txn.Keys[:0] resp.Txn.CommitTs = cts calculateMutationMetrics() + + // Update the program facet cache for future authorization checks + updateProgramFacetCache(ns, edges) + return nil } @@ -2245,6 +2255,148 @@ func parseSubject(predSubject string) (uint64, error) { } } +// programFacetCache stores recently committed program facets for authorization checks. +// +// WHY THIS EXISTS: +// Dgraph's MemoryLayer is a read-through cache - it only populates on reads, not writes. +// When a mutation commits, the data goes to badger asynchronously. The MemoryLayer's +// updateItemInCache() only updates keys that are ALREADY cached; it doesn't add new ones. +// (See posting/mvcc.go:589-591) +// +// For program authorization, we need to check if a user can modify existing data BEFORE +// the Raft proposal. But for recently created data, the MemoryLayer won't have it cached +// and badger won't have synced it yet, so GetNoStoreSafe returns 0 postings. +// +// This cache provides write-through semantics specifically for program facets: +// - Updated after successful CommitOverNetwork in doMutate() +// - Checked in checkMutationProgramAuth() before the posting store fallback +// +// FUTURE CONSIDERATION: +// A more holistic approach might store program data directly in the Posting protobuf +// rather than as facets, which would integrate naturally with the existing caching. +// However, that would require protobuf changes and migration strategy. +var programFacetCache = struct { + sync.RWMutex + // Key: "namespace-predicate-entity", Value: comma-separated programs + data map[string]string +}{data: make(map[string]string)} + +func programFacetCacheKey(ns uint64, attr string, entity uint64) string { + return fmt.Sprintf("%d-%s-%d", ns, attr, entity) +} + +// updateProgramFacetCache updates the cache with program facets from committed mutations. +func updateProgramFacetCache(ns uint64, edges []*pb.DirectedEdge) { + programFacetCache.Lock() + defer programFacetCache.Unlock() + + for _, edge := range edges { + if edge.Entity == 0 { + continue + } + for _, f := range edge.Facets { + if f.Key == x.ProgramFacetKey { + key := programFacetCacheKey(ns, edge.Attr, edge.Entity) + programFacetCache.data[key] = string(f.Value) + glog.V(1).Infof("updateProgramFacetCache: cached programs=%s for key=%s", f.Value, key) + break + } + } + } +} + +// checkMutationProgramAuth verifies that the user is authorized to modify existing data +// protected by program facets. This check happens before Raft proposal. +func checkMutationProgramAuth(ctx context.Context, edges []*pb.DirectedEdge, ns uint64) error { + authCtx := auth.ExtractOrNil(ctx) + if authCtx == nil || authCtx.IsNil { + // No auth context means no program restrictions - allow mutation + glog.V(1).Infof("checkMutationProgramAuth: no auth context, allowing mutation") + return nil + } + + glog.V(1).Infof("checkMutationProgramAuth: auth context has programs=%v", authCtx.Programs) + + // Build a set of user's programs for fast lookup + userPrograms := make(map[string]bool) + for _, p := range authCtx.Programs { + userPrograms[p] = true + } + + for _, edge := range edges { + // Only check existing entities (non-zero UID) + if edge.Entity == 0 { + continue + } + + // First check the in-memory cache for recently committed program facets + cacheKey := programFacetCacheKey(ns, edge.Attr, edge.Entity) + programFacetCache.RLock() + cachedPrograms, found := programFacetCache.data[cacheKey] + programFacetCache.RUnlock() + + if found { + glog.V(1).Infof("checkMutationProgramAuth: found cached programs=%s for Entity=%d, Attr=%s", + cachedPrograms, edge.Entity, edge.Attr) + programs := strings.Split(cachedPrograms, ",") + hasMatch := false + for _, prog := range programs { + if userPrograms[prog] { + hasMatch = true + break + } + } + if !hasMatch && len(programs) > 0 && programs[0] != "" { + glog.V(1).Infof("checkMutationProgramAuth: DENYING - no program match (from cache)") + return errors.Errorf("not authorized to modify program-protected data") + } + continue // Authorized via cache, move to next edge + } + + // Fall back to reading from posting store for older data + namespacedAttr := x.NamespaceAttr(ns, edge.Attr) + key := x.DataKey(namespacedAttr, edge.Entity) + glog.V(1).Infof("checkMutationProgramAuth: checking store for Entity=%d, Attr=%s", edge.Entity, edge.Attr) + + pl, err := posting.GetNoStoreSafe(key, math.MaxUint64) + if err != nil || pl == nil { + continue + } + + // Check each existing posting for program authorization + var authErr error + pl.Iterate(math.MaxUint64, 0, func(p *pb.Posting) error { + for _, f := range p.Facets { + if f.Key == x.ProgramFacetKey { + programs := strings.Split(string(f.Value), ",") + glog.V(1).Infof("checkMutationProgramAuth: store has programs=%v, user has=%v", programs, authCtx.Programs) + hasMatch := false + for _, prog := range programs { + if userPrograms[prog] { + hasMatch = true + break + } + } + if !hasMatch && len(programs) > 0 && programs[0] != "" { + glog.V(1).Infof("checkMutationProgramAuth: DENYING - no program match (from store)") + authErr = errors.Errorf("not authorized to modify program-protected data") + return posting.ErrStopIteration + } + break + } + } + return nil + }) + + if authErr != nil { + return authErr + } + } + + glog.V(1).Infof("checkMutationProgramAuth: allowing mutation") + return nil +} + // injectProgramFacets adds the dgraph.programs facet to edges based on the auth context. // If the user has programs in their auth context, those programs are attached to each edge // being mutated. This enables server-side filtering during queries. diff --git a/posting/lists.go b/posting/lists.go index a4bc4fb355b..174dda15d2f 100644 --- a/posting/lists.go +++ b/posting/lists.go @@ -57,6 +57,15 @@ func GetNoStore(key []byte, readTs uint64) (rlist *List, err error) { return getNew(key, pstore, readTs, false) } +// GetNoStoreSafe is like GetNoStore but returns nil if the store is not initialized. +// This is safe to call even before the posting package is fully initialized. +func GetNoStoreSafe(key []byte, readTs uint64) (rlist *List, err error) { + if pstore == nil { + return nil, nil + } + return getNew(key, pstore, readTs, false) +} + // LocalCache stores a cache of posting lists and deltas. // This doesn't sync, so call this only when you don't care about dirty posting lists in // memory(for example before populating snapshot) or after calling syncAllMarks diff --git a/systest/label/label_auth_test.go b/systest/label/label_auth_test.go index 7d5d6e0505f..c45efe6c6d1 100644 --- a/systest/label/label_auth_test.go +++ b/systest/label/label_auth_test.go @@ -833,6 +833,149 @@ func TestProgramAuthUidTraversal(t *testing.T) { t.Log("Test passed: UID traversal respects program auth") } +// TestProgramAuthUpdateBlocked tests that users cannot update program-protected data they don't have access to. +func TestProgramAuthUpdateBlocked(t *testing.T) { + t.Log("=== TestProgramAuthUpdateBlocked: Users cannot update data they can't access ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Secret" .`), + }) + require.NoError(t, err) + uid := resp.Uids["p1"] + t.Logf("Created ALPHA-protected node with UID: %s", uid) + + // Try to update with BRAVO program - should fail + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + t.Log("Attempting to update ALPHA data with BRAVO credentials...") + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(fmt.Sprintf(`<%s> "Hacked by BRAVO" .`, uid)), + }) + require.Error(t, err, "BRAVO user should not be able to update ALPHA data") + require.Contains(t, err.Error(), "not authorized", "error should indicate authorization failure") + t.Log("Test passed: update correctly blocked") +} + +// TestProgramAuthDeleteBlocked tests that users cannot delete program-protected data they don't have access to. +func TestProgramAuthDeleteBlocked(t *testing.T) { + t.Log("=== TestProgramAuthDeleteBlocked: Users cannot delete data they can't access ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Alpha Secret" .`), + }) + require.NoError(t, err) + uid := resp.Uids["p1"] + t.Logf("Created ALPHA-protected node with UID: %s", uid) + + // Try to delete with BRAVO program - should fail + ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"BRAVO"}, + }) + t.Log("Attempting to delete ALPHA data with BRAVO credentials...") + _, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{ + CommitNow: true, + DelNquads: []byte(fmt.Sprintf(`<%s> * .`, uid)), + }) + require.Error(t, err, "BRAVO user should not be able to delete ALPHA data") + require.Contains(t, err.Error(), "not authorized", "error should indicate authorization failure") + + // Verify data still exists with ALPHA credentials + queryResp, err := dg.NewTxn().Query(ctxAlpha, fmt.Sprintf(` + { + node(func: uid(%s)) { + name + } + } + `, uid)) + require.NoError(t, err) + require.Contains(t, string(queryResp.GetJson()), "Alpha Secret", "data should still exist") + t.Log("Test passed: delete correctly blocked") +} + +// TestProgramAuthUpdateAllowed tests that authorized users can update their own data. +func TestProgramAuthUpdateAllowed(t *testing.T) { + t.Log("=== TestProgramAuthUpdateAllowed: Users can update data they have access to ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Original Name" .`), + }) + require.NoError(t, err) + uid := resp.Uids["p1"] + + // Update with same ALPHA program - should succeed + t.Log("Updating with ALPHA credentials...") + _, err = dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(fmt.Sprintf(`<%s> "Updated Name" .`, uid)), + }) + require.NoError(t, err, "ALPHA user should be able to update ALPHA data") + + // Verify update + queryResp, err := dg.NewTxn().Query(ctxAlpha, fmt.Sprintf(` + { + node(func: uid(%s)) { + name + } + } + `, uid)) + require.NoError(t, err) + require.Contains(t, string(queryResp.GetJson()), "Updated Name") + t.Log("Test passed: authorized update succeeded") +} + +// TestProgramAuthNoAuthCannotModifyProtected tests that users without auth cannot modify protected data. +func TestProgramAuthNoAuthCannotModifyProtected(t *testing.T) { + t.Log("=== TestProgramAuthNoAuthCannotModifyProtected: No-auth users cannot modify protected data ===") + setupProgramAuthTest(t) + dg := waitForCluster(t) + + // Insert data with ALPHA program + ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{"ALPHA"}, + }) + resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(`_:p1 "Protected Data" .`), + }) + require.NoError(t, err) + uid := resp.Uids["p1"] + + // Try to update with empty auth context (auth active but no programs) + ctxEmpty := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{ + Programs: []string{}, + }) + t.Log("Attempting to update protected data with empty auth...") + _, err = dg.NewTxn().Mutate(ctxEmpty, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(fmt.Sprintf(`<%s> "Attempted Overwrite" .`, uid)), + }) + require.Error(t, err, "user with no programs should not be able to modify protected data") + t.Log("Test passed: empty auth cannot modify protected data") +} + // TestProgramAuthComparisonOps tests program auth with comparison operators (lt, gt, le, ge). func TestProgramAuthComparisonOps(t *testing.T) { t.Log("=== TestProgramAuthComparisonOps: comparison operators should respect program auth ===") From 3fca11513a0b1fb25fdafdd4efef6ea768c7e00b Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Wed, 4 Feb 2026 09:17:51 -0500 Subject: [PATCH 08/10] chore: add .osgrep to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8dace988b71..caa22f8e5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ x/log_test/*.enc ## .claude/ CLAUDE.md +.osgrep From 988bb6ed974234268d345072194e173cfe0f2d30 Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Wed, 4 Feb 2026 09:17:58 -0500 Subject: [PATCH 09/10] refactor(sharding): introduce IsLabeled() helpers for protobuf types Add IsLabeled() methods to Tablet, Member, and SchemaUpdate protobuf types, replacing raw `Label != ""` checks with nil-safe, semantic helpers. This centralizes the label-check logic and makes the intent clearer at each call site. --- dgraph/cmd/zero/raft.go | 2 +- dgraph/cmd/zero/tablet.go | 2 +- dgraph/cmd/zero/zero.go | 8 ++++---- protos/pb/labeled.go | 25 +++++++++++++++++++++++++ worker/mutation.go | 2 +- 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 protos/pb/labeled.go diff --git a/dgraph/cmd/zero/raft.go b/dgraph/cmd/zero/raft.go index 1c82c60ea3a..7cc8debb8c1 100644 --- a/dgraph/cmd/zero/raft.go +++ b/dgraph/cmd/zero/raft.go @@ -335,7 +335,7 @@ func (n *node) handleTablet(tablet *pb.Tablet) error { if tablet.Force { originalGroup := state.Groups[prev.GroupId] delete(originalGroup.Tablets, tablet.Predicate) - } else if tablet.Label != "" && prev.Label != tablet.Label { + } else if tablet.IsLabeled() && prev.Label != tablet.Label { // Allow re-routing when labels differ. This happens when a schema with @label // is applied after the predicate was created without a label. glog.Infof("Tablet for attr: [%s] re-routing from group %d to %d due to label change (%q -> %q)", diff --git a/dgraph/cmd/zero/tablet.go b/dgraph/cmd/zero/tablet.go index 5260d458b87..3cb9582c1d8 100644 --- a/dgraph/cmd/zero/tablet.go +++ b/dgraph/cmd/zero/tablet.go @@ -277,7 +277,7 @@ func (s *Server) chooseTablet() (predicate string, srcGroup uint32, dstGroup uin // Reserved predicates should always be in group 1 so do not re-balance them. continue } - if tab.Label != "" { + if tab.IsLabeled() { // labeled predicates are pinned and should not be re-balanced either continue } diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index c28b5f47846..bbd56346154 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -456,7 +456,7 @@ func (s *Server) Inform(ctx context.Context, req *pb.TabletRequest) (*pb.TabletR // This will also make it easier to restore the reserved predicates after // a DropAll operation. t.GroupId = 1 - case t.Label != "": + case t.IsLabeled(): // Labeled predicate: route to matching labeled group gid, err := s.labelGroup(t.Label) if err != nil { @@ -713,7 +713,7 @@ func (s *Server) ShouldServe( // If the existing tablet has a different label than requested, we need to re-route. // This can happen when a schema is applied with @label after the predicate was // created without a label (e.g., during DropAll). - if tablet.Label != "" && tab.Label != tablet.Label { + if tablet.IsLabeled() && tab.Label != tablet.Label { glog.Infof("ShouldServe: tablet %s has label %q but request has label %q, re-routing", tablet.Predicate, tab.Label, tablet.Label) // Fall through to re-assign the tablet with the new label @@ -746,7 +746,7 @@ func (s *Server) ShouldServe( // This will also make it easier to restore the reserved predicates after // a DropAll operation. tablet.GroupId = 1 - case tablet.Label != "": + case tablet.IsLabeled(): // Labeled predicate: route to matching labeled group gid, err := s.labelGroup(tablet.Label) if err != nil { @@ -931,7 +931,7 @@ func (s *Server) groupLabel(gid uint32) string { return "" } for _, member := range group.Members { - if member.Label != "" { + if member.IsLabeled() { return member.Label } } diff --git a/protos/pb/labeled.go b/protos/pb/labeled.go new file mode 100644 index 00000000000..b59236e4172 --- /dev/null +++ b/protos/pb/labeled.go @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package pb + +// IsLabeled returns true if this tablet has a label assigned via the @label +// schema directive. Labeled tablets are pinned to specific alpha groups and +// receive special routing, rebalancing, and authorization treatment. +func (t *Tablet) IsLabeled() bool { + return t != nil && t.Label != "" +} + +// IsLabeled returns true if this member was started with a --label flag. +// Labeled members serve only predicates whose @label matches their label. +func (m *Member) IsLabeled() bool { + return m != nil && m.Label != "" +} + +// IsLabeled returns true if this schema update carries a @label directive. +// Labeled predicates are routed to the alpha group whose label matches. +func (s *SchemaUpdate) IsLabeled() bool { + return s != nil && s.Label != "" +} diff --git a/worker/mutation.go b/worker/mutation.go index 5f61f2bff07..83137fafbe2 100644 --- a/worker/mutation.go +++ b/worker/mutation.go @@ -224,7 +224,7 @@ func runSchemaMutation(ctx context.Context, updates []*pb.SchemaUpdate, startTs // For labeled predicates, the tablet is intentionally served by a different group. // We still need to record the schema metadata so queries know the predicate type, // but we skip all index operations since we don't store the data. - if su.Label != "" { + if su.IsLabeled() { glog.V(2).Infof("Recording schema metadata for labeled predicate %s (label: %s), served by group %d", su.Predicate, su.Label, tablet.GetGroupId()) if err := checkSchema(su); err != nil { From ff906a162bb13a2d196fb9cc4ddc3da8a5f72b6b Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Wed, 4 Feb 2026 10:39:36 -0500 Subject: [PATCH 10/10] docs: add entity-level sub-tablet routing design Design document for extending label-based predicate routing to support entity-level routing via dgraph.label. Key decisions: - Sub-tablet keys: predicate@label (backward compat for unlabeled) - Entity label stored on group 1 as reserved predicate - Two-phase mutation routing (extract labels, then route) - Query fan-out to all authorized sub-tablets - Synchronous reclassification following predicate-move pattern - Entity label > predicate @label > unlabeled priority --- ...-entity-level-sub-tablet-routing-design.md | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 docs/plans/2026-02-04-entity-level-sub-tablet-routing-design.md diff --git a/docs/plans/2026-02-04-entity-level-sub-tablet-routing-design.md b/docs/plans/2026-02-04-entity-level-sub-tablet-routing-design.md new file mode 100644 index 00000000000..11c19ad65c4 --- /dev/null +++ b/docs/plans/2026-02-04-entity-level-sub-tablet-routing-design.md @@ -0,0 +1,337 @@ +# Entity-Level Sub-Tablet Routing + +**Date:** 2026-02-04 **Status:** Draft / Design **Branch:** sharding-poc **PR:** #9574 + +--- + +## Problem Statement + +The current predicate-level `@label` routing pins an _entire predicate_ to a specific alpha group. +All UIDs for that predicate live on the same group. This is useful for field-level classification +("this field is always secret") but does not support entity-level classification ("this document is +secret"). + +Entity-level routing means that when a UID has `dgraph.label = "secret"`, **all predicates for that +UID** are stored on the group assigned the "secret" label. Different UIDs for the same predicate can +live on different groups depending on their entity label. + +### Example + +```rdf +_:doc1 "Document" . +_:doc1 "secret" . +_:doc1 "Secret.pdf" . +_:doc1 "Classified content" . + +_:doc2 "Document" . +_:doc2 "top_secret" . +_:doc2 "Top Secret.pdf" . +_:doc2 "Highly classified content" . + +_:doc3 "Document" . +_:doc3 "Boring.pdf" . +_:doc3 "Unclassified memo" . +``` + +Expected routing: + +| Entity | Label | `Document.name` stored on | `Document.text` stored on | +| ------ | ---------- | ------------------------- | ------------------------- | +| doc1 | secret | group 2 (secret) | group 2 (secret) | +| doc2 | top_secret | group 3 (top_secret) | group 3 (top_secret) | +| doc3 | (none) | group 1 (unlabeled) | group 1 (unlabeled) | + +--- + +## Core Constraint + +Dgraph's sharding unit is the **predicate tablet**. Zero's tablet map is `predicate -> group`. A +predicate can only be served by one group. Entity-level routing requires the same predicate to be +served by multiple groups simultaneously. + +## Chosen Approach: Sub-Tablet Routing + +Extend the tablet system so a single predicate can have multiple **sub-tablets**, each keyed by +`(predicate, label)` and assigned to a different group. No predicate renaming. The routing layer +becomes label-aware. + +--- + +## Design + +### 1. Entity Label Registry (`dgraph.label`) + +`dgraph.label` is a **reserved predicate on group 1**, like `dgraph.type` and ACL predicates. It +maps `UID -> label string`. + +**Query path does NOT need per-UID label lookups.** The query planner fans out to all authorized +sub-tablets. Each group returns only UIDs it stores. Label filtering is implicit in data +distribution. + +**Mutation path needs the lookup.** Two cases: + +- **New entity:** Extract label from the mutation batch itself (scan for `dgraph.label` edges before + routing other edges). +- **Existing entity:** Look up from local cache. Cache miss reads from group 1. + +**Caching:** Each alpha maintains a local `UID -> label` cache, populated on reads and mutations. +Invalidated when `dgraph.label` changes (triggers reclassification). + +### 2. Composite Tablet Key + +The tablet map key changes from `predicate` to `predicate@label` for labeled sub-tablets. Unlabeled +sub-tablets keep the bare predicate name for backward compatibility. + +```go +func tabletKey(predicate, label string) string { + if label == "" { + return predicate // "Document.name" + } + return predicate + "@" + label // "Document.name@secret" +} +``` + +The `@` character is not valid in Dgraph predicate names (allowed chars: `a-zA-Z0-9_.~`), so +collisions are impossible. + +**Tablet map examples:** + +``` +Group 1 tablets: + "Document.name" → unlabeled sub-tablet + "dgraph.label" → reserved, entity label storage + "dgraph.type" → reserved + +Group 2 tablets: + "Document.name@secret" → secret sub-tablet + "Document.text@secret" → secret sub-tablet + +Group 3 tablets: + "Document.name@top_secret" → top_secret sub-tablet + "Document.text@top_secret" → top_secret sub-tablet +``` + +**Key property:** Existing code that accesses `group.Tablets["Document.name"]` still works unchanged +— it matches the unlabeled sub-tablet. Only new label-aware code parses the `@` separator. + +### 3. Zero State Machine Changes + +**Three lookup functions replace one:** + +| Function | Purpose | Used By | +| ------------------------------- | ------------------------------------------------------ | ------------------------- | +| `ServingSubTablet(pred, label)` | Find the ONE group serving this (pred, label) pair | Mutations, `handleTablet` | +| `ServingTablets(pred)` | Find ALL sub-tablets for a predicate across groups | Query fan-out | +| `ServingTablet(pred)` | **Backward compat** — returns the unlabeled sub-tablet | Existing code | + +**`handleTablet` change:** The duplicate-detection check changes from "is anyone serving this +predicate?" to "is anyone serving this (predicate, label) pair?". Multiple groups can serve the same +predicate as long as they have different labels. + +**Rebalancer:** Sub-tablets with non-empty labels are pinned. Only unlabeled sub-tablets participate +in rebalancing. + +### 4. Mutation Routing + +`populateMutationMap` changes from predicate-based to entity-label-based routing. + +**Two-phase approach:** + +``` +PHASE 1: Build entity -> label map from this mutation batch. + Scan for dgraph.label edges (handles new entities). + +PHASE 2: Route each edge using the entity's label. + - dgraph.label edges always route to group 1 (reserved) + - All other edges route to the entity's label group +``` + +**Label resolution priority:** + +``` +1. Entity label (dgraph.label) → highest priority +2. Predicate label (@label schema) → fallback default +3. Neither → normal unlabeled routing +``` + +This means predicate-level `@label` acts as a default for predicates where entities don't have their +own labels. Entity-level `dgraph.label` is an override. + +**`resolveLabel` function:** + +```go +func resolveLabel(uid uint64, predicate string, batchLabels map[uint64]string) string { + // 1. Entity label takes priority + if label := resolveEntityLabel(uid, batchLabels); label != "" { + return label + } + // 2. Fall back to predicate-level @label + if label, ok := schema.State().GetLabel(ctx, predicate); ok { + return label + } + // 3. Unlabeled + return "" +} +``` + +**`resolveEntityLabel` function:** + +```go +func resolveEntityLabel(uid uint64, batchLabels map[uint64]string) string { + // 1. Check mutation batch (new entity) + if label, ok := batchLabels[uid]; ok { + return label + } + // 2. Check local cache + if label, ok := entityLabelCache.Get(uid); ok { + return label + } + // 3. Cache miss — read from group 1 + label, _ := readEntityLabel(uid) + entityLabelCache.Set(uid, label) + return label +} +``` + +**Mixed-label mutations work naturally.** A single mutation batch containing edges for entities with +different labels produces multiple group-specific mutation batches via `populateMutationMap`. + +### 5. Query Fan-Out + +`ProcessTaskOverNetwork` changes from single-group dispatch to multi-group scatter-gather. + +**Fast path:** When a predicate has only one sub-tablet (common case for unlabeled predicates), +routing is identical to today — zero overhead. + +**Fan-out path:** When multiple sub-tablets exist: + +1. Look up all sub-tablets for the predicate. +2. Filter by auth context (only query labels the user can access). +3. Send the query (including full UID list) to each authorized sub-tablet in parallel. +4. Merge results. + +**UID list handling:** Send the full UID list to all groups. Each group ignores UIDs it doesn't have +postings for. This avoids per-UID label lookups on the query path. Slight network overhead but much +simpler. + +**Functions that need fan-out:** + +| Function | Location | +| ------------------------------------ | --------------------- | +| `ProcessTaskOverNetwork` | `worker/task.go` | +| `processSort` | `worker/sort.go` | +| Internal callers in `query/query.go` | Benefit automatically | + +**Index-backed functions** (`handleHasFunction`, `handleRegexFunction`, etc.) need no changes — by +the time they execute, the query is already scoped to one group's data. + +### 6. Reclassification (Entity Label Changes) + +When an entity's label changes, all its postings must migrate from the old group to the new group. +This follows the existing predicate-move pattern but scoped to a single entity. + +**Synchronous, blocking migration** (consistent with how predicate moves work today). + +**Sequence:** + +``` +1. DETECT: Old label != new label for the entity +2. BLOCK: Block mutations for this entity (per-entity block) +3. ENUMERATE: Query source group for all predicates where entity has postings + (iterate group's tablet list, check each for the target UID) +4. MIGRATE: For each predicate: + a. Read postings for this UID from source group + b. Write postings to destination group + c. Delete from source group +5. UPDATE: Write new dgraph.label on group 1 +6. INVALIDATE: Clear entity label caches across alphas +7. UNBLOCK: Resume mutations for this entity +``` + +**Data volume is small.** An entity typically has data across dozens of predicates, but each +predicate has only one posting for the UID. Migration should complete in milliseconds to seconds. + +**Fence timestamp pattern:** Same as predicate moves — lease a timestamp from Zero before migration. +Queries with `readTs` before the fence see the old location; queries after see the new location. + +### 7. Cross-Label Edges + +Cross-label edges work naturally with no special handling. + +```rdf +_:doc1 "secret" . +_:doc1 _:person1 . # person1 is unlabeled +``` + +The edge posting `(Document.author, doc1) -> person1` is stored on group 2 (where doc1's data +lives). The target UID (person1) lives on group 1. Dgraph resolves cross-group UID references at +query time during graph traversal. This is existing behavior. + +### 8. Edge Cases + +**DropAll:** + +- Deletes all data, tablets, and sub-tablets. +- Entity label cache is invalidated. +- Sub-tablets are recreated on re-schema + re-mutation. + +**Backup / Restore:** + +- Each group backs up its own sub-tablet data. +- Restore group 1 first (includes `dgraph.label` mappings). +- Sub-tablets are recreated via `ForceTablet` during restore. +- Entity label cache rebuilds naturally. + +**Live Loader:** + +- Uses `populateMutationMap` — benefits from two-phase routing automatically. + +**Bulk Loader:** + +- Needs similar two-phase logic in its map phase: scan for `dgraph.label` edges, then route by + entity label. + +--- + +## Coexistence with Predicate-Level @label + +Entity-level routing coexists with the existing predicate-level `@label` directive. Both produce the +same sub-tablet key format (`predicate@label`). + +| Aspect | Predicate-level `@label` | Entity-level `dgraph.label` | +| -------------- | ----------------------------- | ---------------------------- | +| Label source | Schema definition | Entity data | +| Routing lookup | `schema.State().GetLabel()` | `resolveEntityLabel()` | +| Granularity | Every UID for that predicate | Every predicate for that UID | +| Use case | "This field is always secret" | "This document is secret" | + +**Conflict resolution:** Entity label wins. If a predicate has `@label(secret)` and an entity has +`dgraph.label = "top_secret"`, the entity's label takes precedence. + +--- + +## Files Affected (Estimated) + +| Area | Files | Scope | +| ------------------ | ------------------------------------------------- | --------------------------------------------------------------------------------- | +| Proto | `protos/pb.proto`, `protos/pb/labeled.go` | Add `tabletKey()` helper | +| Zero state machine | `dgraph/cmd/zero/zero.go`, `raft.go`, `tablet.go` | `ServingSubTablet`, `ServingTablets`, composite key in `handleTablet`, rebalancer | +| Worker routing | `worker/groups.go`, `mutation.go`, `proposal.go` | `populateMutationMap` two-phase, `BelongsTo` entity-label-aware | +| Query fan-out | `worker/task.go`, `worker/sort.go` | `ProcessTaskOverNetwork` scatter-gather, `mergeResults` | +| Entity label cache | `worker/groups.go` (new) | `entityLabelCache`, `resolveEntityLabel` | +| Reclassification | `worker/` (new file) | `reclassifyEntity`, per-entity blocking, migration | +| Schema interaction | `worker/mutation.go` | `resolveLabel` priority: entity > predicate > none | +| Online restore | `worker/online_restore.go` | Pass entity labels during `ForceTablet` | +| Tests | `systest/label/` | New entity-level routing and reclassification tests | + +--- + +## Open Questions + +1. **Entity label cache eviction policy.** LRU with max size? TTL? Bounded by namespace? +2. **Bulk loader support.** How deep should entity-label awareness go in the bulk loader's + map/reduce phases? +3. **Metrics / observability.** What new metrics are needed for sub-tablet fan-out latency, + reclassification duration, cache hit rates? +4. **`/moveTablet` API.** Should it accept a label parameter to move a specific sub-tablet? Or only + operate on unlabeled tablets?