github.com/koko1123/flow-go-1@v0.29.6/engine/consensus/compliance/core_test.go (about)

     1  package compliance
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/stretchr/testify/suite"
    13  
    14  	hotstuff "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks"
    15  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    16  	"github.com/koko1123/flow-go-1/model/flow"
    17  	"github.com/koko1123/flow-go-1/model/messages"
    18  	realModule "github.com/koko1123/flow-go-1/module"
    19  	real "github.com/koko1123/flow-go-1/module/buffer"
    20  	"github.com/koko1123/flow-go-1/module/compliance"
    21  	"github.com/koko1123/flow-go-1/module/metrics"
    22  	module "github.com/koko1123/flow-go-1/module/mock"
    23  	"github.com/koko1123/flow-go-1/module/trace"
    24  	netint "github.com/koko1123/flow-go-1/network"
    25  	"github.com/koko1123/flow-go-1/network/channels"
    26  	"github.com/koko1123/flow-go-1/network/mocknetwork"
    27  	protint "github.com/koko1123/flow-go-1/state/protocol"
    28  	protocol "github.com/koko1123/flow-go-1/state/protocol/mock"
    29  	storerr "github.com/koko1123/flow-go-1/storage"
    30  	storage "github.com/koko1123/flow-go-1/storage/mock"
    31  	"github.com/koko1123/flow-go-1/utils/unittest"
    32  )
    33  
    34  func TestComplianceCore(t *testing.T) {
    35  	suite.Run(t, new(ComplianceCoreSuite))
    36  }
    37  
    38  type ComplianceCoreSuite struct {
    39  	suite.Suite
    40  
    41  	// engine parameters
    42  	participants flow.IdentityList
    43  	myID         flow.Identifier
    44  	head         *flow.Header
    45  
    46  	// storage data
    47  	headerDB   map[flow.Identifier]*flow.Header
    48  	payloadDB  map[flow.Identifier]*flow.Payload
    49  	pendingDB  map[flow.Identifier]flow.Slashable[flow.Block]
    50  	childrenDB map[flow.Identifier][]flow.Slashable[flow.Block]
    51  
    52  	// mocked dependencies
    53  	me             *module.Local
    54  	metrics        *metrics.NoopCollector
    55  	tracer         realModule.Tracer
    56  	cleaner        *storage.Cleaner
    57  	headers        *storage.Headers
    58  	payloads       *storage.Payloads
    59  	state          *protocol.MutableState
    60  	snapshot       *protocol.Snapshot
    61  	con            *mocknetwork.Conduit
    62  	net            *mocknetwork.Network
    63  	prov           *mocknetwork.Engine
    64  	pending        *module.PendingBlockBuffer
    65  	hotstuff       *module.HotStuff
    66  	sync           *module.BlockRequester
    67  	voteAggregator *hotstuff.VoteAggregator
    68  
    69  	// engine under test
    70  	core *Core
    71  }
    72  
    73  func doneChan() <-chan struct{} {
    74  	c := make(chan struct{})
    75  	close(c)
    76  	return c
    77  }
    78  
    79  func (cs *ComplianceCoreSuite) SetupTest() {
    80  	// seed the RNG
    81  	rand.Seed(time.Now().UnixNano())
    82  
    83  	// initialize the paramaters
    84  	cs.participants = unittest.IdentityListFixture(3,
    85  		unittest.WithRole(flow.RoleConsensus),
    86  		unittest.WithWeight(1000),
    87  	)
    88  	cs.myID = cs.participants[0].NodeID
    89  	block := unittest.BlockFixture()
    90  	cs.head = block.Header
    91  
    92  	// initialize the storage data
    93  	cs.headerDB = make(map[flow.Identifier]*flow.Header)
    94  	cs.payloadDB = make(map[flow.Identifier]*flow.Payload)
    95  	cs.pendingDB = make(map[flow.Identifier]flow.Slashable[flow.Block])
    96  	cs.childrenDB = make(map[flow.Identifier][]flow.Slashable[flow.Block])
    97  
    98  	// store the head header and payload
    99  	cs.headerDB[block.ID()] = block.Header
   100  	cs.payloadDB[block.ID()] = block.Payload
   101  
   102  	// set up local module mock
   103  	cs.me = &module.Local{}
   104  	cs.me.On("NodeID").Return(
   105  		func() flow.Identifier {
   106  			return cs.myID
   107  		},
   108  	)
   109  
   110  	// set up storage cleaner
   111  	cs.cleaner = &storage.Cleaner{}
   112  	cs.cleaner.On("RunGC").Return()
   113  
   114  	// set up header storage mock
   115  	cs.headers = &storage.Headers{}
   116  	cs.headers.On("Store", mock.Anything).Return(
   117  		func(header *flow.Header) error {
   118  			cs.headerDB[header.ID()] = header
   119  			return nil
   120  		},
   121  	)
   122  	cs.headers.On("ByBlockID", mock.Anything).Return(
   123  		func(blockID flow.Identifier) *flow.Header {
   124  			return cs.headerDB[blockID]
   125  		},
   126  		func(blockID flow.Identifier) error {
   127  			_, exists := cs.headerDB[blockID]
   128  			if !exists {
   129  				return storerr.ErrNotFound
   130  			}
   131  			return nil
   132  		},
   133  	)
   134  
   135  	// set up payload storage mock
   136  	cs.payloads = &storage.Payloads{}
   137  	cs.payloads.On("Store", mock.Anything, mock.Anything).Return(
   138  		func(header *flow.Header, payload *flow.Payload) error {
   139  			cs.payloadDB[header.ID()] = payload
   140  			return nil
   141  		},
   142  	)
   143  	cs.payloads.On("ByBlockID", mock.Anything).Return(
   144  		func(blockID flow.Identifier) *flow.Payload {
   145  			return cs.payloadDB[blockID]
   146  		},
   147  		func(blockID flow.Identifier) error {
   148  			_, exists := cs.payloadDB[blockID]
   149  			if !exists {
   150  				return storerr.ErrNotFound
   151  			}
   152  			return nil
   153  		},
   154  	)
   155  
   156  	// set up protocol state mock
   157  	cs.state = &protocol.MutableState{}
   158  	cs.state.On("Final").Return(
   159  		func() protint.Snapshot {
   160  			return cs.snapshot
   161  		},
   162  	)
   163  	cs.state.On("AtBlockID", mock.Anything).Return(
   164  		func(blockID flow.Identifier) protint.Snapshot {
   165  			return cs.snapshot
   166  		},
   167  	)
   168  	cs.state.On("Extend", mock.Anything, mock.Anything).Return(nil)
   169  
   170  	// set up protocol snapshot mock
   171  	cs.snapshot = &protocol.Snapshot{}
   172  	cs.snapshot.On("Identities", mock.Anything).Return(
   173  		func(filter flow.IdentityFilter) flow.IdentityList {
   174  			return cs.participants.Filter(filter)
   175  		},
   176  		nil,
   177  	)
   178  	cs.snapshot.On("Head").Return(
   179  		func() *flow.Header {
   180  			return cs.head
   181  		},
   182  		nil,
   183  	)
   184  
   185  	// set up network conduit mock
   186  	cs.con = &mocknetwork.Conduit{}
   187  	cs.con.On("Publish", mock.Anything, mock.Anything).Return(nil)
   188  	cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
   189  	cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
   190  	cs.con.On("Unicast", mock.Anything, mock.Anything).Return(nil)
   191  
   192  	// set up network module mock
   193  	cs.net = &mocknetwork.Network{}
   194  	cs.net.On("Register", mock.Anything, mock.Anything).Return(
   195  		func(channel channels.Channel, engine netint.MessageProcessor) netint.Conduit {
   196  			return cs.con
   197  		},
   198  		nil,
   199  	)
   200  
   201  	// set up the provider engine
   202  	cs.prov = &mocknetwork.Engine{}
   203  	cs.prov.On("SubmitLocal", mock.Anything).Return()
   204  
   205  	// set up pending module mock
   206  	cs.pending = &module.PendingBlockBuffer{}
   207  	cs.pending.On("Add", mock.Anything, mock.Anything).Return(true)
   208  	cs.pending.On("ByID", mock.Anything).Return(
   209  		func(blockID flow.Identifier) flow.Slashable[flow.Block] {
   210  			return cs.pendingDB[blockID]
   211  		},
   212  		func(blockID flow.Identifier) bool {
   213  			_, ok := cs.pendingDB[blockID]
   214  			return ok
   215  		},
   216  	)
   217  	cs.pending.On("ByParentID", mock.Anything).Return(
   218  		func(blockID flow.Identifier) []flow.Slashable[flow.Block] {
   219  			return cs.childrenDB[blockID]
   220  		},
   221  		func(blockID flow.Identifier) bool {
   222  			_, ok := cs.childrenDB[blockID]
   223  			return ok
   224  		},
   225  	)
   226  	cs.pending.On("DropForParent", mock.Anything).Return()
   227  	cs.pending.On("Size").Return(uint(0))
   228  	cs.pending.On("PruneByView", mock.Anything).Return()
   229  
   230  	closed := func() <-chan struct{} {
   231  		channel := make(chan struct{})
   232  		close(channel)
   233  		return channel
   234  	}()
   235  
   236  	// set up hotstuff module mock
   237  	cs.hotstuff = &module.HotStuff{}
   238  
   239  	cs.voteAggregator = &hotstuff.VoteAggregator{}
   240  
   241  	// set up synchronization module mock
   242  	cs.sync = &module.BlockRequester{}
   243  	cs.sync.On("RequestBlock", mock.Anything, mock.Anything).Return(nil)
   244  	cs.sync.On("Done", mock.Anything).Return(closed)
   245  
   246  	// set up no-op metrics mock
   247  	cs.metrics = metrics.NewNoopCollector()
   248  
   249  	// set up no-op tracer
   250  	cs.tracer = trace.NewNoopTracer()
   251  
   252  	// initialize the engine
   253  	e, err := NewCore(
   254  		unittest.Logger(),
   255  		cs.metrics,
   256  		cs.tracer,
   257  		cs.metrics,
   258  		cs.metrics,
   259  		cs.cleaner,
   260  		cs.headers,
   261  		cs.payloads,
   262  		cs.state,
   263  		cs.pending,
   264  		cs.sync,
   265  		cs.voteAggregator,
   266  	)
   267  	require.NoError(cs.T(), err, "engine initialization should pass")
   268  
   269  	cs.core = e
   270  	// assign engine with consensus & synchronization
   271  	cs.core.hotstuff = cs.hotstuff
   272  }
   273  
   274  func (cs *ComplianceCoreSuite) TestOnBlockProposalValidParent() {
   275  
   276  	// create a proposal that directly descends from the latest finalized header
   277  	originID := cs.participants[1].NodeID
   278  	block := unittest.BlockWithParentFixture(cs.head)
   279  	proposal := unittest.ProposalFromBlock(block)
   280  
   281  	// store the data for retrieval
   282  	cs.headerDB[block.Header.ParentID] = cs.head
   283  
   284  	cs.hotstuff.On("SubmitProposal", block.Header, cs.head.View).Return(doneChan())
   285  
   286  	// it should be processed without error
   287  	err := cs.core.OnBlockProposal(originID, proposal, false)
   288  	require.NoError(cs.T(), err, "valid block proposal should pass")
   289  
   290  	// we should extend the state with the header
   291  	cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block)
   292  
   293  	// we should submit the proposal to hotstuff
   294  	cs.hotstuff.AssertExpectations(cs.T())
   295  }
   296  
   297  func (cs *ComplianceCoreSuite) TestOnBlockProposalValidAncestor() {
   298  
   299  	// create a proposal that has two ancestors in the cache
   300  	originID := cs.participants[1].NodeID
   301  	ancestor := unittest.BlockWithParentFixture(cs.head)
   302  	parent := unittest.BlockWithParentFixture(ancestor.Header)
   303  	block := unittest.BlockWithParentFixture(parent.Header)
   304  	proposal := unittest.ProposalFromBlock(block)
   305  
   306  	// store the data for retrieval
   307  	cs.headerDB[parent.ID()] = parent.Header
   308  	cs.headerDB[ancestor.ID()] = ancestor.Header
   309  
   310  	cs.hotstuff.On("SubmitProposal", block.Header, parent.Header.View).Return(doneChan())
   311  
   312  	// it should be processed without error
   313  	err := cs.core.OnBlockProposal(originID, proposal, false)
   314  	require.NoError(cs.T(), err, "valid block proposal should pass")
   315  
   316  	// we should extend the state with the header
   317  	cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block)
   318  
   319  	// we should submit the proposal to hotstuff
   320  	cs.hotstuff.AssertExpectations(cs.T())
   321  }
   322  
   323  func (cs *ComplianceCoreSuite) TestOnBlockProposalSkipProposalThreshold() {
   324  
   325  	// create a proposal which is far enough ahead to be dropped
   326  	originID := cs.participants[1].NodeID
   327  	block := unittest.BlockFixture()
   328  	block.Header.Height = cs.head.Height + compliance.DefaultConfig().SkipNewProposalsThreshold + 1
   329  	proposal := unittest.ProposalFromBlock(&block)
   330  
   331  	err := cs.core.OnBlockProposal(originID, proposal, false)
   332  	require.NoError(cs.T(), err)
   333  
   334  	// block should be dropped - not added to state or cache
   335  	cs.state.AssertNotCalled(cs.T(), "Extend", mock.Anything)
   336  	cs.pending.AssertNotCalled(cs.T(), "Add", originID, mock.Anything)
   337  }
   338  
   339  func (cs *ComplianceCoreSuite) TestOnBlockProposalInvalidExtension() {
   340  
   341  	// create a proposal that has two ancestors in the cache
   342  	originID := cs.participants[1].NodeID
   343  	ancestor := unittest.BlockWithParentFixture(cs.head)
   344  	parent := unittest.BlockWithParentFixture(ancestor.Header)
   345  	block := unittest.BlockWithParentFixture(parent.Header)
   346  	proposal := unittest.ProposalFromBlock(block)
   347  
   348  	// store the data for retrieval
   349  	cs.headerDB[parent.ID()] = parent.Header
   350  	cs.headerDB[ancestor.ID()] = ancestor.Header
   351  
   352  	// make sure we fail to extend the state
   353  	*cs.state = protocol.MutableState{}
   354  	cs.state.On("Final").Return(
   355  		func() protint.Snapshot {
   356  			return cs.snapshot
   357  		},
   358  	)
   359  	cs.state.On("Extend", mock.Anything, mock.Anything).Return(errors.New("dummy error"))
   360  
   361  	// it should be processed without error
   362  	err := cs.core.OnBlockProposal(originID, proposal, false)
   363  	require.Error(cs.T(), err, "proposal with invalid extension should fail")
   364  
   365  	// we should extend the state with the header
   366  	cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block)
   367  
   368  	// we should not submit the proposal to hotstuff
   369  	cs.hotstuff.AssertExpectations(cs.T())
   370  }
   371  
   372  func (cs *ComplianceCoreSuite) TestProcessBlockAndDescendants() {
   373  
   374  	// create three children blocks
   375  	parent := unittest.BlockWithParentFixture(cs.head)
   376  	block1 := unittest.BlockWithParentFixture(parent.Header)
   377  	block2 := unittest.BlockWithParentFixture(parent.Header)
   378  	block3 := unittest.BlockWithParentFixture(parent.Header)
   379  
   380  	// create the pending blocks
   381  	pending1 := unittest.AsSlashable(block1)
   382  	pending2 := unittest.AsSlashable(block2)
   383  	pending3 := unittest.AsSlashable(block3)
   384  
   385  	// store the parent on disk
   386  	parentID := parent.ID()
   387  	cs.headerDB[parentID] = parent.Header
   388  
   389  	// store the pending children in the cache
   390  	cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending1)
   391  	cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending2)
   392  	cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending3)
   393  
   394  	cs.hotstuff.On("SubmitProposal", parent.Header, cs.head.View).Return(doneChan()).Once()
   395  	cs.hotstuff.On("SubmitProposal", block1.Header, parent.Header.View).Return(doneChan()).Once()
   396  	cs.hotstuff.On("SubmitProposal", block2.Header, parent.Header.View).Return(doneChan()).Once()
   397  	cs.hotstuff.On("SubmitProposal", block3.Header, parent.Header.View).Return(doneChan()).Once()
   398  
   399  	// execute the connected children handling
   400  	err := cs.core.processBlockAndDescendants(parent, false)
   401  	require.NoError(cs.T(), err, "should pass handling children")
   402  
   403  	// check that we submitted each child to hotstuff
   404  	cs.hotstuff.AssertExpectations(cs.T())
   405  
   406  	// make sure we drop the cache after trying to process
   407  	cs.pending.AssertCalled(cs.T(), "DropForParent", parent.Header.ID())
   408  }
   409  
   410  func (cs *ComplianceCoreSuite) TestOnSubmitVote() {
   411  	// create a vote
   412  	originID := unittest.IdentifierFixture()
   413  	vote := messages.BlockVote{
   414  		BlockID: unittest.IdentifierFixture(),
   415  		View:    rand.Uint64(),
   416  		SigData: unittest.SignatureFixture(),
   417  	}
   418  
   419  	cs.voteAggregator.On("AddVote", &model.Vote{
   420  		View:     vote.View,
   421  		BlockID:  vote.BlockID,
   422  		SignerID: originID,
   423  		SigData:  vote.SigData,
   424  	}).Return()
   425  
   426  	// execute the vote submission
   427  	err := cs.core.OnBlockVote(originID, &vote)
   428  	require.NoError(cs.T(), err, "block vote should pass")
   429  
   430  	// check that submit vote was called with correct parameters
   431  	cs.hotstuff.AssertExpectations(cs.T())
   432  }
   433  
   434  func (cs *ComplianceCoreSuite) TestProposalBufferingOrder() {
   435  
   436  	// create a proposal that we will not submit until the end
   437  	originID := cs.participants[1].NodeID
   438  	block := unittest.BlockWithParentFixture(cs.head)
   439  	missing := unittest.ProposalFromBlock(block)
   440  
   441  	// create a chain of descendants
   442  	var proposals []*messages.BlockProposal
   443  	parent := missing
   444  	for i := 0; i < 3; i++ {
   445  		descendant := unittest.BlockWithParentFixture(&parent.Block.Header)
   446  		proposal := unittest.ProposalFromBlock(descendant)
   447  		proposals = append(proposals, proposal)
   448  		parent = proposal
   449  	}
   450  
   451  	// replace the engine buffer with the real one
   452  	cs.core.pending = real.NewPendingBlocks()
   453  
   454  	// process all of the descendants
   455  	for _, proposal := range proposals {
   456  
   457  		// check that we request the ancestor block each time
   458  		cs.sync.On("RequestBlock", mock.Anything, mock.Anything).Once().Run(
   459  			func(args mock.Arguments) {
   460  				ancestorID := args.Get(0).(flow.Identifier)
   461  				assert.Equal(cs.T(), missing.Block.Header.ID(), ancestorID, "should always request root block")
   462  			},
   463  		)
   464  
   465  		// process and make sure no error occurs (as they are unverifiable)
   466  		err := cs.core.OnBlockProposal(originID, proposal, false)
   467  		require.NoError(cs.T(), err, "proposal buffering should pass")
   468  
   469  		// make sure no block is forwarded to hotstuff
   470  		cs.hotstuff.AssertExpectations(cs.T())
   471  	}
   472  
   473  	// check that we submit ech proposal in order
   474  	*cs.hotstuff = module.HotStuff{}
   475  	index := 0
   476  	order := []flow.Identifier{
   477  		missing.Block.Header.ID(),
   478  		proposals[0].Block.Header.ID(),
   479  		proposals[1].Block.Header.ID(),
   480  		proposals[2].Block.Header.ID(),
   481  	}
   482  	cs.hotstuff.On("SubmitProposal", mock.Anything, mock.Anything).Times(4).Run(
   483  		func(args mock.Arguments) {
   484  			header := args.Get(0).(*flow.Header)
   485  			assert.Equal(cs.T(), order[index], header.ID(), "should submit correct header to hotstuff")
   486  			index++
   487  			cs.headerDB[header.ID()] = header
   488  		},
   489  	).Return(doneChan())
   490  
   491  	// process the root proposal
   492  	err := cs.core.OnBlockProposal(originID, missing, false)
   493  	require.NoError(cs.T(), err, "root proposal should pass")
   494  
   495  	// make sure we submitted all four proposals
   496  	cs.hotstuff.AssertExpectations(cs.T())
   497  }