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 }