github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/discovery/endorsement/endorsement.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package endorsement 8 9 import ( 10 "fmt" 11 12 "github.com/hyperledger/fabric-protos-go/discovery" 13 "github.com/hyperledger/fabric-protos-go/msp" 14 "github.com/osdi23p228/fabric/common/chaincode" 15 "github.com/osdi23p228/fabric/common/flogging" 16 "github.com/osdi23p228/fabric/common/graph" 17 "github.com/osdi23p228/fabric/common/policies" 18 "github.com/osdi23p228/fabric/common/policies/inquire" 19 "github.com/osdi23p228/fabric/gossip/api" 20 "github.com/osdi23p228/fabric/gossip/common" 21 . "github.com/osdi23p228/fabric/gossip/discovery" 22 "github.com/pkg/errors" 23 ) 24 25 var ( 26 logger = flogging.MustGetLogger("discovery.endorsement") 27 ) 28 29 type principalEvaluator interface { 30 // SatisfiesPrincipal returns whether a given peer identity satisfies a certain principal 31 // on a given channel 32 SatisfiesPrincipal(channel string, identity []byte, principal *msp.MSPPrincipal) error 33 } 34 35 type chaincodeMetadataFetcher interface { 36 // ChaincodeMetadata returns the metadata of the chaincode as appears in the ledger, 37 // or nil if the channel doesn't exist, or the chaincode isn't found in the ledger 38 Metadata(channel string, cc string, collections ...string) *chaincode.Metadata 39 } 40 41 type policyFetcher interface { 42 // PoliciesByChaincode returns the chaincode policy or existing collection level policies that can be 43 // inquired for which identities satisfy them 44 PoliciesByChaincode(channel string, cc string, collections ...string) []policies.InquireablePolicy 45 } 46 47 type gossipSupport interface { 48 // IdentityInfo returns identity information about peers 49 IdentityInfo() api.PeerIdentitySet 50 51 // PeersOfChannel returns the NetworkMembers considered alive 52 // and also subscribed to the channel given 53 PeersOfChannel(common.ChannelID) Members 54 55 // Peers returns the NetworkMembers considered alive 56 Peers() Members 57 } 58 59 type membersChaincodeMapping struct { 60 members Members 61 chaincodeMapping map[string]NetworkMember 62 } 63 64 type endorsementAnalyzer struct { 65 gossipSupport 66 principalEvaluator 67 policyFetcher 68 chaincodeMetadataFetcher 69 } 70 71 // NewEndorsementAnalyzer constructs an NewEndorsementAnalyzer out of the given support 72 func NewEndorsementAnalyzer(gs gossipSupport, pf policyFetcher, pe principalEvaluator, mf chaincodeMetadataFetcher) *endorsementAnalyzer { 73 return &endorsementAnalyzer{ 74 gossipSupport: gs, 75 policyFetcher: pf, 76 principalEvaluator: pe, 77 chaincodeMetadataFetcher: mf, 78 } 79 } 80 81 type peerPrincipalEvaluator func(member NetworkMember, principal *msp.MSPPrincipal) bool 82 83 // PeersForEndorsement returns an EndorsementDescriptor for a given set of peers, channel, and chaincode 84 func (ea *endorsementAnalyzer) PeersForEndorsement(channelID common.ChannelID, interest *discovery.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) { 85 membersAndCC, err := ea.peersByCriteria(channelID, interest, false) 86 if err != nil { 87 return nil, errors.WithStack(err) 88 } 89 channelMembersById := membersAndCC.members.ByID() 90 // Choose only the alive messages of those that have joined the channel 91 aliveMembership := ea.Peers().Intersect(membersAndCC.members) 92 membersById := aliveMembership.ByID() 93 // Compute a mapping between the PKI-IDs of members to their identities 94 identitiesOfMembers := computeIdentitiesOfMembers(ea.IdentityInfo(), membersById) 95 principalsSets, err := ea.computePrincipalSets(channelID, interest) 96 if err != nil { 97 logger.Warningf("Principal set computation failed: %v", err) 98 return nil, errors.WithStack(err) 99 } 100 101 return ea.computeEndorsementResponse(&context{ 102 chaincode: interest.Chaincodes[0].Name, 103 channel: string(channelID), 104 principalsSets: principalsSets, 105 channelMembersById: channelMembersById, 106 aliveMembership: aliveMembership, 107 identitiesOfMembers: identitiesOfMembers, 108 chaincodeMapping: membersAndCC.chaincodeMapping, 109 }) 110 } 111 112 func (ea *endorsementAnalyzer) PeersAuthorizedByCriteria(channelID common.ChannelID, interest *discovery.ChaincodeInterest) (Members, error) { 113 res, err := ea.peersByCriteria(channelID, interest, true) 114 return res.members, err 115 } 116 117 func (ea *endorsementAnalyzer) peersByCriteria(channelID common.ChannelID, interest *discovery.ChaincodeInterest, excludePeersWithoutChaincode bool) (membersChaincodeMapping, error) { 118 peersOfChannel := ea.PeersOfChannel(channelID) 119 if interest == nil || len(interest.Chaincodes) == 0 { 120 return membersChaincodeMapping{members: peersOfChannel}, nil 121 } 122 identities := ea.IdentityInfo() 123 identitiesByID := identities.ByID() 124 metadataAndCollectionFilters, err := loadMetadataAndFilters(metadataAndFilterContext{ 125 identityInfoByID: identitiesByID, 126 interest: interest, 127 chainID: channelID, 128 evaluator: ea, 129 fetch: ea, 130 }) 131 if err != nil { 132 return membersChaincodeMapping{}, errors.WithStack(err) 133 } 134 metadata := metadataAndCollectionFilters.md 135 // Filter out peers that don't have the chaincode installed on them if required 136 peersWithChaincode := peersOfChannel.Filter(peersWithChaincode(metadata...)) 137 chanMembership := peersOfChannel 138 if excludePeersWithoutChaincode { 139 chanMembership = peersWithChaincode 140 } 141 142 // Filter out peers that aren't authorized by the collection configs of the chaincode invocation chain 143 members := chanMembership.Filter(metadataAndCollectionFilters.isMemberAuthorized) 144 return membersChaincodeMapping{ 145 members: members, 146 chaincodeMapping: peersWithChaincode.ByID(), 147 }, nil 148 } 149 150 type context struct { 151 chaincode string 152 channel string 153 aliveMembership Members 154 principalsSets []policies.PrincipalSet 155 channelMembersById map[string]NetworkMember 156 identitiesOfMembers memberIdentities 157 chaincodeMapping map[string]NetworkMember 158 } 159 160 func (ea *endorsementAnalyzer) computeEndorsementResponse(ctx *context) (*discovery.EndorsementDescriptor, error) { 161 // mapPrincipalsToGroups returns a mapping from principals to their corresponding groups. 162 // groups are just human readable representations that mask the principals behind them 163 principalGroups := mapPrincipalsToGroups(ctx.principalsSets) 164 // principalsToPeersGraph computes a bipartite graph (V1 U V2 , E) 165 // such that V1 is the peers, V2 are the principals, 166 // and each e=(peer,principal) is in E if the peer satisfies the principal 167 satGraph := principalsToPeersGraph(principalAndPeerData{ 168 members: ctx.aliveMembership, 169 pGrps: principalGroups, 170 }, ea.satisfiesPrincipal(ctx.channel, ctx.identitiesOfMembers)) 171 172 layouts := computeLayouts(ctx.principalsSets, principalGroups, satGraph) 173 if len(layouts) == 0 { 174 return nil, errors.New("no peer combination can satisfy the endorsement policy") 175 } 176 177 criteria := &peerMembershipCriteria{ 178 possibleLayouts: layouts, 179 satGraph: satGraph, 180 chanMemberById: ctx.channelMembersById, 181 idOfMembers: ctx.identitiesOfMembers, 182 chaincodeMapping: ctx.chaincodeMapping, 183 } 184 185 groupToEndorserListMapping := endorsersByGroup(criteria) 186 layouts = filterOutUnsatisfiedLayouts(groupToEndorserListMapping, layouts) 187 188 if len(layouts) == 0 { 189 return nil, errors.New("required chaincodes are not installed on sufficient peers") 190 } 191 192 return &discovery.EndorsementDescriptor{ 193 Chaincode: ctx.chaincode, 194 Layouts: layouts, 195 EndorsersByGroups: groupToEndorserListMapping, 196 }, nil 197 } 198 199 func filterOutUnsatisfiedLayouts(endorsersByGroup map[string]*discovery.Peers, layouts []*discovery.Layout) []*discovery.Layout { 200 // Iterate once again over all layouts and ensure every layout has enough peers in the EndorsersByGroups 201 // as required by the quantity in the layout. 202 filteredLayouts := make([]*discovery.Layout, 0, len(layouts)) 203 for _, layout := range layouts { 204 var layoutInvalid bool 205 for group, quantity := range layout.QuantitiesByGroup { 206 peerList := endorsersByGroup[group] 207 if peerList == nil || len(peerList.Peers) < int(quantity) { 208 layoutInvalid = true 209 } 210 } 211 if layoutInvalid { 212 continue 213 } 214 filteredLayouts = append(filteredLayouts, layout) 215 } 216 return filteredLayouts 217 } 218 219 func (ea *endorsementAnalyzer) computePrincipalSets(channelID common.ChannelID, interest *discovery.ChaincodeInterest) (policies.PrincipalSets, error) { 220 sessionLogger := logger.With("channel", string(channelID)) 221 var inquireablePolicies []policies.InquireablePolicy 222 for _, chaincode := range interest.Chaincodes { 223 policies := ea.PoliciesByChaincode(string(channelID), chaincode.Name, chaincode.CollectionNames...) 224 if len(policies) == 0 { 225 sessionLogger.Debug("Policy for chaincode '", chaincode, "'doesn't exist") 226 return nil, errors.New("policy not found") 227 } 228 inquireablePolicies = append(inquireablePolicies, policies...) 229 } 230 231 var cpss []inquire.ComparablePrincipalSets 232 233 for _, policy := range inquireablePolicies { 234 var cmpsets inquire.ComparablePrincipalSets 235 for _, ps := range policy.SatisfiedBy() { 236 cps := inquire.NewComparablePrincipalSet(ps) 237 if cps == nil { 238 return nil, errors.New("failed creating a comparable principal set") 239 } 240 cmpsets = append(cmpsets, cps) 241 } 242 if len(cmpsets) == 0 { 243 return nil, errors.New("endorsement policy cannot be satisfied") 244 } 245 cpss = append(cpss, cmpsets) 246 } 247 248 cps, err := mergePrincipalSets(cpss) 249 if err != nil { 250 return nil, errors.WithStack(err) 251 } 252 253 return cps.ToPrincipalSets(), nil 254 } 255 256 type metadataAndFilterContext struct { 257 chainID common.ChannelID 258 interest *discovery.ChaincodeInterest 259 fetch chaincodeMetadataFetcher 260 identityInfoByID map[string]api.PeerIdentityInfo 261 evaluator principalEvaluator 262 } 263 264 // metadataAndColFilter holds metadata and member filters 265 type metadataAndColFilter struct { 266 md []*chaincode.Metadata 267 isMemberAuthorized memberFilter 268 } 269 270 func loadMetadataAndFilters(ctx metadataAndFilterContext) (*metadataAndColFilter, error) { 271 sessionLogger := logger.With("channel", string(ctx.chainID)) 272 var metadata []*chaincode.Metadata 273 var filters []identityFilter 274 275 for _, chaincode := range ctx.interest.Chaincodes { 276 ccMD := ctx.fetch.Metadata(string(ctx.chainID), chaincode.Name, chaincode.CollectionNames...) 277 if ccMD == nil { 278 return nil, errors.Errorf("No metadata was found for chaincode %s in channel %s", chaincode.Name, string(ctx.chainID)) 279 } 280 metadata = append(metadata, ccMD) 281 if len(chaincode.CollectionNames) == 0 { 282 sessionLogger.Debugf("No collections for %s, skipping", chaincode.Name) 283 continue 284 } 285 if chaincode.NoPrivateReads { 286 sessionLogger.Debugf("No private reads, skipping") 287 continue 288 } 289 principalSetByCollections, err := principalsFromCollectionConfig(ccMD.CollectionsConfig) 290 if err != nil { 291 sessionLogger.Warningf("Failed initializing collection filter for chaincode %s: %v", chaincode.Name, err) 292 return nil, errors.WithStack(err) 293 } 294 filter, err := principalSetByCollections.toIdentityFilter(string(ctx.chainID), ctx.evaluator, chaincode) 295 if err != nil { 296 sessionLogger.Warningf("Failed computing collection principal sets for chaincode %s due to %v", chaincode.Name, err) 297 return nil, errors.WithStack(err) 298 } 299 filters = append(filters, filter) 300 } 301 302 return computeFiltersWithMetadata(filters, metadata, ctx.identityInfoByID), nil 303 } 304 305 func computeFiltersWithMetadata(filters identityFilters, metadata []*chaincode.Metadata, identityInfoByID map[string]api.PeerIdentityInfo) *metadataAndColFilter { 306 if len(filters) == 0 { 307 return &metadataAndColFilter{ 308 md: metadata, 309 isMemberAuthorized: noopMemberFilter, 310 } 311 } 312 filter := filters.combine().toMemberFilter(identityInfoByID) 313 return &metadataAndColFilter{ 314 isMemberAuthorized: filter, 315 md: metadata, 316 } 317 } 318 319 // identityFilter accepts or rejects peer identities 320 type identityFilter func(api.PeerIdentityType) bool 321 322 // identityFilters aggregates multiple identityFilters 323 type identityFilters []identityFilter 324 325 // memberFilter accepts or rejects NetworkMembers 326 type memberFilter func(member NetworkMember) bool 327 328 // noopMemberFilter accepts every NetworkMember 329 func noopMemberFilter(_ NetworkMember) bool { 330 return true 331 } 332 333 // combine combines all identityFilters into a single identityFilter which only accepts identities 334 // which all the original filters accept 335 func (filters identityFilters) combine() identityFilter { 336 return func(identity api.PeerIdentityType) bool { 337 for _, f := range filters { 338 if !f(identity) { 339 return false 340 } 341 } 342 return true 343 } 344 } 345 346 // toMemberFilter converts this identityFilter to a memberFilter based on the given mapping 347 // from PKI-ID as strings, to PeerIdentityInfo which holds the peer identities 348 func (idf identityFilter) toMemberFilter(identityInfoByID map[string]api.PeerIdentityInfo) memberFilter { 349 return func(member NetworkMember) bool { 350 identity, exists := identityInfoByID[string(member.PKIid)] 351 if !exists { 352 return false 353 } 354 return idf(identity.Identity) 355 } 356 } 357 358 func (ea *endorsementAnalyzer) satisfiesPrincipal(channel string, identitiesOfMembers memberIdentities) peerPrincipalEvaluator { 359 return func(member NetworkMember, principal *msp.MSPPrincipal) bool { 360 err := ea.SatisfiesPrincipal(channel, identitiesOfMembers.identityByPKIID(member.PKIid), principal) 361 if err == nil { 362 // TODO: log the principals in a human readable form 363 logger.Debug(member, "satisfies principal", principal) 364 return true 365 } 366 logger.Debug(member, "doesn't satisfy principal", principal, ":", err) 367 return false 368 } 369 } 370 371 type peerMembershipCriteria struct { 372 satGraph *principalPeerGraph 373 idOfMembers memberIdentities 374 chanMemberById map[string]NetworkMember 375 possibleLayouts layouts 376 chaincodeMapping map[string]NetworkMember 377 } 378 379 // endorsersByGroup computes a map from groups to peers. 380 // Each group included, is found in some layout, which means 381 // that there is some principal combination that includes the corresponding 382 // group. 383 // This means that if a group isn't included in the result, there is no 384 // principal combination (that includes the principal corresponding to the group), 385 // such that there are enough peers to satisfy the principal combination. 386 func endorsersByGroup(criteria *peerMembershipCriteria) map[string]*discovery.Peers { 387 satGraph := criteria.satGraph 388 idOfMembers := criteria.idOfMembers 389 chanMemberById := criteria.chanMemberById 390 includedGroups := criteria.possibleLayouts.groupsSet() 391 392 res := make(map[string]*discovery.Peers) 393 // Map endorsers to their corresponding groups. 394 // Iterate the principals, and put the peers into each group that corresponds with a principal vertex 395 for grp, principalVertex := range satGraph.principalVertices { 396 if _, exists := includedGroups[grp]; !exists { 397 // If the current group is not found in any layout, skip the corresponding principal 398 continue 399 } 400 peerList := &discovery.Peers{} 401 for _, peerVertex := range principalVertex.Neighbors() { 402 member := peerVertex.Data.(NetworkMember) 403 // Check if this peer has the chaincode installed 404 stateInfo := chanMemberById[string(member.PKIid)] 405 _, hasChaincodeInstalled := criteria.chaincodeMapping[string(stateInfo.PKIid)] 406 if !hasChaincodeInstalled { 407 continue 408 } 409 peerList.Peers = append(peerList.Peers, &discovery.Peer{ 410 Identity: idOfMembers.identityByPKIID(member.PKIid), 411 StateInfo: stateInfo.Envelope, 412 MembershipInfo: member.Envelope, 413 }) 414 } 415 416 if len(peerList.Peers) > 0 { 417 res[grp] = peerList 418 } 419 } 420 return res 421 } 422 423 // computeLayouts computes all possible principal combinations 424 // that can be used to satisfy the endorsement policy, given a graph 425 // of available peers that maps each peer to a principal it satisfies. 426 // Each such a combination is called a layout, because it maps 427 // a group (alias for a principal) to a threshold of peers that need to endorse, 428 // and that satisfy the corresponding principal 429 func computeLayouts(principalsSets []policies.PrincipalSet, principalGroups principalGroupMapper, satGraph *principalPeerGraph) []*discovery.Layout { 430 var layouts []*discovery.Layout 431 // principalsSets is a collection of combinations of principals, 432 // such that each combination (given enough peers) satisfies the endorsement policy. 433 for _, principalSet := range principalsSets { 434 layout := &discovery.Layout{ 435 QuantitiesByGroup: make(map[string]uint32), 436 } 437 // Since principalsSet has repetitions, we first 438 // compute a mapping from the principal to repetitions in the set. 439 for principal, plurality := range principalSet.UniqueSet() { 440 key := principalKey{ 441 cls: int32(principal.PrincipalClassification), 442 principal: string(principal.Principal), 443 } 444 // We map the principal to a group, which is an alias for the principal. 445 layout.QuantitiesByGroup[principalGroups.group(key)] = uint32(plurality) 446 } 447 // Check that the layout can be satisfied with the current known peers 448 // This is done by iterating the current layout, and ensuring that 449 // each principal vertex is connected to at least <plurality> peer vertices. 450 if isLayoutSatisfied(layout.QuantitiesByGroup, satGraph) { 451 // If so, then add the layout to the layouts, since we have enough peers to satisfy the 452 // principal combination 453 layouts = append(layouts, layout) 454 } 455 } 456 return layouts 457 } 458 459 func isLayoutSatisfied(layout map[string]uint32, satGraph *principalPeerGraph) bool { 460 for grp, plurality := range layout { 461 // Do we have more than <plurality> peers connected to the principal? 462 if len(satGraph.principalVertices[grp].Neighbors()) < int(plurality) { 463 return false 464 } 465 } 466 return true 467 } 468 469 type principalPeerGraph struct { 470 peerVertices []*graph.Vertex 471 principalVertices map[string]*graph.Vertex 472 } 473 474 type principalAndPeerData struct { 475 members Members 476 pGrps principalGroupMapper 477 } 478 479 func principalsToPeersGraph(data principalAndPeerData, satisfiesPrincipal peerPrincipalEvaluator) *principalPeerGraph { 480 // Create the peer vertices 481 peerVertices := make([]*graph.Vertex, len(data.members)) 482 for i, member := range data.members { 483 peerVertices[i] = graph.NewVertex(string(member.PKIid), member) 484 } 485 486 // Create the principal vertices 487 principalVertices := make(map[string]*graph.Vertex) 488 for pKey, grp := range data.pGrps { 489 principalVertices[grp] = graph.NewVertex(grp, pKey.toPrincipal()) 490 } 491 492 // Connect principals and peers 493 for _, principalVertex := range principalVertices { 494 for _, peerVertex := range peerVertices { 495 // If the current peer satisfies the principal, connect their corresponding vertices with an edge 496 principal := principalVertex.Data.(*msp.MSPPrincipal) 497 member := peerVertex.Data.(NetworkMember) 498 if satisfiesPrincipal(member, principal) { 499 peerVertex.AddNeighbor(principalVertex) 500 } 501 } 502 } 503 return &principalPeerGraph{ 504 peerVertices: peerVertices, 505 principalVertices: principalVertices, 506 } 507 } 508 509 func mapPrincipalsToGroups(principalsSets []policies.PrincipalSet) principalGroupMapper { 510 groupMapper := make(principalGroupMapper) 511 totalPrincipals := make(map[principalKey]struct{}) 512 for _, principalSet := range principalsSets { 513 for _, principal := range principalSet { 514 totalPrincipals[principalKey{ 515 principal: string(principal.Principal), 516 cls: int32(principal.PrincipalClassification), 517 }] = struct{}{} 518 } 519 } 520 for principal := range totalPrincipals { 521 groupMapper.group(principal) 522 } 523 return groupMapper 524 } 525 526 type memberIdentities map[string]api.PeerIdentityType 527 528 func (m memberIdentities) identityByPKIID(id common.PKIidType) api.PeerIdentityType { 529 return m[string(id)] 530 } 531 532 func computeIdentitiesOfMembers(identitySet api.PeerIdentitySet, members map[string]NetworkMember) memberIdentities { 533 identitiesByPKIID := make(map[string]api.PeerIdentityType) 534 identitiesOfMembers := make(map[string]api.PeerIdentityType, len(members)) 535 for _, identity := range identitySet { 536 identitiesByPKIID[string(identity.PKIId)] = identity.Identity 537 } 538 for _, member := range members { 539 if identity, exists := identitiesByPKIID[string(member.PKIid)]; exists { 540 identitiesOfMembers[string(member.PKIid)] = identity 541 } 542 } 543 return identitiesOfMembers 544 } 545 546 // principalGroupMapper maps principals to names of groups 547 type principalGroupMapper map[principalKey]string 548 549 func (mapper principalGroupMapper) group(principal principalKey) string { 550 if grp, exists := mapper[principal]; exists { 551 return grp 552 } 553 grp := fmt.Sprintf("G%d", len(mapper)) 554 mapper[principal] = grp 555 return grp 556 } 557 558 type principalKey struct { 559 cls int32 560 principal string 561 } 562 563 func (pk principalKey) toPrincipal() *msp.MSPPrincipal { 564 return &msp.MSPPrincipal{ 565 PrincipalClassification: msp.MSPPrincipal_Classification(pk.cls), 566 Principal: []byte(pk.principal), 567 } 568 } 569 570 // layouts is an aggregation of several layouts 571 type layouts []*discovery.Layout 572 573 // groupsSet returns a set of groups that the layouts contain 574 func (l layouts) groupsSet() map[string]struct{} { 575 m := make(map[string]struct{}) 576 for _, layout := range l { 577 for grp := range layout.QuantitiesByGroup { 578 m[grp] = struct{}{} 579 } 580 } 581 return m 582 } 583 584 func peersWithChaincode(metadata ...*chaincode.Metadata) func(member NetworkMember) bool { 585 return func(member NetworkMember) bool { 586 if member.Properties == nil { 587 return false 588 } 589 for _, ccMD := range metadata { 590 var found bool 591 for _, cc := range member.Properties.Chaincodes { 592 if cc.Name == ccMD.Name && cc.Version == ccMD.Version { 593 found = true 594 } 595 } 596 if !found { 597 return false 598 } 599 } 600 return true 601 } 602 } 603 604 func mergePrincipalSets(cpss []inquire.ComparablePrincipalSets) (inquire.ComparablePrincipalSets, error) { 605 // Obtain the first ComparablePrincipalSet first 606 var cps inquire.ComparablePrincipalSets 607 cps, cpss, err := popComparablePrincipalSets(cpss) 608 if err != nil { 609 return nil, errors.WithStack(err) 610 } 611 612 for _, cps2 := range cpss { 613 cps = inquire.Merge(cps, cps2) 614 } 615 return cps, nil 616 } 617 618 func popComparablePrincipalSets(sets []inquire.ComparablePrincipalSets) (inquire.ComparablePrincipalSets, []inquire.ComparablePrincipalSets, error) { 619 if len(sets) == 0 { 620 return nil, nil, errors.New("no principal sets remained after filtering") 621 } 622 cps, cpss := sets[0], sets[1:] 623 return cps, cpss, nil 624 }