github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/signature/block_signer_decoder_test.go (about) 1 package signature 2 3 import ( 4 "errors" 5 "testing" 6 7 "github.com/stretchr/testify/mock" 8 "github.com/stretchr/testify/require" 9 "github.com/stretchr/testify/suite" 10 11 hotstuff "github.com/onflow/flow-go/consensus/hotstuff/mocks" 12 "github.com/onflow/flow-go/consensus/hotstuff/model" 13 "github.com/onflow/flow-go/model/flow" 14 "github.com/onflow/flow-go/module/signature" 15 "github.com/onflow/flow-go/state" 16 "github.com/onflow/flow-go/utils/unittest" 17 ) 18 19 func TestBlockSignerDecoder(t *testing.T) { 20 suite.Run(t, new(blockSignerDecoderSuite)) 21 } 22 23 type blockSignerDecoderSuite struct { 24 suite.Suite 25 allConsensus flow.IdentityList 26 committee *hotstuff.DynamicCommittee 27 28 decoder *BlockSignerDecoder 29 block flow.Block 30 } 31 32 func (s *blockSignerDecoderSuite) SetupTest() { 33 // the default header fixture creates signerIDs for a committee of 10 nodes, so we prepare a committee same as that 34 s.allConsensus = unittest.IdentityListFixture(40, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical[flow.Identity]) 35 36 // mock consensus committee 37 s.committee = hotstuff.NewDynamicCommittee(s.T()) 38 s.committee.On("IdentitiesByEpoch", mock.Anything).Return(s.allConsensus.ToSkeleton(), nil).Maybe() 39 40 // prepare valid test block: 41 voterIndices, err := signature.EncodeSignersToIndices(s.allConsensus.NodeIDs(), s.allConsensus.NodeIDs()) 42 require.NoError(s.T(), err) 43 s.block = unittest.BlockFixture() 44 s.block.Header.ParentVoterIndices = voterIndices 45 46 s.decoder = NewBlockSignerDecoder(s.committee) 47 } 48 49 // Test_SuccessfulDecode tests happy path decoding 50 func (s *blockSignerDecoderSuite) Test_SuccessfulDecode() { 51 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 52 require.NoError(s.T(), err) 53 require.Equal(s.T(), s.allConsensus.NodeIDs(), ids) 54 } 55 56 // Test_RootBlock tests decoder accepts root block with empty signer indices 57 func (s *blockSignerDecoderSuite) Test_RootBlock() { 58 s.block.Header.ParentVoterIndices = nil 59 s.block.Header.ParentVoterSigData = nil 60 s.block.Header.View = 0 61 62 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 63 require.NoError(s.T(), err) 64 require.Empty(s.T(), ids) 65 } 66 67 // Test_CommitteeException verifies that `BlockSignerDecoder` 68 // does _not_ erroneously interpret an unexpected exception from the committee as 69 // a sign of an unknown block, i.e. the decoder should _not_ return an `model.ErrViewForUnknownEpoch` or `signature.InvalidSignerIndicesError` 70 func (s *blockSignerDecoderSuite) Test_CommitteeException() { 71 s.Run("ByEpoch exception", func() { 72 exception := errors.New("unexpected exception") 73 *s.committee = *hotstuff.NewDynamicCommittee(s.T()) 74 s.committee.On("IdentitiesByEpoch", mock.Anything).Return(nil, exception) 75 76 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 77 require.Empty(s.T(), ids) 78 require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch) 79 require.False(s.T(), signature.IsInvalidSignerIndicesError(err)) 80 require.ErrorIs(s.T(), err, exception) 81 }) 82 s.Run("ByBlock exception", func() { 83 exception := errors.New("unexpected exception") 84 *s.committee = *hotstuff.NewDynamicCommittee(s.T()) 85 s.committee.On("IdentitiesByEpoch", mock.Anything).Return(nil, model.ErrViewForUnknownEpoch) 86 s.committee.On("IdentitiesByBlock", mock.Anything).Return(nil, exception) 87 88 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 89 require.Empty(s.T(), ids) 90 require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch) 91 require.False(s.T(), signature.IsInvalidSignerIndicesError(err)) 92 require.ErrorIs(s.T(), err, exception) 93 }) 94 } 95 96 // Test_UnknownEpoch_KnownBlock tests handling of a block from an un-cached epoch but 97 // where the block is known - should return identities for block. 98 func (s *blockSignerDecoderSuite) Test_UnknownEpoch_KnownBlock() { 99 *s.committee = *hotstuff.NewDynamicCommittee(s.T()) 100 s.committee.On("IdentitiesByEpoch", s.block.Header.ParentView).Return(nil, model.ErrViewForUnknownEpoch) 101 s.committee.On("IdentitiesByBlock", s.block.Header.ParentID).Return(s.allConsensus, nil) 102 103 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 104 require.NoError(s.T(), err) 105 require.Equal(s.T(), s.allConsensus.NodeIDs(), ids) 106 } 107 108 // Test_UnknownEpoch_UnknownBlock tests handling of a block from an un-cached epoch 109 // where the block is unknown - should propagate state.ErrUnknownSnapshotReference. 110 func (s *blockSignerDecoderSuite) Test_UnknownEpoch_UnknownBlock() { 111 *s.committee = *hotstuff.NewDynamicCommittee(s.T()) 112 s.committee.On("IdentitiesByEpoch", s.block.Header.ParentView).Return(nil, model.ErrViewForUnknownEpoch) 113 s.committee.On("IdentitiesByBlock", s.block.Header.ParentID).Return(nil, state.ErrUnknownSnapshotReference) 114 115 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 116 require.ErrorIs(s.T(), err, state.ErrUnknownSnapshotReference) 117 require.Empty(s.T(), ids) 118 } 119 120 // Test_InvalidIndices verifies that `BlockSignerDecoder` returns 121 // signature.InvalidSignerIndicesError if the signer indices in the provided header 122 // are not a valid encoding. 123 func (s *blockSignerDecoderSuite) Test_InvalidIndices() { 124 s.block.Header.ParentVoterIndices = unittest.RandomBytes(1) 125 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 126 require.Empty(s.T(), ids) 127 require.True(s.T(), signature.IsInvalidSignerIndicesError(err)) 128 } 129 130 // Test_EpochTransition verifies that `BlockSignerDecoder` correctly handles blocks 131 // near a boundary where the committee changes - an epoch transition. 132 func (s *blockSignerDecoderSuite) Test_EpochTransition() { 133 // The block under test B is the first block of a new epoch, where the committee changed. 134 // B contains a QC formed during the view of B's parent -- hence B's signatures must 135 // be decoded w.r.t. the committee as of the parent's view. 136 // 137 // Epoch 1 Epoch 2 138 // PARENT <- | -- B 139 blockView := s.block.Header.View 140 parentView := s.block.Header.ParentView 141 epoch1Committee := s.allConsensus.ToSkeleton() 142 epoch2Committee, err := s.allConsensus.SamplePct(.8) 143 require.NoError(s.T(), err) 144 145 *s.committee = *hotstuff.NewDynamicCommittee(s.T()) 146 s.committee.On("IdentitiesByEpoch", parentView).Return(epoch1Committee, nil).Maybe() 147 s.committee.On("IdentitiesByEpoch", blockView).Return(epoch2Committee.ToSkeleton(), nil).Maybe() 148 149 ids, err := s.decoder.DecodeSignerIDs(s.block.Header) 150 require.NoError(s.T(), err) 151 require.Equal(s.T(), epoch1Committee.NodeIDs(), ids) 152 }