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  }