From 1e3510706ce704201c56a57490f5d6c8f297f5de Mon Sep 17 00:00:00 2001 From: Yoav Tock Date: Wed, 1 Mar 2023 13:48:39 +0200 Subject: [PATCH] Backport PR 4011 to release-2.4 PR 4011 addressed a bug (#3998) in IsChannelMember that manifested itself in the test `integration/raft/cft_test.go` use-case: "disregards certificate renewal if only the validity period changed" after it was converted to run without the system channel. Here we - Backport the fix (in etcdraft/consenter.go) - Add the same test case which runs without the system channel (w/o the fix this test case fails). - Backport some test infrastructure that helps running tests without the system channel. Signed-off-by: Yoav Tock Change-Id: I7205dbe264248c4ad534499955bb22710d07444d --- .../channel_participation.go | 58 ++++++++-- integration/nwo/network.go | 60 +++++++--- integration/nwo/standard_networks.go | 108 ++++++++++++++++++ integration/nwo/template/orderer_template.go | 2 + integration/raft/cft_test.go | 79 +++++++++++++ orderer/consensus/etcdraft/consenter.go | 17 +-- 6 files changed, 293 insertions(+), 31 deletions(-) diff --git a/integration/channelparticipation/channel_participation.go b/integration/channelparticipation/channel_participation.go index 2c5a1a318ed..d416d2326e4 100644 --- a/integration/channelparticipation/channel_participation.go +++ b/integration/channelparticipation/channel_participation.go @@ -10,7 +10,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "mime/multipart" "net/http" @@ -25,11 +25,20 @@ import ( func Join(n *nwo.Network, o *nwo.Orderer, channel string, block *common.Block, expectedChannelInfo ChannelInfo) { blockBytes, err := proto.Marshal(block) Expect(err).NotTo(HaveOccurred()) - url := fmt.Sprintf("https://127.0.0.1:%d/participation/v1/channels", n.OrdererPort(o, nwo.AdminPort)) + + protocol := "http" + if n.TLSEnabled { + protocol = "https" + } + url := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels", protocol, n.OrdererPort(o, nwo.AdminPort)) req := GenerateJoinRequest(url, channel, blockBytes) - authClient, _ := nwo.OrdererOperationalClients(n, o) + authClient, unauthClient := nwo.OrdererOperationalClients(n, o) - body := doBody(authClient, req) + client := unauthClient + if n.TLSEnabled { + client = authClient + } + body := doBody(client, req) c := &ChannelInfo{} err = json.Unmarshal(body, c) Expect(err).NotTo(HaveOccurred()) @@ -56,7 +65,7 @@ func doBody(client *http.Client, req *http.Request) []byte { resp, err := client.Do(req) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusCreated)) - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) resp.Body.Close() @@ -75,7 +84,12 @@ type ChannelInfoShort struct { func List(n *nwo.Network, o *nwo.Orderer) ChannelList { authClient, _ := nwo.OrdererOperationalClients(n, o) - listChannelsURL := fmt.Sprintf("https://127.0.0.1:%d/participation/v1/channels", n.OrdererPort(o, nwo.AdminPort)) + + protocol := "http" + if n.TLSEnabled { + protocol = "https" + } + listChannelsURL := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels", protocol, n.OrdererPort(o, nwo.AdminPort)) body := getBody(authClient, listChannelsURL)() list := &ChannelList{} @@ -89,7 +103,7 @@ func getBody(client *http.Client, url string) func() string { return func() string { resp, err := client.Get(url) Expect(err).NotTo(HaveOccurred()) - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) resp.Body.Close() return string(bodyBytes) @@ -106,7 +120,12 @@ type ChannelInfo struct { func ListOne(n *nwo.Network, o *nwo.Orderer, channel string) ChannelInfo { authClient, _ := nwo.OrdererOperationalClients(n, o) - listChannelURL := fmt.Sprintf("https://127.0.0.1:%d/participation/v1/channels/%s", n.OrdererPort(o, nwo.AdminPort), channel) + + protocol := "http" + if n.TLSEnabled { + protocol = "https" + } + listChannelURL := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels/%s", protocol, n.OrdererPort(o, nwo.AdminPort), channel) body := getBody(authClient, listChannelURL)() c := &ChannelInfo{} @@ -117,7 +136,12 @@ func ListOne(n *nwo.Network, o *nwo.Orderer, channel string) ChannelInfo { func Remove(n *nwo.Network, o *nwo.Orderer, channel string) { authClient, _ := nwo.OrdererOperationalClients(n, o) - url := fmt.Sprintf("https://127.0.0.1:%d/participation/v1/channels/%s", n.OrdererPort(o, nwo.AdminPort), channel) + + protocol := "http" + if n.TLSEnabled { + protocol = "https" + } + url := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels/%s", protocol, n.OrdererPort(o, nwo.AdminPort), channel) req, err := http.NewRequest(http.MethodDelete, url, nil) Expect(err).NotTo(HaveOccurred()) @@ -158,3 +182,19 @@ func channelInfoShortMatcher(channel string) types.GomegaMatcher { "URL": Equal(fmt.Sprintf("/participation/v1/channels/%s", channel)), }) } + +// JoinOrderersAppChannelCluster Joins a set of orderers to a channel for which the genesis block was created by the network +// bootstrap. It assumes a channel with one or more orderers (a cluster). +func JoinOrderersAppChannelCluster(network *nwo.Network, channelID string, orderers ...*nwo.Orderer) { + appGenesisBlock := network.LoadAppChannelGenesisBlock(channelID) + for _, orderer := range orderers { + expectedChannelInfo := ChannelInfo{ + Name: channelID, + URL: fmt.Sprintf("/participation/v1/channels/%s", channelID), + Status: "active", + ConsensusRelation: "consenter", + Height: 1, + } + Join(network, orderer, channelID, appGenesisBlock, expectedChannelInfo) + } +} diff --git a/integration/nwo/network.go b/integration/nwo/network.go index 3cfde9c7644..55f36c16a77 100644 --- a/integration/nwo/network.go +++ b/integration/nwo/network.go @@ -25,6 +25,9 @@ import ( "text/template" "time" + "github.com/hyperledger/fabric-protos-go/common" + "github.com/hyperledger/fabric/protoutil" + docker "github.com/fsouza/go-dockerclient" "github.com/hyperledger/fabric/integration/nwo/commands" "github.com/hyperledger/fabric/integration/nwo/fabricconfig" @@ -286,7 +289,9 @@ func (n *Network) AddOrg(o *Organization, peers ...*Peer) { } n.Organizations = append(n.Organizations, o) - n.Consortiums[0].Organizations = append(n.Consortiums[0].Organizations, o.Name) + if n.Consortiums != nil { + n.Consortiums[0].Organizations = append(n.Consortiums[0].Organizations, o.Name) + } } // ConfigTxPath returns the path to the generated configtxgen configuration @@ -787,22 +792,38 @@ func (n *Network) Bootstrap() { n.bootstrapIdemix() - sess, err = n.ConfigTxGen(commands.OutputBlock{ - ChannelID: n.SystemChannel.Name, - Profile: n.SystemChannel.Profile, - ConfigPath: n.RootDir, - OutputBlock: n.OutputBlockPath(n.SystemChannel.Name), - }) - Expect(err).NotTo(HaveOccurred()) - Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) + if n.SystemChannel != nil { + sess, err = n.ConfigTxGen(commands.OutputBlock{ + ChannelID: n.SystemChannel.Name, + Profile: n.SystemChannel.Profile, + ConfigPath: n.RootDir, + OutputBlock: n.OutputBlockPath(n.SystemChannel.Name), + }) + Expect(err).NotTo(HaveOccurred()) + Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) + + for _, c := range n.Channels { + sess, err := n.ConfigTxGen(commands.CreateChannelTx{ + ChannelID: c.Name, + Profile: c.Profile, + BaseProfile: c.BaseProfile, + ConfigPath: n.RootDir, + OutputCreateChannelTx: n.CreateChannelTxPath(c.Name), + }) + Expect(err).NotTo(HaveOccurred()) + Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) + } + + n.ConcatenateTLSCACertificates() + return + } for _, c := range n.Channels { - sess, err := n.ConfigTxGen(commands.CreateChannelTx{ - ChannelID: c.Name, - Profile: c.Profile, - BaseProfile: c.BaseProfile, - ConfigPath: n.RootDir, - OutputCreateChannelTx: n.CreateChannelTxPath(c.Name), + sess, err := n.ConfigTxGen(commands.OutputBlock{ + ChannelID: c.Name, + Profile: c.Profile, + ConfigPath: n.RootDir, + OutputBlock: n.OutputBlockPath(c.Name), }) Expect(err).NotTo(HaveOccurred()) Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0)) @@ -2006,3 +2027,12 @@ func (n *Network) GenerateCoreConfig(p *Peer) { err = t.Execute(io.MultiWriter(core, pw), n) Expect(err).NotTo(HaveOccurred()) } + +func (n *Network) LoadAppChannelGenesisBlock(channelID string) *common.Block { + appGenesisPath := n.OutputBlockPath(channelID) + appGenesisBytes, err := ioutil.ReadFile(appGenesisPath) + Expect(err).NotTo(HaveOccurred()) + appGenesisBlock, err := protoutil.UnmarshalBlock(appGenesisBytes) + Expect(err).NotTo(HaveOccurred()) + return appGenesisBlock +} diff --git a/integration/nwo/standard_networks.go b/integration/nwo/standard_networks.go index 70ab1da1b1a..f4c0c26431b 100644 --- a/integration/nwo/standard_networks.go +++ b/integration/nwo/standard_networks.go @@ -302,3 +302,111 @@ func MultiNodeEtcdRaft() *Config { }} return config } + +// === configurations for testing without the system channel === + +// BasicConfig is a configuration with two organizations and one peer per org. +// This configuration does not specify a consensus type. +func BasicConfig() *Config { + return &Config{ + Organizations: []*Organization{{ + Name: "OrdererOrg", + MSPID: "OrdererMSP", + Domain: "example.com", + EnableNodeOUs: false, + Users: 0, + CA: &CA{Hostname: "ca"}, + }, { + Name: "Org1", + MSPID: "Org1MSP", + Domain: "org1.example.com", + EnableNodeOUs: true, + Users: 2, + CA: &CA{Hostname: "ca"}, + }, { + Name: "Org2", + MSPID: "Org2MSP", + Domain: "org2.example.com", + EnableNodeOUs: true, + Users: 2, + CA: &CA{Hostname: "ca"}, + }}, + Consortiums: []*Consortium{{ + Name: "SampleConsortium", + Organizations: []string{ + "Org1", + "Org2", + }, + }}, + Consensus: &Consensus{ + BootstrapMethod: "file", + }, + SystemChannel: &SystemChannel{ + Name: "systemchannel", + Profile: "TwoOrgsOrdererGenesis", + }, + Orderers: []*Orderer{ + {Name: "orderer", Organization: "OrdererOrg"}, + }, + Channels: []*Channel{ + {Name: "testchannel", Profile: "TwoOrgsChannel"}, + }, + Peers: []*Peer{{ + Name: "peer0", + Organization: "Org1", + Channels: []*PeerChannel{ + {Name: "testchannel", Anchor: true}, + }, + }, { + Name: "peer0", + Organization: "Org2", + Channels: []*PeerChannel{ + {Name: "testchannel", Anchor: true}, + }, + }}, + Profiles: []*Profile{ + { + Name: "TwoOrgsOrdererGenesis", + Orderers: []string{"orderer"}, + }, + { + Name: "TwoOrgsChannel", + Consortium: "SampleConsortium", + Organizations: []string{"Org1", "Org2"}, + }, + }, + } +} + +func BasicEtcdRaftNoSysChan() *Config { + config := BasicConfig() + + config.Consensus.Type = "etcdraft" + config.Profiles = []*Profile{ + { + Name: "TwoOrgsAppChannelEtcdRaft", + Consortium: "SampleConsortium", + Orderers: []string{"orderer"}, + Organizations: []string{"Org1", "Org2"}, + }, + } + config.SystemChannel = nil + config.Consensus.ChannelParticipationEnabled = true + config.Consensus.BootstrapMethod = "none" + config.Consortiums = nil + config.Channels = []*Channel{{Name: "testchannel", Profile: "TwoOrgsAppChannelEtcdRaft"}} + + return config +} + +func MultiNodeEtcdRaftNoSysChan() *Config { + config := BasicEtcdRaftNoSysChan() + config.Orderers = []*Orderer{ + {Name: "orderer1", Organization: "OrdererOrg"}, + {Name: "orderer2", Organization: "OrdererOrg"}, + {Name: "orderer3", Organization: "OrdererOrg"}, + } + config.Profiles[0].Orderers = []string{"orderer1", "orderer2", "orderer3"} + + return config +} diff --git a/integration/nwo/template/orderer_template.go b/integration/nwo/template/orderer_template.go index 65b9f8248f0..33d245277df 100644 --- a/integration/nwo/template/orderer_template.go +++ b/integration/nwo/template/orderer_template.go @@ -36,7 +36,9 @@ General: ServerInterval: 7200s ServerTimeout: 20s BootstrapMethod: {{ .Consensus.BootstrapMethod }} + {{- if eq $w.Consensus.BootstrapMethod "file" }} BootstrapFile: {{ .RootDir }}/{{ .SystemChannel.Name }}_block.pb + {{- end }} LocalMSPDir: {{ $w.OrdererLocalMSPDir Orderer }} LocalMSPID: {{ ($w.Organization Orderer.Organization).MSPID }} Profile: diff --git a/integration/raft/cft_test.go b/integration/raft/cft_test.go index a42ab4f6f91..115e8866681 100644 --- a/integration/raft/cft_test.go +++ b/integration/raft/cft_test.go @@ -22,6 +22,8 @@ import ( "syscall" "time" + "github.com/hyperledger/fabric/integration/channelparticipation" + docker "github.com/fsouza/go-dockerclient" "github.com/golang/protobuf/proto" conftx "github.com/hyperledger/fabric-config/configtx" @@ -849,6 +851,83 @@ var _ = Describe("EndToEnd Crash Fault Tolerance", func() { "systemchannel": 2, }, []*nwo.Orderer{o1, o2, o3}, peer, network) }) + + It("disregards certificate renewal if only the validity period changed, without system channel", func() { + config := nwo.MultiNodeEtcdRaftNoSysChan() + config.Channels = append(config.Channels, &nwo.Channel{Name: "foo", Profile: "TwoOrgsAppChannelEtcdRaft"}) + config.Channels = append(config.Channels, &nwo.Channel{Name: "bar", Profile: "TwoOrgsAppChannelEtcdRaft"}) + network = nwo.New(config, testDir, client, StartPort(), components) + + network.GenerateConfigTree() + network.Bootstrap() + + peer = network.Peer("Org1", "peer0") + + o1 := network.Orderer("orderer1") + o2 := network.Orderer("orderer2") + o3 := network.Orderer("orderer3") + + orderers := []*nwo.Orderer{o1, o2, o3} + + o1Runner := network.OrdererRunner(o1) + o2Runner := network.OrdererRunner(o2) + o3Runner := network.OrdererRunner(o3) + ordererRunners := []*ginkgomon.Runner{o1Runner, o2Runner, o3Runner} + + o1Proc = ifrit.Invoke(o1Runner) + o2Proc = ifrit.Invoke(o2Runner) + o3Proc = ifrit.Invoke(o3Runner) + ordererProcesses := []ifrit.Process{o1Proc, o2Proc, o3Proc} + + Eventually(o1Proc.Ready(), network.EventuallyTimeout).Should(BeClosed()) + Eventually(o2Proc.Ready(), network.EventuallyTimeout).Should(BeClosed()) + Eventually(o3Proc.Ready(), network.EventuallyTimeout).Should(BeClosed()) + + channelparticipation.JoinOrderersAppChannelCluster(network, "testchannel", o1, o2, o3) + By("Waiting for them to elect a leader") + findLeader(ordererRunners) + + By("Creating a channel") + channelparticipation.JoinOrderersAppChannelCluster(network, "foo", o1, o2, o3) + + assertBlockReception(map[string]int{ + "foo": 0, + "testchannel": 0, + }, []*nwo.Orderer{o1, o2, o3}, peer, network) + + By("Killing all orderers") + for i := range orderers { + ordererProcesses[i].Signal(syscall.SIGTERM) + Eventually(ordererProcesses[i].Wait(), network.EventuallyTimeout).Should(Receive()) + } + + By("Renewing the certificates for all orderers") + renewOrdererCertificates(network, o1, o2, o3) + + By("Starting the orderers again") + for i := range orderers { + ordererRunner := network.OrdererRunner(orderers[i]) + ordererRunners[i] = ordererRunner + ordererProcesses[i] = ifrit.Invoke(ordererRunner) + Eventually(ordererProcesses[0].Ready(), network.EventuallyTimeout).Should(BeClosed()) + } + + o1Proc = ordererProcesses[0] + o2Proc = ordererProcesses[1] + o3Proc = ordererProcesses[2] + + By("Waiting for them to elect a leader once again") + findLeader(ordererRunners) + + By("Creating a channel again") + channelparticipation.JoinOrderersAppChannelCluster(network, "bar", o1, o2, o3) + + assertBlockReception(map[string]int{ + "foo": 0, + "bar": 0, + "testchannel": 0, + }, []*nwo.Orderer{o1, o2, o3}, peer, network) + }) }) When("admin certificate expires", func() { diff --git a/orderer/consensus/etcdraft/consenter.go b/orderer/consensus/etcdraft/consenter.go index b34e14030b1..74fcb9a9226 100644 --- a/orderer/consensus/etcdraft/consenter.go +++ b/orderer/consensus/etcdraft/consenter.go @@ -7,7 +7,6 @@ SPDX-License-Identifier: Apache-2.0 package etcdraft import ( - "bytes" "path" "reflect" "time" @@ -291,15 +290,19 @@ func (c *Consenter) IsChannelMember(joinBlock *common.Block) (bool, error) { return false, errors.Wrapf(err, "failed to validate config metadata of ordering config") } - member := false - for _, consenter := range configMetadata.Consenters { - if bytes.Equal(c.Cert, consenter.ServerTlsCert) || bytes.Equal(c.Cert, consenter.ClientTlsCert) { - member = true - break + consenters := make(map[uint64]*etcdraft.Consenter) + for i, c := range configMetadata.Consenters { + consenters[uint64(i+1)] = c // the IDs don't matter + } + + if _, err := c.detectSelfID(consenters); err != nil { + if err != cluster.ErrNotInChannel { + return false, errors.Wrapf(err, "failed to detect self ID by comparing public keys") } + return false, nil } - return member, nil + return true, nil } // RemoveInactiveChainRegistry stops and removes the inactive chain registry.