github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/collection/compliance/engine_test.go (about)

     1  package compliance
     2  
     3  import (
     4  	"context"
     5  	"sync"
     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  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    15  	"github.com/onflow/flow-go/model/cluster"
    16  	"github.com/onflow/flow-go/model/flow"
    17  	"github.com/onflow/flow-go/model/messages"
    18  	"github.com/onflow/flow-go/module/irrecoverable"
    19  	module "github.com/onflow/flow-go/module/mock"
    20  	netint "github.com/onflow/flow-go/network"
    21  	"github.com/onflow/flow-go/network/channels"
    22  	"github.com/onflow/flow-go/network/mocknetwork"
    23  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    24  	storerr "github.com/onflow/flow-go/storage"
    25  	storage "github.com/onflow/flow-go/storage/mock"
    26  	"github.com/onflow/flow-go/utils/unittest"
    27  )
    28  
    29  func TestComplianceEngine(t *testing.T) {
    30  	suite.Run(t, new(EngineSuite))
    31  }
    32  
    33  type EngineSuite struct {
    34  	CommonSuite
    35  
    36  	clusterID  flow.ChainID
    37  	myID       flow.Identifier
    38  	cluster    flow.IdentityList
    39  	me         *module.Local
    40  	net        *mocknetwork.Network
    41  	payloads   *storage.ClusterPayloads
    42  	protoState *protocol.State
    43  	con        *mocknetwork.Conduit
    44  
    45  	payloadDB map[flow.Identifier]*cluster.Payload
    46  
    47  	ctx    irrecoverable.SignalerContext
    48  	cancel context.CancelFunc
    49  	errs   <-chan error
    50  
    51  	engine *Engine
    52  }
    53  
    54  func (cs *EngineSuite) SetupTest() {
    55  	cs.CommonSuite.SetupTest()
    56  
    57  	// initialize the parameters
    58  	cs.cluster = unittest.IdentityListFixture(3,
    59  		unittest.WithRole(flow.RoleCollection),
    60  		unittest.WithInitialWeight(1000),
    61  	)
    62  	cs.myID = cs.cluster[0].NodeID
    63  
    64  	protoEpoch := &protocol.Epoch{}
    65  	clusters := flow.ClusterList{cs.cluster.ToSkeleton()}
    66  	protoEpoch.On("Clustering").Return(clusters, nil)
    67  
    68  	protoQuery := &protocol.EpochQuery{}
    69  	protoQuery.On("Current").Return(protoEpoch)
    70  
    71  	protoSnapshot := &protocol.Snapshot{}
    72  	protoSnapshot.On("Epochs").Return(protoQuery)
    73  	protoSnapshot.On("Identities", mock.Anything).Return(
    74  		func(selector flow.IdentityFilter[flow.Identity]) flow.IdentityList {
    75  			return cs.cluster.Filter(selector)
    76  		},
    77  		nil,
    78  	)
    79  
    80  	cs.protoState = &protocol.State{}
    81  	cs.protoState.On("Final").Return(protoSnapshot)
    82  
    83  	cs.clusterID = "cluster-id"
    84  	clusterParams := &protocol.Params{}
    85  	clusterParams.On("ChainID").Return(cs.clusterID, nil)
    86  
    87  	cs.state.On("Params").Return(clusterParams)
    88  
    89  	// set up local module mock
    90  	cs.me = &module.Local{}
    91  	cs.me.On("NodeID").Return(
    92  		func() flow.Identifier {
    93  			return cs.myID
    94  		},
    95  	)
    96  
    97  	cs.payloadDB = make(map[flow.Identifier]*cluster.Payload)
    98  
    99  	// set up payload storage mock
   100  	cs.payloads = &storage.ClusterPayloads{}
   101  	cs.payloads.On("Store", mock.Anything, mock.Anything).Return(
   102  		func(blockID flow.Identifier, payload *cluster.Payload) error {
   103  			cs.payloadDB[blockID] = payload
   104  			return nil
   105  		},
   106  	)
   107  	cs.payloads.On("ByBlockID", mock.Anything).Return(
   108  		func(blockID flow.Identifier) *cluster.Payload {
   109  			return cs.payloadDB[blockID]
   110  		},
   111  		func(blockID flow.Identifier) error {
   112  			_, exists := cs.payloadDB[blockID]
   113  			if !exists {
   114  				return storerr.ErrNotFound
   115  			}
   116  			return nil
   117  		},
   118  	)
   119  
   120  	// set up network conduit mock
   121  	cs.con = &mocknetwork.Conduit{}
   122  	cs.con.On("Publish", mock.Anything, mock.Anything).Return(nil)
   123  	cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
   124  	cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
   125  	cs.con.On("Unicast", mock.Anything, mock.Anything).Return(nil)
   126  
   127  	// set up network module mock
   128  	cs.net = &mocknetwork.Network{}
   129  	cs.net.On("Register", mock.Anything, mock.Anything).Return(
   130  		func(channel channels.Channel, engine netint.MessageProcessor) netint.Conduit {
   131  			return cs.con
   132  		},
   133  		nil,
   134  	)
   135  
   136  	e, err := NewEngine(unittest.Logger(), cs.me, cs.protoState, cs.payloads, cs.core)
   137  	require.NoError(cs.T(), err)
   138  	cs.engine = e
   139  
   140  	cs.ctx, cs.cancel, cs.errs = irrecoverable.WithSignallerAndCancel(context.Background())
   141  	cs.engine.Start(cs.ctx)
   142  	go unittest.FailOnIrrecoverableError(cs.T(), cs.ctx.Done(), cs.errs)
   143  
   144  	unittest.AssertClosesBefore(cs.T(), cs.engine.Ready(), time.Second)
   145  }
   146  
   147  // TearDownTest stops the engine and checks there are no errors thrown to the SignallerContext.
   148  func (cs *EngineSuite) TearDownTest() {
   149  	cs.cancel()
   150  	unittest.RequireCloseBefore(cs.T(), cs.engine.Done(), time.Second, "engine failed to stop")
   151  	select {
   152  	case err := <-cs.errs:
   153  		assert.NoError(cs.T(), err)
   154  	default:
   155  	}
   156  }
   157  
   158  // TestSubmittingMultipleVotes tests that we can send multiple votes and they
   159  // are queued and processed in expected way
   160  func (cs *EngineSuite) TestSubmittingMultipleEntries() {
   161  	blockCount := 15
   162  	var wg sync.WaitGroup
   163  	wg.Add(1)
   164  	go func() {
   165  		for i := 0; i < blockCount; i++ {
   166  			block := unittest.ClusterBlockWithParent(cs.head)
   167  			proposal := messages.NewClusterBlockProposal(&block)
   168  			hotstuffProposal := model.ProposalFromFlow(block.Header)
   169  			cs.hotstuff.On("SubmitProposal", hotstuffProposal).Return().Once()
   170  			cs.voteAggregator.On("AddBlock", hotstuffProposal).Once()
   171  			cs.validator.On("ValidateProposal", hotstuffProposal).Return(nil).Once()
   172  			// execute the block submission
   173  			cs.engine.OnClusterBlockProposal(flow.Slashable[*messages.ClusterBlockProposal]{
   174  				OriginID: unittest.IdentifierFixture(),
   175  				Message:  proposal,
   176  			})
   177  		}
   178  		wg.Done()
   179  	}()
   180  	wg.Add(1)
   181  	go func() {
   182  		// create a proposal that directly descends from the latest finalized header
   183  		block := unittest.ClusterBlockWithParent(cs.head)
   184  		proposal := messages.NewClusterBlockProposal(&block)
   185  
   186  		hotstuffProposal := model.ProposalFromFlow(block.Header)
   187  		cs.hotstuff.On("SubmitProposal", hotstuffProposal).Once()
   188  		cs.voteAggregator.On("AddBlock", hotstuffProposal).Once()
   189  		cs.validator.On("ValidateProposal", hotstuffProposal).Return(nil).Once()
   190  		cs.engine.OnClusterBlockProposal(flow.Slashable[*messages.ClusterBlockProposal]{
   191  			OriginID: unittest.IdentifierFixture(),
   192  			Message:  proposal,
   193  		})
   194  		wg.Done()
   195  	}()
   196  
   197  	wg.Wait()
   198  
   199  	// wait for the votes queue to drain
   200  	assert.Eventually(cs.T(), func() bool {
   201  		return cs.engine.pendingBlocks.Len() == 0
   202  	}, time.Second, time.Millisecond*10)
   203  }
   204  
   205  // TestOnFinalizedBlock tests if finalized block gets processed when send through `Engine`.
   206  // Tests the whole processing pipeline.
   207  func (cs *EngineSuite) TestOnFinalizedBlock() {
   208  	finalizedBlock := unittest.ClusterBlockFixture()
   209  	cs.head = &finalizedBlock
   210  	cs.headerDB[finalizedBlock.ID()] = &finalizedBlock
   211  
   212  	*cs.pending = module.PendingClusterBlockBuffer{}
   213  	// wait for both expected calls before ending the test
   214  	wg := new(sync.WaitGroup)
   215  	wg.Add(2)
   216  	cs.pending.On("PruneByView", finalizedBlock.Header.View).
   217  		Run(func(_ mock.Arguments) { wg.Done() }).
   218  		Return(nil).Once()
   219  	cs.pending.On("Size").
   220  		Run(func(_ mock.Arguments) { wg.Done() }).
   221  		Return(uint(0)).Once()
   222  
   223  	err := cs.engine.processOnFinalizedBlock(model.BlockFromFlow(finalizedBlock.Header))
   224  	require.NoError(cs.T(), err)
   225  	unittest.AssertReturnsBefore(cs.T(), wg.Wait, time.Second, "an expected call to block buffer wasn't made")
   226  }