/* Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package etcdraft_test import ( "encoding/pem" "fmt" "io/ioutil" "os" "path" "path/filepath" "strings" "time" "github.com/golang/protobuf/proto" "github.com/hyperledger/fabric-protos-go/common" "github.com/hyperledger/fabric-protos-go/orderer" etcdraftproto "github.com/hyperledger/fabric-protos-go/orderer/etcdraft" "github.com/hyperledger/fabric/bccsp/sw" "github.com/hyperledger/fabric/common/channelconfig" "github.com/hyperledger/fabric/common/crypto/tlsgen" "github.com/hyperledger/fabric/common/flogging" "github.com/hyperledger/fabric/common/ledger/testutil" "github.com/hyperledger/fabric/core/config/configtest" "github.com/hyperledger/fabric/internal/configtxgen/encoder" "github.com/hyperledger/fabric/internal/configtxgen/genesisconfig" "github.com/hyperledger/fabric/internal/pkg/comm" "github.com/hyperledger/fabric/orderer/common/cluster" clustermocks "github.com/hyperledger/fabric/orderer/common/cluster/mocks" "github.com/hyperledger/fabric/orderer/consensus" "github.com/hyperledger/fabric/orderer/consensus/etcdraft" "github.com/hyperledger/fabric/orderer/consensus/etcdraft/mocks" consensusmocks "github.com/hyperledger/fabric/orderer/consensus/mocks" "github.com/hyperledger/fabric/protoutil" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" gtypes "github.com/onsi/gomega/types" "github.com/stretchr/testify/mock" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var certAsPEM []byte //go:generate counterfeiter -o mocks/orderer_capabilities.go --fake-name OrdererCapabilities . ordererCapabilities type ordererCapabilities interface { channelconfig.OrdererCapabilities } //go:generate counterfeiter -o mocks/orderer_config.go --fake-name OrdererConfig . ordererConfig type ordererConfig interface { channelconfig.Orderer } var _ = Describe("Consenter", func() { var ( chainManager *mocks.ChainManager support *consensusmocks.FakeConsenterSupport dataDir string snapDir string walDir string tlsCA tlsgen.CA ) BeforeEach(func() { var err error tlsCA, err = tlsgen.NewCA() Expect(err).NotTo(HaveOccurred()) kp, err := tlsCA.NewClientCertKeyPair() Expect(err).NotTo(HaveOccurred()) if certAsPEM == nil { certAsPEM = kp.Cert } chainManager = &mocks.ChainManager{} support = &consensusmocks.FakeConsenterSupport{} dataDir, err = ioutil.TempDir("", "consenter-") Expect(err).NotTo(HaveOccurred()) walDir = path.Join(dataDir, "wal-") snapDir = path.Join(dataDir, "snap-") blockBytes, err := ioutil.ReadFile("testdata/mychannel.block") Expect(err).NotTo(HaveOccurred()) goodConfigBlock := &common.Block{} proto.Unmarshal(blockBytes, goodConfigBlock) lastBlock := &common.Block{ Header: &common.BlockHeader{ Number: 1, }, Data: goodConfigBlock.Data, Metadata: &common.BlockMetadata{ Metadata: [][]byte{{}, protoutil.MarshalOrPanic(&common.Metadata{ Value: protoutil.MarshalOrPanic(&common.LastConfig{Index: 0}), })}, }, } support.BlockReturns(lastBlock) }) AfterEach(func() { if dataDir != "" { os.RemoveAll(dataDir) } }) When("the consenter is extracting the channel", func() { It("extracts successfully from step requests", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) ch := consenter.TargetChannel(&orderer.ConsensusRequest{Channel: "mychannel"}) Expect(ch).To(BeIdenticalTo("mychannel")) }) It("extracts successfully from submit requests", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) ch := consenter.TargetChannel(&orderer.SubmitRequest{Channel: "mychannel"}) Expect(ch).To(BeIdenticalTo("mychannel")) }) It("returns an empty string for the rest of the messages", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) ch := consenter.TargetChannel(&common.Block{}) Expect(ch).To(BeEmpty()) }) }) DescribeTable("identifies a bad block", func(block *common.Block, errMatcher gtypes.GomegaMatcher) { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) isMem, err := consenter.IsChannelMember(block) Expect(isMem).To(BeFalse()) Expect(err).To(errMatcher) }, Entry("nil block", nil, MatchError("nil block")), Entry("data is nil", &common.Block{}, MatchError("block data is nil")), Entry("data is empty", protoutil.NewBlock(10, []byte{1, 2, 3, 4}), MatchError("envelope index out of bounds")), Entry("bad data", func() *common.Block { block := protoutil.NewBlock(10, []byte{1, 2, 3, 4}) block.Data.Data = [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}} return block }(), MatchError(HavePrefix("block data does not carry an envelope at index 0: error unmarshalling Envelope: proto:"))), ) When("the consenter is asked about join-block membership", func() { var ( mspDir string memberKeyPair *tlsgen.CertKeyPair genesisBlockApp *common.Block confAppRaft *genesisconfig.Profile ) BeforeEach(func() { var err error mspDir, err = ioutil.TempDir(dataDir, "msp") Expect(err).NotTo(HaveOccurred()) confAppRaft = genesisconfig.Load(genesisconfig.SampleDevModeEtcdRaftProfile, configtest.GetDevConfigDir()) confAppRaft.Consortiums = nil confAppRaft.Consortium = "" // IsChannelMember verifies config meta along with it's tls certs // ofconsenters. So when we add new conseter with tls certs, they must be // signed by any msp from orderer config. Consenters in this test will // have certificates from fixtures generated by tlsgen pkg. To pass // validation, root ca cert should be part of a MSP in orderer config. // Adding tls ca root cert to an existing ordering org's MSP definition. Expect(confAppRaft.Orderer).NotTo(BeNil()) Expect(confAppRaft.Orderer.Organizations).ToNot(HaveLen(0)) Expect(confAppRaft.Orderer.EtcdRaft.Consenters).ToNot(HaveLen(0)) // one consenter is enough for testing confAppRaft.Orderer.EtcdRaft.Consenters = confAppRaft.Orderer.EtcdRaft.Consenters[:1] // Generate client pair using tlsCA and set it to the consenter memberKeyPair, err = tlsCA.NewServerCertKeyPair("127.0.0.1", "::1", "localhost") Expect(err).NotTo(HaveOccurred()) consenterDir, err := ioutil.TempDir(dataDir, "consenter") Expect(err).NotTo(HaveOccurred()) consenterCertPath := filepath.Join(consenterDir, "client.pem") err = ioutil.WriteFile(consenterCertPath, memberKeyPair.Cert, 0o644) Expect(err).NotTo(HaveOccurred()) confAppRaft.Orderer.EtcdRaft.Consenters[0].ClientTlsCert = []byte(consenterCertPath) confAppRaft.Orderer.EtcdRaft.Consenters[0].ServerTlsCert = []byte(consenterCertPath) // Don't want to spoil sampleconfig, copying it to some tmp dir. err = testutil.CopyDir(confAppRaft.Orderer.Organizations[0].MSPDir, mspDir, true) Expect(err).NotTo(HaveOccurred()) confAppRaft.Orderer.Organizations[0].MSPDir = mspDir confAppRaft.Orderer.Organizations[0].ID = fmt.Sprintf("SampleMSP-%d", time.Now().UnixNano()) // Write the TLS root cert to the msp folder err = ioutil.WriteFile(filepath.Join(mspDir, "tlscacerts", "cert.pem"), tlsCA.CertBytes(), 0o644) Expect(err).NotTo(HaveOccurred()) bootstrapper, err := encoder.NewBootstrapper(confAppRaft) Expect(err).NotTo(HaveOccurred()) genesisBlockApp = bootstrapper.GenesisBlockForChannel("my-raft-channel") Expect(genesisBlockApp).NotTo(BeNil()) }) It("identifies a member block", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), memberKeyPair.Cert) isMem, err := consenter.IsChannelMember(genesisBlockApp) Expect(isMem).To(BeTrue()) Expect(err).NotTo(HaveOccurred()) }) It("identifies a non-member block", func() { foreignCA, err := tlsgen.NewCA() Expect(err).NotTo(HaveOccurred()) nonMemberKeyPair, err := foreignCA.NewServerCertKeyPair("127.0.0.1", "::1", "localhost") Expect(err).NotTo(HaveOccurred()) consenter := newConsenter(chainManager, tlsCA.CertBytes(), nonMemberKeyPair.Cert) isMem, err := consenter.IsChannelMember(genesisBlockApp) Expect(isMem).To(BeFalse()) Expect(err).NotTo(HaveOccurred()) }) It("raft config has consenter with certificate that is not signed by any msp", func() { // This TLS root cert will not be part of the MSP. foreignCA, err := tlsgen.NewCA() Expect(err).NotTo(HaveOccurred()) foreignKeyPair, err := foreignCA.NewServerCertKeyPair("127.0.0.1", "::1", "localhost") Expect(err).NotTo(HaveOccurred()) consenterDir, err := ioutil.TempDir(dataDir, "foreign-consenter") Expect(err).NotTo(HaveOccurred()) foreignConsenterCertPath := filepath.Join(consenterDir, "client.pem") err = ioutil.WriteFile(foreignConsenterCertPath, foreignKeyPair.Cert, 0o644) Expect(err).NotTo(HaveOccurred()) confAppRaft.Orderer.EtcdRaft.Consenters[0].ClientTlsCert = []byte(foreignConsenterCertPath) confAppRaft.Orderer.EtcdRaft.Consenters[0].ServerTlsCert = []byte(foreignConsenterCertPath) consenter := newConsenter(chainManager, foreignCA.CertBytes(), foreignKeyPair.Cert) bootstrapper, err := encoder.NewBootstrapper(confAppRaft) Expect(err).NotTo(HaveOccurred()) genesisBlockApp = bootstrapper.GenesisBlockForChannel("my-raft-channel") Expect(genesisBlockApp).NotTo(BeNil()) isMem, err := consenter.IsChannelMember(genesisBlockApp) Expect(isMem).To(BeFalse()) Expect(err).To(HaveOccurred()) }) }) When("the consenter is asked for a chain", func() { cryptoProvider, _ := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore()) chainInstance := &etcdraft.Chain{CryptoProvider: cryptoProvider} BeforeEach(func() { chainManager.GetConsensusChainStub = func(channel string) consensus.Chain { switch channel { case "mychannel": return chainInstance default: return nil } } }) It("calls the chain manager and returns the reference when it is found", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) Expect(consenter).NotTo(BeNil()) chain := consenter.ReceiverByChain("mychannel") Expect(chain).NotTo(BeNil()) Expect(chain).To(BeIdenticalTo(chainInstance)) }) It("calls the chain manager and returns nil when it's not found", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) Expect(consenter).NotTo(BeNil()) chain := consenter.ReceiverByChain("notmychannel") Expect(chain).To(BeNil()) }) It("calls the chain manager and returns nil when it's not a raft chain", func() { consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) Expect(consenter).NotTo(BeNil()) chain := consenter.ReceiverByChain("notraftchain") Expect(chain).To(BeNil()) }) }) It("successfully constructs a Chain", func() { certAsPEMWithLineFeed := certAsPEM certAsPEMWithLineFeed = append(certAsPEMWithLineFeed, []byte("\n")...) m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: certAsPEMWithLineFeed}, }, Options: &etcdraftproto.Options{ TickInterval: "500ms", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) support.SharedConfigReturns(mockOrderer) consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) consenter.EtcdRaftConfig.WALDir = walDir consenter.EtcdRaftConfig.SnapDir = snapDir // consenter.EtcdRaftConfig.EvictionSuspicion is missing var defaultSuspicionFallback bool var trackChainCallback bool consenter.Metrics = newFakeMetrics(newFakeMetricsFields()) consenter.Logger = consenter.Logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error { if strings.Contains(entry.Message, "EvictionSuspicion not set, defaulting to 10m0s") { defaultSuspicionFallback = true } if strings.Contains(entry.Message, "After eviction from the cluster Registrar.SwitchToFollower will be called, and the orderer will become a follower of the channel.") { trackChainCallback = true } return nil })) chain, err := consenter.HandleChain(support, nil) Expect(err).NotTo(HaveOccurred()) Expect(chain).NotTo(BeNil()) Expect(chain.Start).NotTo(Panic()) Expect(defaultSuspicionFallback).To(BeTrue()) Expect(trackChainCallback).To(BeTrue()) }) It("successfully constructs a Chain without a system channel", func() { // We append a line feed to our cert, just to ensure that we can still consume it and ignore. certAsPEMWithLineFeed := certAsPEM certAsPEMWithLineFeed = append(certAsPEMWithLineFeed, []byte("\n")...) m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: certAsPEMWithLineFeed}, }, Options: &etcdraftproto.Options{ TickInterval: "500ms", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) support.SharedConfigReturns(mockOrderer) consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) consenter.EtcdRaftConfig.WALDir = walDir consenter.EtcdRaftConfig.SnapDir = snapDir // consenter.EtcdRaftConfig.EvictionSuspicion is missing var defaultSuspicionFallback bool var switchToFollowerCallback bool consenter.Metrics = newFakeMetrics(newFakeMetricsFields()) consenter.Logger = consenter.Logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error { if strings.Contains(entry.Message, "EvictionSuspicion not set, defaulting to 10m0s") { defaultSuspicionFallback = true } if strings.Contains(entry.Message, "After eviction from the cluster Registrar.SwitchToFollower will be called, and the orderer will become a follower of the channel.") { switchToFollowerCallback = true } return nil })) chain, err := consenter.HandleChain(support, nil) Expect(err).NotTo(HaveOccurred()) Expect(chain).NotTo(BeNil()) Expect(chain.Start).NotTo(Panic()) Expect(defaultSuspicionFallback).To(BeTrue()) Expect(switchToFollowerCallback).To(BeTrue()) Expect(chain.Halt).NotTo(Panic()) }) It("fails to handle chain if no matching cert found", func() { m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("foo")})}, }, Options: &etcdraftproto.Options{ TickInterval: "500ms", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) support := &consensusmocks.FakeConsenterSupport{} mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) support.SharedConfigReturns(mockOrderer) support.ChannelIDReturns("foo") consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) chain, err := consenter.HandleChain(support, &common.Metadata{}) Expect(chain).To((BeNil())) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("without a system channel, a follower should have been created")) }) It("fails to handle chain if etcdraft options have not been provided", func() { m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: []byte("cert.orderer1.org1")}, }, } metadata := protoutil.MarshalOrPanic(m) mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) support.SharedConfigReturns(mockOrderer) consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) chain, err := consenter.HandleChain(support, nil) Expect(chain).To(BeNil()) Expect(err).To(MatchError("etcdraft options have not been provided")) }) It("fails to handle chain if tick interval is invalid", func() { m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: certAsPEM}, }, Options: &etcdraftproto.Options{ TickInterval: "500", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) mockOrderer.CapabilitiesReturns(&mocks.OrdererCapabilities{}) support.SharedConfigReturns(mockOrderer) consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) chain, err := consenter.HandleChain(support, nil) Expect(chain).To(BeNil()) Expect(err).To(MatchError("failed to parse TickInterval (500) to time duration")) }) When("the TickIntervalOverride is invalid", func() { It("returns an error", func() { m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: certAsPEM}, }, Options: &etcdraftproto.Options{ TickInterval: "500", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) mockOrderer.CapabilitiesReturns(&mocks.OrdererCapabilities{}) support.SharedConfigReturns(mockOrderer) consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) consenter.EtcdRaftConfig.TickIntervalOverride = "seven" _, err := consenter.HandleChain(support, nil) Expect(err).To(MatchError(HavePrefix("failed parsing Consensus.TickIntervalOverride:"))) Expect(err).To(MatchError(ContainSubstring("seven"))) }) }) It("returns an error if no matching cert found", func() { m := &etcdraftproto.ConfigMetadata{ Consenters: []*etcdraftproto.Consenter{ {ServerTlsCert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("foo")})}, }, Options: &etcdraftproto.Options{ TickInterval: "500ms", ElectionTick: 10, HeartbeatTick: 1, MaxInflightBlocks: 5, }, } metadata := protoutil.MarshalOrPanic(m) support := &consensusmocks.FakeConsenterSupport{} mockOrderer := &mocks.OrdererConfig{} mockOrderer.ConsensusMetadataReturns(metadata) mockOrderer.BatchSizeReturns( &orderer.BatchSize{ PreferredMaxBytes: 2 * 1024 * 1024, }, ) support.SharedConfigReturns(mockOrderer) support.ChannelIDReturns("foo") consenter := newConsenter(chainManager, tlsCA.CertBytes(), certAsPEM) chain, err := consenter.HandleChain(support, &common.Metadata{}) Expect(chain).To((BeNil())) Expect(err).To(MatchError("without a system channel, a follower should have been created: not in the channel")) }) }) type consenter struct { *etcdraft.Consenter } func newConsenter(chainManager *mocks.ChainManager, caCert, cert []byte) *consenter { communicator := &clustermocks.Communicator{} communicator.On("Configure", mock.Anything, mock.Anything) cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore()) Expect(err).NotTo(HaveOccurred()) c := &etcdraft.Consenter{ ChainManager: chainManager, Communication: communicator, Cert: cert, Logger: flogging.MustGetLogger("test"), Dispatcher: &etcdraft.Dispatcher{ Logger: flogging.MustGetLogger("test"), ChainSelector: &mocks.ReceiverGetter{}, }, Dialer: &cluster.PredicateDialer{ Config: comm.ClientConfig{ SecOpts: comm.SecureOptions{ Certificate: caCert, }, }, }, BCCSP: cryptoProvider, } return &consenter{ Consenter: c, } }