go_study/fabric-main/discovery/endorsement/endorsement.go

696 lines
25 KiB
Go

/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package endorsement
import (
"fmt"
"github.com/hyperledger/fabric-protos-go/peer"
"github.com/hyperledger/fabric-protos-go/discovery"
"github.com/hyperledger/fabric-protos-go/msp"
"github.com/hyperledger/fabric/common/chaincode"
"github.com/hyperledger/fabric/common/flogging"
"github.com/hyperledger/fabric/common/graph"
"github.com/hyperledger/fabric/common/policies"
"github.com/hyperledger/fabric/common/policies/inquire"
"github.com/hyperledger/fabric/gossip/api"
"github.com/hyperledger/fabric/gossip/common"
gossipdiscovery "github.com/hyperledger/fabric/gossip/discovery"
"github.com/pkg/errors"
)
var logger = flogging.MustGetLogger("discovery.endorsement")
type principalEvaluator interface {
// SatisfiesPrincipal returns whether a given peer identity satisfies a certain principal
// on a given channel
SatisfiesPrincipal(channel string, identity []byte, principal *msp.MSPPrincipal) error
}
type chaincodeMetadataFetcher interface {
// ChaincodeMetadata returns the metadata of the chaincode as appears in the ledger,
// or nil if the channel doesn't exist, or the chaincode isn't found in the ledger
Metadata(channel string, cc string, collections ...string) *chaincode.Metadata
}
type policyFetcher interface {
// PoliciesByChaincode returns the chaincode policy or existing collection level policies that can be
// inquired for which identities satisfy them
PoliciesByChaincode(channel string, cc string, collections ...string) []policies.InquireablePolicy
}
type gossipSupport interface {
// IdentityInfo returns identity information about peers
IdentityInfo() api.PeerIdentitySet
// PeersOfChannel returns the NetworkMembers considered alive
// and also subscribed to the channel given
PeersOfChannel(common.ChannelID) gossipdiscovery.Members
// Peers returns the NetworkMembers considered alive
Peers() gossipdiscovery.Members
}
type membersChaincodeMapping struct {
members gossipdiscovery.Members
chaincodeMapping map[string]gossipdiscovery.NetworkMember
}
type endorsementAnalyzer struct {
gossipSupport
principalEvaluator
policyFetcher
chaincodeMetadataFetcher
}
// NewEndorsementAnalyzer constructs an NewEndorsementAnalyzer out of the given support
func NewEndorsementAnalyzer(gs gossipSupport, pf policyFetcher, pe principalEvaluator, mf chaincodeMetadataFetcher) *endorsementAnalyzer {
return &endorsementAnalyzer{
gossipSupport: gs,
policyFetcher: pf,
principalEvaluator: pe,
chaincodeMetadataFetcher: mf,
}
}
type peerPrincipalEvaluator func(member gossipdiscovery.NetworkMember, principal *msp.MSPPrincipal) bool
// PeersForEndorsement returns an EndorsementDescriptor for a given set of peers, channel, and chaincode
func (ea *endorsementAnalyzer) PeersForEndorsement(channelID common.ChannelID, interest *peer.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) {
membersAndCC, err := ea.peersByCriteria(channelID, interest, false)
if err != nil {
return nil, errors.WithStack(err)
}
channelMembersById := membersAndCC.members.ByID()
// Choose only the alive messages of those that have joined the channel
aliveMembership := ea.Peers().Intersect(membersAndCC.members)
membersById := aliveMembership.ByID()
// Compute a mapping between the PKI-IDs of members to their identities
identitiesOfMembers := computeIdentitiesOfMembers(ea.IdentityInfo(), membersById)
principalsSets, err := ea.computePrincipalSets(channelID, interest)
if err != nil {
logger.Warningf("Principal set computation failed: %v", err)
return nil, errors.WithStack(err)
}
return ea.computeEndorsementResponse(&context{
chaincode: interest.Chaincodes[0].Name,
channel: string(channelID),
principalsSets: principalsSets,
channelMembersById: channelMembersById,
aliveMembership: aliveMembership,
identitiesOfMembers: identitiesOfMembers,
chaincodeMapping: membersAndCC.chaincodeMapping,
})
}
func (ea *endorsementAnalyzer) PeersAuthorizedByCriteria(channelID common.ChannelID, interest *peer.ChaincodeInterest) (gossipdiscovery.Members, error) {
res, err := ea.peersByCriteria(channelID, interest, true)
return res.members, err
}
func (ea *endorsementAnalyzer) peersByCriteria(channelID common.ChannelID, interest *peer.ChaincodeInterest, excludePeersWithoutChaincode bool) (membersChaincodeMapping, error) {
peersOfChannel := ea.PeersOfChannel(channelID)
if interest == nil || len(interest.Chaincodes) == 0 {
return membersChaincodeMapping{members: peersOfChannel}, nil
}
identities := ea.IdentityInfo()
identitiesByID := identities.ByID()
metadataAndCollectionFilters, err := loadMetadataAndFilters(metadataAndFilterContext{
identityInfoByID: identitiesByID,
interest: interest,
chainID: channelID,
evaluator: ea,
fetch: ea,
})
if err != nil {
return membersChaincodeMapping{}, errors.WithStack(err)
}
metadata := metadataAndCollectionFilters.md
// Filter out peers that don't have the chaincode installed on them if required
peersWithChaincode := peersOfChannel.Filter(peersWithChaincode(metadata...))
chanMembership := peersOfChannel
if excludePeersWithoutChaincode {
chanMembership = peersWithChaincode
}
// Filter out peers that aren't authorized by the collection configs of the chaincode invocation chain
members := chanMembership.Filter(metadataAndCollectionFilters.isMemberAuthorized)
return membersChaincodeMapping{
members: members,
chaincodeMapping: peersWithChaincode.ByID(),
}, nil
}
type context struct {
chaincode string
channel string
aliveMembership gossipdiscovery.Members
principalsSets []policies.PrincipalSet
channelMembersById map[string]gossipdiscovery.NetworkMember
identitiesOfMembers memberIdentities
chaincodeMapping map[string]gossipdiscovery.NetworkMember
}
func (ea *endorsementAnalyzer) computeEndorsementResponse(ctx *context) (*discovery.EndorsementDescriptor, error) {
// mapPrincipalsToGroups returns a mapping from principals to their corresponding groups.
// groups are just human readable representations that mask the principals behind them
principalGroups := mapPrincipalsToGroups(ctx.principalsSets)
// principalsToPeersGraph computes a bipartite graph (V1 U V2 , E)
// such that V1 is the peers, V2 are the principals,
// and each e=(peer,principal) is in E if the peer satisfies the principal
satGraph := principalsToPeersGraph(principalAndPeerData{
members: ctx.aliveMembership,
pGrps: principalGroups,
}, ea.satisfiesPrincipal(ctx.channel, ctx.identitiesOfMembers))
layouts := computeLayouts(ctx.principalsSets, principalGroups, satGraph)
if len(layouts) == 0 {
return nil, errors.New("no peer combination can satisfy the endorsement policy")
}
criteria := &peerMembershipCriteria{
possibleLayouts: layouts,
satGraph: satGraph,
chanMemberById: ctx.channelMembersById,
idOfMembers: ctx.identitiesOfMembers,
chaincodeMapping: ctx.chaincodeMapping,
}
groupToEndorserListMapping := endorsersByGroup(criteria)
layouts = filterOutUnsatisfiedLayouts(groupToEndorserListMapping, layouts)
if len(layouts) == 0 {
return nil, errors.New("required chaincodes are not installed on sufficient peers")
}
return &discovery.EndorsementDescriptor{
Chaincode: ctx.chaincode,
Layouts: layouts,
EndorsersByGroups: groupToEndorserListMapping,
}, nil
}
func filterOutUnsatisfiedLayouts(endorsersByGroup map[string]*discovery.Peers, layouts []*discovery.Layout) []*discovery.Layout {
// Iterate once again over all layouts and ensure every layout has enough peers in the EndorsersByGroups
// as required by the quantity in the layout.
filteredLayouts := make([]*discovery.Layout, 0, len(layouts))
for _, layout := range layouts {
var layoutInvalid bool
for group, quantity := range layout.QuantitiesByGroup {
peerList := endorsersByGroup[group]
if peerList == nil || len(peerList.Peers) < int(quantity) {
layoutInvalid = true
}
}
if layoutInvalid {
continue
}
filteredLayouts = append(filteredLayouts, layout)
}
return filteredLayouts
}
func computeStateBasedPrincipalSets(chaincodes []*peer.ChaincodeCall, logger *flogging.FabricLogger) (inquire.ComparablePrincipalSets, error) {
var stateBasedCPS []inquire.ComparablePrincipalSets
for _, chaincode := range chaincodes {
if len(chaincode.KeyPolicies) == 0 {
continue
}
logger.Debugf("Chaincode call to %s is satisfied by %d state based policies of %v",
chaincode.Name, len(chaincode.KeyPolicies), chaincode.KeyPolicies)
for _, stateBasedPolicy := range chaincode.KeyPolicies {
var cmpsets inquire.ComparablePrincipalSets
stateBasedPolicy := inquire.NewInquireableSignaturePolicy(stateBasedPolicy)
for _, ps := range stateBasedPolicy.SatisfiedBy() {
cps := inquire.NewComparablePrincipalSet(ps)
if cps == nil {
return nil, errors.New("failed creating a comparable principal set for state based endorsement")
}
cmpsets = append(cmpsets, cps)
}
if len(cmpsets) == 0 {
return nil, errors.New("state based endorsement policy cannot be satisfied")
}
stateBasedCPS = append(stateBasedCPS, cmpsets)
}
}
if len(stateBasedCPS) > 0 {
stateBasedPrincipalSet, err := mergePrincipalSets(stateBasedCPS)
if err != nil {
return nil, errors.WithStack(err)
}
logger.Debugf("Merging state based policies: %v --> %v", stateBasedCPS, stateBasedPrincipalSet)
return stateBasedPrincipalSet, nil
}
logger.Debugf("No state based policies requested")
return nil, nil
}
func (ea *endorsementAnalyzer) computePrincipalSets(channelID common.ChannelID, interest *peer.ChaincodeInterest) (policies.PrincipalSets, error) {
sessionLogger := logger.With("channel", string(channelID))
var inquireablePoliciesForChaincodeAndCollections []policies.InquireablePolicy
for _, chaincode := range interest.Chaincodes {
policies := ea.PoliciesByChaincode(string(channelID), chaincode.Name, chaincode.CollectionNames...)
if len(policies) == 0 {
sessionLogger.Debug("Policy for chaincode '", chaincode, "'doesn't exist")
return nil, errors.New("policy not found")
}
if chaincode.DisregardNamespacePolicy && len(chaincode.KeyPolicies) == 0 && len(policies) == 1 {
sessionLogger.Warnf("Client requested to disregard chaincode %s's policy, but it did not specify any "+
"collection policies or key policies. This is probably a bug in the client side code, as the client should"+
"either not specify DisregardNamespacePolicy, or specify at least one key policy or at least one collection policy", chaincode.Name)
return nil, errors.Errorf("requested to disregard chaincode %s's policy but key and collection policies are missing, either "+
"disable DisregardNamespacePolicy or specify at least one key policy or at least one collection policy", chaincode.Name)
}
if chaincode.DisregardNamespacePolicy {
if len(policies) == 1 {
sessionLogger.Debugf("Client requested to disregard the namespace policy for chaincode %s,"+
" and no collection policies are present", chaincode.Name)
continue
}
sessionLogger.Debugf("Client requested to disregard the namespace policy for chaincode %s,"+
" however there exist %d collection policies taken into account", chaincode.Name, len(policies)-1)
policies = policies[1:]
}
inquireablePoliciesForChaincodeAndCollections = append(inquireablePoliciesForChaincodeAndCollections, policies...)
}
var cpss []inquire.ComparablePrincipalSets
for _, policy := range inquireablePoliciesForChaincodeAndCollections {
var cmpsets inquire.ComparablePrincipalSets
for _, ps := range policy.SatisfiedBy() {
cps := inquire.NewComparablePrincipalSet(ps)
if cps == nil {
return nil, errors.New("failed creating a comparable principal set")
}
cmpsets = append(cmpsets, cps)
}
if len(cmpsets) == 0 {
return nil, errors.New("endorsement policy cannot be satisfied")
}
cpss = append(cpss, cmpsets)
}
stateBasedCPS, err := computeStateBasedPrincipalSets(interest.Chaincodes, sessionLogger)
if err != nil {
return nil, errors.WithStack(err)
}
if len(stateBasedCPS) > 0 {
cpss = append(cpss, stateBasedCPS)
}
cps, err := mergePrincipalSets(cpss)
if err != nil {
return nil, errors.WithStack(err)
}
sessionLogger.Debugf("Merging principal sets: %v --> %v", cpss, cps)
return cps.ToPrincipalSets(), nil
}
type metadataAndFilterContext struct {
chainID common.ChannelID
interest *peer.ChaincodeInterest
fetch chaincodeMetadataFetcher
identityInfoByID map[string]api.PeerIdentityInfo
evaluator principalEvaluator
}
// metadataAndColFilter holds metadata and member filters
type metadataAndColFilter struct {
md []*chaincode.Metadata
isMemberAuthorized memberFilter
}
func loadMetadataAndFilters(ctx metadataAndFilterContext) (*metadataAndColFilter, error) {
sessionLogger := logger.With("channel", string(ctx.chainID))
var metadata []*chaincode.Metadata
var filters []identityFilter
for _, chaincode := range ctx.interest.Chaincodes {
ccMD := ctx.fetch.Metadata(string(ctx.chainID), chaincode.Name, chaincode.CollectionNames...)
if ccMD == nil {
return nil, errors.Errorf("No metadata was found for chaincode %s in channel %s", chaincode.Name, string(ctx.chainID))
}
metadata = append(metadata, ccMD)
if len(chaincode.CollectionNames) == 0 {
sessionLogger.Debugf("No collections for %s, skipping", chaincode.Name)
continue
}
if chaincode.NoPrivateReads {
sessionLogger.Debugf("No private reads, skipping")
continue
}
principalSetByCollections, err := principalsFromCollectionConfig(ccMD.CollectionsConfig)
if err != nil {
sessionLogger.Warningf("Failed initializing collection filter for chaincode %s: %v", chaincode.Name, err)
return nil, errors.WithStack(err)
}
filter, err := principalSetByCollections.toIdentityFilter(string(ctx.chainID), ctx.evaluator, chaincode)
if err != nil {
sessionLogger.Warningf("Failed computing collection principal sets for chaincode %s due to %v", chaincode.Name, err)
return nil, errors.WithStack(err)
}
filters = append(filters, filter)
}
return computeFiltersWithMetadata(filters, metadata, ctx.identityInfoByID), nil
}
func computeFiltersWithMetadata(filters identityFilters, metadata []*chaincode.Metadata, identityInfoByID map[string]api.PeerIdentityInfo) *metadataAndColFilter {
if len(filters) == 0 {
return &metadataAndColFilter{
md: metadata,
isMemberAuthorized: noopMemberFilter,
}
}
filter := filters.combine().toMemberFilter(identityInfoByID)
return &metadataAndColFilter{
isMemberAuthorized: filter,
md: metadata,
}
}
// identityFilter accepts or rejects peer identities
type identityFilter func(api.PeerIdentityType) bool
// identityFilters aggregates multiple identityFilters
type identityFilters []identityFilter
// memberFilter accepts or rejects NetworkMembers
type memberFilter func(member gossipdiscovery.NetworkMember) bool
// noopMemberFilter accepts every NetworkMember
func noopMemberFilter(_ gossipdiscovery.NetworkMember) bool {
return true
}
// combine combines all identityFilters into a single identityFilter which only accepts identities
// which all the original filters accept
func (filters identityFilters) combine() identityFilter {
return func(identity api.PeerIdentityType) bool {
for _, f := range filters {
if !f(identity) {
return false
}
}
return true
}
}
// toMemberFilter converts this identityFilter to a memberFilter based on the given mapping
// from PKI-ID as strings, to PeerIdentityInfo which holds the peer identities
func (idf identityFilter) toMemberFilter(identityInfoByID map[string]api.PeerIdentityInfo) memberFilter {
return func(member gossipdiscovery.NetworkMember) bool {
identity, exists := identityInfoByID[string(member.PKIid)]
if !exists {
return false
}
return idf(identity.Identity)
}
}
func (ea *endorsementAnalyzer) satisfiesPrincipal(channel string, identitiesOfMembers memberIdentities) peerPrincipalEvaluator {
return func(member gossipdiscovery.NetworkMember, principal *msp.MSPPrincipal) bool {
err := ea.SatisfiesPrincipal(channel, identitiesOfMembers.identityByPKIID(member.PKIid), principal)
if err == nil {
// TODO: log the principals in a human readable form
logger.Debug(member, "satisfies principal", principal)
return true
}
logger.Debug(member, "doesn't satisfy principal", principal, ":", err)
return false
}
}
type peerMembershipCriteria struct {
satGraph *principalPeerGraph
idOfMembers memberIdentities
chanMemberById map[string]gossipdiscovery.NetworkMember
possibleLayouts layouts
chaincodeMapping map[string]gossipdiscovery.NetworkMember
}
// endorsersByGroup computes a map from groups to peers.
// Each group included, is found in some layout, which means
// that there is some principal combination that includes the corresponding
// group.
// This means that if a group isn't included in the result, there is no
// principal combination (that includes the principal corresponding to the group),
// such that there are enough peers to satisfy the principal combination.
func endorsersByGroup(criteria *peerMembershipCriteria) map[string]*discovery.Peers {
satGraph := criteria.satGraph
idOfMembers := criteria.idOfMembers
chanMemberById := criteria.chanMemberById
includedGroups := criteria.possibleLayouts.groupsSet()
res := make(map[string]*discovery.Peers)
// Map endorsers to their corresponding groups.
// Iterate the principals, and put the peers into each group that corresponds with a principal vertex
for grp, principalVertex := range satGraph.principalVertices {
if _, exists := includedGroups[grp]; !exists {
// If the current group is not found in any layout, skip the corresponding principal
continue
}
peerList := &discovery.Peers{}
for _, peerVertex := range principalVertex.Neighbors() {
member := peerVertex.Data.(gossipdiscovery.NetworkMember)
// Check if this peer has the chaincode installed
stateInfo := chanMemberById[string(member.PKIid)]
_, hasChaincodeInstalled := criteria.chaincodeMapping[string(stateInfo.PKIid)]
if !hasChaincodeInstalled {
continue
}
peerList.Peers = append(peerList.Peers, &discovery.Peer{
Identity: idOfMembers.identityByPKIID(member.PKIid),
StateInfo: stateInfo.Envelope,
MembershipInfo: member.Envelope,
})
}
if len(peerList.Peers) > 0 {
res[grp] = peerList
}
}
return res
}
// computeLayouts computes all possible principal combinations
// that can be used to satisfy the endorsement policy, given a graph
// of available peers that maps each peer to a principal it satisfies.
// Each such a combination is called a layout, because it maps
// a group (alias for a principal) to a threshold of peers that need to endorse,
// and that satisfy the corresponding principal
func computeLayouts(principalsSets []policies.PrincipalSet, principalGroups principalGroupMapper, satGraph *principalPeerGraph) []*discovery.Layout {
var layouts []*discovery.Layout
// principalsSets is a collection of combinations of principals,
// such that each combination (given enough peers) satisfies the endorsement policy.
for _, principalSet := range principalsSets {
layout := &discovery.Layout{
QuantitiesByGroup: make(map[string]uint32),
}
// Since principalsSet has repetitions, we first
// compute a mapping from the principal to repetitions in the set.
for principal, plurality := range principalSet.UniqueSet() {
key := principalKey{
cls: int32(principal.PrincipalClassification),
principal: string(principal.Principal),
}
// We map the principal to a group, which is an alias for the principal.
layout.QuantitiesByGroup[principalGroups.group(key)] = uint32(plurality)
}
// Check that the layout can be satisfied with the current known peers
// This is done by iterating the current layout, and ensuring that
// each principal vertex is connected to at least <plurality> peer vertices.
if isLayoutSatisfied(layout.QuantitiesByGroup, satGraph) {
// If so, then add the layout to the layouts, since we have enough peers to satisfy the
// principal combination
layouts = append(layouts, layout)
}
}
return layouts
}
func isLayoutSatisfied(layout map[string]uint32, satGraph *principalPeerGraph) bool {
for grp, plurality := range layout {
// Do we have more than <plurality> peers connected to the principal?
if len(satGraph.principalVertices[grp].Neighbors()) < int(plurality) {
return false
}
}
return true
}
type principalPeerGraph struct {
peerVertices []*graph.Vertex
principalVertices map[string]*graph.Vertex
}
type principalAndPeerData struct {
members gossipdiscovery.Members
pGrps principalGroupMapper
}
func principalsToPeersGraph(data principalAndPeerData, satisfiesPrincipal peerPrincipalEvaluator) *principalPeerGraph {
// Create the peer vertices
peerVertices := make([]*graph.Vertex, len(data.members))
for i, member := range data.members {
peerVertices[i] = graph.NewVertex(string(member.PKIid), member)
}
// Create the principal vertices
principalVertices := make(map[string]*graph.Vertex)
for pKey, grp := range data.pGrps {
principalVertices[grp] = graph.NewVertex(grp, pKey.toPrincipal())
}
// Connect principals and peers
for _, principalVertex := range principalVertices {
for _, peerVertex := range peerVertices {
// If the current peer satisfies the principal, connect their corresponding vertices with an edge
principal := principalVertex.Data.(*msp.MSPPrincipal)
member := peerVertex.Data.(gossipdiscovery.NetworkMember)
if satisfiesPrincipal(member, principal) {
peerVertex.AddNeighbor(principalVertex)
}
}
}
return &principalPeerGraph{
peerVertices: peerVertices,
principalVertices: principalVertices,
}
}
func mapPrincipalsToGroups(principalsSets []policies.PrincipalSet) principalGroupMapper {
groupMapper := make(principalGroupMapper)
totalPrincipals := make(map[principalKey]struct{})
for _, principalSet := range principalsSets {
for _, principal := range principalSet {
totalPrincipals[principalKey{
principal: string(principal.Principal),
cls: int32(principal.PrincipalClassification),
}] = struct{}{}
}
}
for principal := range totalPrincipals {
groupMapper.group(principal)
}
return groupMapper
}
type memberIdentities map[string]api.PeerIdentityType
func (m memberIdentities) identityByPKIID(id common.PKIidType) api.PeerIdentityType {
return m[string(id)]
}
func computeIdentitiesOfMembers(identitySet api.PeerIdentitySet, members map[string]gossipdiscovery.NetworkMember) memberIdentities {
identitiesByPKIID := make(map[string]api.PeerIdentityType)
identitiesOfMembers := make(map[string]api.PeerIdentityType, len(members))
for _, identity := range identitySet {
identitiesByPKIID[string(identity.PKIId)] = identity.Identity
}
for _, member := range members {
if identity, exists := identitiesByPKIID[string(member.PKIid)]; exists {
identitiesOfMembers[string(member.PKIid)] = identity
}
}
return identitiesOfMembers
}
// principalGroupMapper maps principals to names of groups
type principalGroupMapper map[principalKey]string
func (mapper principalGroupMapper) group(principal principalKey) string {
if grp, exists := mapper[principal]; exists {
return grp
}
grp := fmt.Sprintf("G%d", len(mapper))
mapper[principal] = grp
return grp
}
type principalKey struct {
cls int32
principal string
}
func (pk principalKey) toPrincipal() *msp.MSPPrincipal {
return &msp.MSPPrincipal{
PrincipalClassification: msp.MSPPrincipal_Classification(pk.cls),
Principal: []byte(pk.principal),
}
}
// layouts is an aggregation of several layouts
type layouts []*discovery.Layout
// groupsSet returns a set of groups that the layouts contain
func (l layouts) groupsSet() map[string]struct{} {
m := make(map[string]struct{})
for _, layout := range l {
for grp := range layout.QuantitiesByGroup {
m[grp] = struct{}{}
}
}
return m
}
func peersWithChaincode(metadata ...*chaincode.Metadata) func(member gossipdiscovery.NetworkMember) bool {
return func(member gossipdiscovery.NetworkMember) bool {
if member.Properties == nil {
return false
}
for _, ccMD := range metadata {
var found bool
for _, cc := range member.Properties.Chaincodes {
if cc.Name == ccMD.Name && cc.Version == ccMD.Version {
found = true
}
}
if !found {
return false
}
}
return true
}
}
func mergePrincipalSets(cpss []inquire.ComparablePrincipalSets) (inquire.ComparablePrincipalSets, error) {
// Obtain the first ComparablePrincipalSet first
var cps inquire.ComparablePrincipalSets
cps, cpss, err := popComparablePrincipalSets(cpss)
if err != nil {
return nil, errors.WithStack(err)
}
for _, cps2 := range cpss {
cps = inquire.Merge(cps, cps2)
}
return cps, nil
}
func popComparablePrincipalSets(sets []inquire.ComparablePrincipalSets) (inquire.ComparablePrincipalSets, []inquire.ComparablePrincipalSets, error) {
if len(sets) == 0 {
return nil, nil, errors.New("no principal sets remained after filtering")
}
cps, cpss := sets[0], sets[1:]
return cps, cpss, nil
}