/* Copyright IBM Corp. 2017 All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package smartbft_test import ( "crypto/sha256" "encoding/hex" "fmt" "strings" "sync/atomic" "testing" "github.com/SmartBFT-Go/consensus/pkg/types" "github.com/SmartBFT-Go/consensus/smartbftprotos" "github.com/golang/protobuf/proto" cb "github.com/hyperledger/fabric-protos-go/common" "github.com/hyperledger/fabric-protos-go/msp" "github.com/hyperledger/fabric/common/flogging" "github.com/hyperledger/fabric/orderer/consensus/smartbft" "github.com/hyperledger/fabric/orderer/consensus/smartbft/mocks" "github.com/hyperledger/fabric/protoutil" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) var hashOfZero = hex.EncodeToString(sha256.New().Sum(nil)) func TestNodeIdentitiesByID(t *testing.T) { m := make(smartbft.NodeIdentitiesByID) for id := uint64(0); id < 4; id++ { m[id] = protoutil.MarshalOrPanic(&msp.SerializedIdentity{ IdBytes: []byte(fmt.Sprintf("%d", id)), Mspid: "OrdererOrg", }) sID := &msp.SerializedIdentity{} err := proto.Unmarshal(m[id], sID) assert.NoError(t, err) id2, ok := m.IdentityToID(m[id]) assert.True(t, ok) assert.Equal(t, id, id2) } _, ok := m.IdentityToID(protoutil.MarshalOrPanic(&msp.SerializedIdentity{ IdBytes: []byte(fmt.Sprintf("%d", 4)), Mspid: "OrdererOrg", })) assert.False(t, ok) _, ok = m.IdentityToID([]byte{1, 2, 3}) assert.False(t, ok) } func TestVerifySignature(t *testing.T) { logger := flogging.MustGetLogger("test") cv := &mocks.ConsenterVerifier{} cv.On("Evaluate", mock.Anything).Return(errors.New("bad signature")) v := &smartbft.Verifier{ Logger: logger, ConsenterVerifier: cv, RuntimeConfig: &atomic.Value{}, } rtc := smartbft.RuntimeConfig{ ID2Identities: map[uint64][]byte{3: {0, 2, 4, 6}}, } v.RuntimeConfig.Store(rtc) t.Run("identity doesn't exist", func(t *testing.T) { err := v.VerifySignature(types.Signature{ ID: 2, }) assert.EqualError(t, err, "node with id of 2 doesn't exist") }) t.Run("signature doesn't verify", func(t *testing.T) { err := v.VerifySignature(types.Signature{ ID: 3, }) assert.EqualError(t, err, "bad signature") }) } func TestVerifyConsenterSig(t *testing.T) { logger := flogging.MustGetLogger("test") lastBlock := makeNonConfigBlock(19, 10) lastConfigBlock := makeConfigBlock(10) ac := &mocks.AccessController{} ac.On("Evaluate", mock.Anything).Return(nil) ledger := &mocks.Ledger{} ledger.On("Height").Return(uint64(20)) ledger.On("Block", uint64(19)).Return(lastBlock) ledger.On("Block", uint64(10)).Return(lastConfigBlock) sequencer := &mocks.Sequencer{} sequencer.On("Sequence").Return(uint64(12)) reqInspector := &smartbft.RequestInspector{ ValidateIdentityStructure: func(_ *msp.SerializedIdentity) error { return nil }, } cv := &mocks.ConsenterVerifier{} cv.On("Evaluate", mock.Anything).Return(errors.New("bad signature")) lastHash := hex.EncodeToString(protoutil.BlockHeaderHash(lastBlock.Header)) for _, testCase := range []struct { description string verificationSequence uint64 lastBlock *cb.Block id2Identity map[uint64][]byte signatureMutator func(types.Signature) types.Signature lastConfigBlockNum uint64 bftMetadataMutator func([]byte) []byte proposalMutator func(proposal types.Proposal) types.Proposal ordererBlockMetadataMutator func(metadata *cb.OrdererBlockMetadata) expectedErr string }{ { description: "No consenter in mapping", expectedErr: "node with id of 3 doesn't exist", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, }, { description: "Bad signature format", expectedErr: "malformed signature format: asn1: structure error: tags don't match (16 vs " + "{class:0 tag:1 length:2 isCompound:false}) {optional:false explicit:false application:" + "false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} Signature @2", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, signatureMutator: func(signature types.Signature) types.Signature { return types.Signature{ ID: signature.ID, Value: signature.Value, Msg: []byte{1, 2, 3}, } }, }, { description: "metadata doesn't match proposal", expectedErr: "consenter metadata in OrdererBlockMetadata doesn't match proposal", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, signatureMutator: func(signature types.Signature) types.Signature { sig := smartbft.Signature{} _ = sig.Unmarshal(signature.Msg) sig.OrdererBlockMetadata = nil return types.Signature{ ID: signature.ID, Value: signature.Value, Msg: sig.Marshal(), } }, }, { description: "block header doesn't match proposal", expectedErr: "mismatched block header", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, signatureMutator: func(signature types.Signature) types.Signature { sig := smartbft.Signature{} _ = sig.Unmarshal(signature.Msg) sig.BlockHeader = nil return types.Signature{ ID: signature.ID, Value: signature.Value, Msg: sig.Marshal(), } }, }, { description: "nonce different than what was used for signing", expectedErr: "bad signature", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, signatureMutator: func(signature types.Signature) types.Signature { sig := smartbft.Signature{} _ = sig.Unmarshal(signature.Msg) // sig.Nonce = nil return types.Signature{ ID: signature.ID, Value: signature.Value, Msg: sig.Marshal(), } }, }, { description: "orderer block metadata is malformed", expectedErr: "malformed orderer metadata in signature: proto: cannot parse invalid wire-format data", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, signatureMutator: func(signature types.Signature) types.Signature { sig := smartbft.Signature{} _ = sig.Unmarshal(signature.Msg) sig.OrdererBlockMetadata = []byte{1, 2, 3} return types.Signature{ ID: signature.ID, Value: signature.Value, Msg: sig.Marshal(), } }, }, { description: "signature doesn't verify", expectedErr: "bad signature", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, }, { description: "malformed proposal", expectedErr: "bad payload and metadata tuple: asn1: structure error: " + "tags don't match (16 vs {class:0 tag:1 length:2 isCompound:false}) " + "{optional:false explicit:false application:false private:false " + "defaultValue: tag: stringType:0 timeType:0 set:false " + "omitEmpty:false} ByteBufferTuple @2", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { proposal.Payload = []byte{1, 2, 3} return proposal }, }, { description: "empty proposal payload", expectedErr: "proposal payload cannot be nil", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { proposal.Payload = nil return proposal }, }, { description: "metadata too short", expectedErr: "block metadata is of size 4 but should be of size 5", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { block, _ := smartbft.ProposalToBlock(proposal) block.Metadata.Metadata = make([][]byte, len(cb.BlockMetadataIndex_name)-1) bbt := &smartbft.ByteBufferTuple{} _ = bbt.FromBytes(proposal.Payload) bbt.B = protoutil.MarshalOrPanic(block.Metadata) proposal.Payload = bbt.ToBytes() return proposal }, }, { description: "malformed signature metadata", expectedErr: "malformed signature metadata: proto: cannot parse invalid wire-format data", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { block, _ := smartbft.ProposalToBlock(proposal) block.Metadata.Metadata[cb.BlockMetadataIndex_SIGNATURES] = []byte{1, 2, 3} bbt := &smartbft.ByteBufferTuple{} _ = bbt.FromBytes(proposal.Payload) bbt.B = protoutil.MarshalOrPanic(block.Metadata) proposal.Payload = bbt.ToBytes() return proposal }, }, { description: "malformed OrdererBlockMetadata", expectedErr: "malformed orderer metadata in block: proto: cannot parse invalid wire-format data", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { block, _ := smartbft.ProposalToBlock(proposal) md := &cb.Metadata{} _ = proto.Unmarshal(block.Metadata.Metadata[cb.BlockMetadataIndex_SIGNATURES], md) md.Value = []byte{1, 2, 3} block.Metadata.Metadata[cb.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(md) bbt := &smartbft.ByteBufferTuple{} _ = bbt.FromBytes(proposal.Payload) bbt.B = protoutil.MarshalOrPanic(block.Metadata) proposal.Payload = bbt.ToBytes() return proposal }, }, { description: "mismatched OrdererBlockMetadata", expectedErr: "signature's OrdererBlockMetadata and OrdererBlockMetadata extracted from block do not match", lastBlock: lastBlock, lastConfigBlockNum: lastConfigBlock.Header.Number, id2Identity: map[uint64][]byte{3: {0, 2, 4, 6}}, proposalMutator: func(proposal types.Proposal) types.Proposal { block, _ := smartbft.ProposalToBlock(proposal) md := &cb.Metadata{} _ = proto.Unmarshal(block.Metadata.Metadata[cb.BlockMetadataIndex_SIGNATURES], md) obm := &cb.OrdererBlockMetadata{} _ = proto.Unmarshal(md.Value, obm) obm.LastConfig.Index++ md.Value = protoutil.MarshalOrPanic(obm) block.Metadata.Metadata[cb.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(md) bbt := &smartbft.ByteBufferTuple{} _ = bbt.FromBytes(proposal.Payload) bbt.B = protoutil.MarshalOrPanic(block.Metadata) proposal.Payload = bbt.ToBytes() return proposal }, }, } { t.Run(testCase.description, func(t *testing.T) { ss := &mocks.SignerSerializer{} ss.On("Sign", mock.Anything).Return([]byte{1, 2, 3}, nil).Once() ss.On("Serialize", mock.Anything).Return([]byte{0, 2, 4, 6}, nil) s := &smartbft.Signer{ LastConfigBlockNum: func(_ *cb.Block) uint64 { return lastConfigBlock.Header.Number }, SignerSerializer: ss, Logger: flogging.MustGetLogger("test"), ID: 3, } rtc := smartbft.RuntimeConfig{ ID2Identities: testCase.id2Identity, LastBlock: testCase.lastBlock, LastConfigBlock: lastConfigBlock, LastCommittedBlockHash: lastHash, } runtimeConfig := &atomic.Value{} runtimeConfig.Store(rtc) assembler := &smartbft.Assembler{ VerificationSeq: func() uint64 { return testCase.verificationSequence }, Logger: logger, RuntimeConfig: runtimeConfig, } md := protoutil.MarshalOrPanic(&smartbftprotos.ViewMetadata{ LatestSequence: 1, ViewId: 2, }) proposal := assembler.AssembleProposal(md, [][]byte{nonConfigTx}) signature := *s.SignProposal(proposal, nil) if testCase.signatureMutator != nil { signature = testCase.signatureMutator(signature) } v := &smartbft.Verifier{ RuntimeConfig: runtimeConfig, Logger: logger, Ledger: ledger, VerificationSequencer: sequencer, AccessController: ac, ConsenterVerifier: cv, ReqInspector: reqInspector, } if testCase.proposalMutator != nil { proposal = testCase.proposalMutator(proposal) } _, err := v.VerifyConsenterSig(signature, proposal) assert.Error(t, err) assert.Equal(t, testCase.expectedErr, strings.ReplaceAll(err.Error(), "\u00a0", " ")) }) } } func TestVerifyProposal(t *testing.T) { logger := flogging.MustGetLogger("test") lastBlock := makeNonConfigBlock(19, 10) notLastBlock := makeNonConfigBlock(18, 10) lastConfigBlock := makeConfigBlock(10) ledger := &mocks.Ledger{} ledger.On("Height").Return(uint64(20)) ledger.On("Block", uint64(19)).Return(lastBlock) ledger.On("Block", uint64(10)).Return(lastConfigBlock) sequencer := &mocks.Sequencer{} sequencer.On("Sequence").Return(uint64(12)) ac := &mocks.AccessController{} ac.On("Evaluate", mock.Anything).Return(nil) cv := &mocks.ConsenterVerifier{} reqInspector := &smartbft.RequestInspector{ ValidateIdentityStructure: func(_ *msp.SerializedIdentity) error { return nil }, } lastHash := hex.EncodeToString(protoutil.BlockHeaderHash(lastBlock.Header)) for _, testCase := range []struct { description string verificationSequence uint64 lastBlock *cb.Block lastConfigBlock *cb.Block bftMetadataMutator func([]byte) []byte ordererBlockMetadataMutator func(metadata *cb.OrdererBlockMetadata) expectedErr string }{ { description: "green path", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, }, { description: "wrong verification sequence 1", verificationSequence: 11, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, expectedErr: "expected verification sequence 12, but proposal has 11", }, { description: "wrong verification sequence 2", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: protoutil.NewBlock(666, nil), bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, expectedErr: "last config in block orderer metadata points to 666 but our persisted last config is 10", }, { description: "wrong verification sequence 3", verificationSequence: 12, lastBlock: notLastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, expectedErr: fmt.Sprintf("previous header hash is %s but expected %s", hex.EncodeToString(protoutil.BlockHeaderHash(notLastBlock.Header)), hex.EncodeToString(protoutil.BlockHeaderHash(lastBlock.Header))), }, { description: "corrupt metadata", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: func([]byte) []byte { return []byte{1, 2, 3} }, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, expectedErr: "failed unmarshaling smartbft metadata from proposal: proto: cannot parse invalid wire-format data", }, { description: "corrupt metadata", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: func([]byte) []byte { return protoutil.MarshalOrPanic(&smartbftprotos.ViewMetadata{LatestSequence: 100, ViewId: 2}) }, ordererBlockMetadataMutator: noopOrdererBlockMetadataMutator, expectedErr: "expected metadata in block to be [view_id:2 latest_sequence:100] but got [view_id:2 latest_sequence:1]", }, { description: "No last config", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: func(metadata *cb.OrdererBlockMetadata) { metadata.LastConfig = nil }, expectedErr: "last config is nil", }, { description: "Mismatched last config", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: func(metadata *cb.OrdererBlockMetadata) { metadata.LastConfig.Index = 666 }, expectedErr: "last config in block orderer metadata points to 666 but our persisted last config is 10", }, { description: "Corrupt inner BFT metadata", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: func(metadata *cb.OrdererBlockMetadata) { metadata.ConsenterMetadata = []byte{1, 2, 3} }, expectedErr: "failed unmarshaling smartbft metadata from block: proto: cannot parse invalid wire-format data", }, { description: "Mismatching inner BFT metadata", verificationSequence: 12, lastBlock: lastBlock, lastConfigBlock: lastConfigBlock, bftMetadataMutator: noopMutator, ordererBlockMetadataMutator: func(metadata *cb.OrdererBlockMetadata) { metadata.ConsenterMetadata = protoutil.MarshalOrPanic(&smartbftprotos.ViewMetadata{LatestSequence: 666}) }, expectedErr: "expected metadata in block to be [view_id:2 latest_sequence:1] but got [view_id:0 latest_sequence:666]", }, } { t.Run(testCase.description, func(t *testing.T) { runtimeConfig := &atomic.Value{} rtc := smartbft.RuntimeConfig{ LastCommittedBlockHash: lastHash, LastBlock: testCase.lastBlock, LastConfigBlock: testCase.lastConfigBlock, } runtimeConfig.Store(rtc) assembler := &smartbft.Assembler{ RuntimeConfig: &atomic.Value{}, VerificationSeq: func() uint64 { return testCase.verificationSequence }, Logger: logger, } assembler.RuntimeConfig.Store(smartbft.RuntimeConfig{ LastConfigBlock: testCase.lastConfigBlock, LastBlock: testCase.lastBlock, }) md := protoutil.MarshalOrPanic(&smartbftprotos.ViewMetadata{ LatestSequence: 1, ViewId: 2, }) proposal := assembler.AssembleProposal(md, [][]byte{nonConfigTx}) // Maybe mutate the BFT metadata proposal.Metadata = testCase.bftMetadataMutator(proposal.Metadata) // Unwrap the OrdererBlockMetadata tuple := &smartbft.ByteBufferTuple{} _ = tuple.FromBytes(proposal.Payload) blockMD := &cb.BlockMetadata{} assert.NoError(t, proto.Unmarshal(tuple.B, blockMD)) sigMD := &cb.Metadata{} assert.NoError(t, proto.Unmarshal(blockMD.Metadata[cb.BlockMetadataIndex_SIGNATURES], sigMD)) ordererMetadataFromSignature := &cb.OrdererBlockMetadata{} assert.NoError(t, proto.Unmarshal(sigMD.Value, ordererMetadataFromSignature)) // Mutate the OrdererBlockMetadata testCase.ordererBlockMetadataMutator(ordererMetadataFromSignature) // And fold it back into the block sigMD.Value = protoutil.MarshalOrPanic(ordererMetadataFromSignature) blockMD.Metadata[cb.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(sigMD) tuple.B = protoutil.MarshalOrPanic(blockMD) proposal.Payload = tuple.ToBytes() runtimeConfig = &atomic.Value{} rtc.LastConfigBlock = lastConfigBlock runtimeConfig.Store(rtc) v := &smartbft.Verifier{ RuntimeConfig: runtimeConfig, Logger: logger, Ledger: ledger, VerificationSequencer: sequencer, AccessController: ac, ConsenterVerifier: cv, ReqInspector: reqInspector, } reqInfo, err := v.VerifyProposal(proposal) if testCase.expectedErr == "" { assert.NoError(t, err) assert.NotNil(t, reqInfo) assert.Len(t, reqInfo, 1) assert.Equal(t, hashOfZero, reqInfo[0].ClientID) assert.Equal(t, hashOfZero, reqInfo[0].ID) return } assert.Error(t, err) assert.Equal(t, testCase.expectedErr, strings.ReplaceAll(err.Error(), "\u00a0", " ")) assert.Nil(t, reqInfo) }) } } func noopMutator(b []byte) []byte { return b } func noopOrdererBlockMetadataMutator(_ *cb.OrdererBlockMetadata) { }