github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/dkg/broker_test.go (about) 1 package dkg 2 3 import ( 4 "fmt" 5 "os" 6 "testing" 7 "time" 8 9 "github.com/rs/zerolog" 10 "github.com/stretchr/testify/assert" 11 mocks "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 14 "github.com/onflow/flow-go/model/flow" 15 msg "github.com/onflow/flow-go/model/messages" 16 "github.com/onflow/flow-go/module" 17 "github.com/onflow/flow-go/module/local" 18 "github.com/onflow/flow-go/module/mock" 19 "github.com/onflow/flow-go/utils/unittest" 20 ) 21 22 // variables that are used throughout the tests 23 var ( 24 orig = 0 // message sender 25 dest = 1 // message destination 26 msgb = []byte("hello world") // message content 27 dkgInstanceID = "flow-testnet-42" // dkg instance identifier 28 ) 29 30 func initCommittee(n int) (identities flow.IdentitySkeletonList, locals []module.Local) { 31 privateStakingKeys := unittest.StakingKeys(n) 32 for i, key := range privateStakingKeys { 33 id := unittest.IdentityFixture(unittest.WithStakingPubKey(key.PublicKey())) 34 identities = append(identities, &id.IdentitySkeleton) 35 local, _ := local.New(id.IdentitySkeleton, privateStakingKeys[i]) 36 locals = append(locals, local) 37 } 38 return identities, locals 39 } 40 41 // TestDefaultConfig checks the default config is reasonable given expected real 42 // network timing and conditions. If this test fails, re-evaluate defaults with 43 // current network conditions. 44 // 45 // NOTE: This assumes exponential backoff 46 func TestDefaultConfig(t *testing.T) { 47 48 phase1Views := 2000 // present configuration for all networks 49 viewsPerSecMainnet := 0.8 // observation from Feb 16 2022 50 phase1LenMainnet := time.Duration(float64(phase1Views)/viewsPerSecMainnet) * time.Second 51 52 conf := DefaultBrokerConfig() 53 // cumulative max delay is sum of delays 54 // 1+2+4+8...+2^n = (2^{n+1}-1) 55 maxDelay := conf.RetryInitialWait<<(conf.PublishMaxRetries+1) - time.Second 56 t.Run("all retries occur within phase 1", func(t *testing.T) { 57 assert.Less(t, maxDelay, phase1LenMainnet) 58 }) 59 60 t.Run("last possible retry is after mid-point of phase 1", func(t *testing.T) { 61 assert.Greater(t, maxDelay, phase1LenMainnet/2) 62 }) 63 } 64 65 // TestPrivateSend_Valid checks that the broker correctly converts the message 66 // destination parameter (index in committee list) to the corresponding 67 // public Identifier, and successfully sends a DKG message to the intended 68 // recipient through the tunnel. 69 func TestPrivateSend_Valid(t *testing.T) { 70 committee, locals := initCommittee(2) 71 72 // sender broker 73 sender := NewBroker( 74 zerolog.Logger{}, 75 dkgInstanceID, 76 committee, 77 locals[orig], 78 orig, 79 []module.DKGContractClient{&mock.DKGContractClient{}}, 80 NewBrokerTunnel(), 81 ) 82 83 // expected DKGMessageOut 84 expectedMsg := msg.PrivDKGMessageOut{ 85 DKGMessage: msg.NewDKGMessage( 86 msgb, 87 dkgInstanceID, 88 ), 89 DestID: committee[dest].NodeID, 90 } 91 92 // launch a background routine to capture messages sent through the tunnel, 93 // and require that the expected message is sent withing 1 second. 94 doneCh := make(chan struct{}) 95 go func() { 96 msg := <-sender.tunnel.MsgChOut 97 require.Equal(t, expectedMsg, msg) 98 close(doneCh) 99 100 }() 101 102 sender.PrivateSend(dest, msgb) 103 104 unittest.RequireCloseBefore(t, doneCh, 50*time.Millisecond, "message not sent") 105 } 106 107 // TestPrivateSend_IndexOutOfRange checks that PrivateSend discards messages if 108 // the message destination parameter is out of range with respect to the 109 // committee list. 110 func TestPrivateSend_IndexOutOfRange(t *testing.T) { 111 committee, locals := initCommittee(2) 112 113 // sender broker 114 sender := NewBroker( 115 zerolog.Logger{}, 116 dkgInstanceID, 117 committee, 118 locals[orig], 119 orig, 120 []module.DKGContractClient{&mock.DKGContractClient{}}, 121 NewBrokerTunnel(), 122 ) 123 124 // Launch a background routine to capture messages sent through the tunnel. 125 // No messages should be received because we are only sending invalid ones. 126 doneCh := make(chan struct{}) 127 go func() { 128 for { 129 <-sender.tunnel.MsgChOut 130 close(doneCh) 131 } 132 }() 133 134 // try providing destination indexes that are out of range 135 sender.PrivateSend(2, msgb) 136 sender.PrivateSend(-1, msgb) 137 138 unittest.RequireNeverClosedWithin(t, doneCh, 50*time.Millisecond, "no invalid message should be sent") 139 } 140 141 // TestReceivePrivateMessage_Valid checks that a valid incoming DKG message is 142 // correctly matched with origin's Identifier, and that the message is forwarded 143 // to the message channel. 144 func TestReceivePrivateMessage_Valid(t *testing.T) { 145 committee, locals := initCommittee(2) 146 147 // receiving broker 148 receiver := NewBroker( 149 zerolog.Logger{}, 150 dkgInstanceID, 151 committee, 152 locals[dest], 153 dest, 154 []module.DKGContractClient{&mock.DKGContractClient{}}, 155 NewBrokerTunnel(), 156 ) 157 158 dkgMessage := msg.NewDKGMessage(msgb, dkgInstanceID) 159 expectedMsg := msg.PrivDKGMessageIn{ 160 OriginID: committee[0].NodeID, 161 DKGMessage: dkgMessage, 162 CommitteeMemberIndex: uint64(orig), 163 } 164 165 // launch a background routine to capture messages forwared to the private 166 // message channel 167 doneCh := make(chan struct{}) 168 go func() { 169 msgCh := receiver.GetPrivateMsgCh() 170 for { 171 msg := <-msgCh 172 require.Equal(t, expectedMsg, msg) 173 close(doneCh) 174 } 175 }() 176 177 // simulate receiving an incoming message through the broker 178 receiver.tunnel.SendIn( 179 msg.PrivDKGMessageIn{ 180 DKGMessage: dkgMessage, 181 OriginID: committee[orig].NodeID, 182 }, 183 ) 184 185 unittest.RequireCloseBefore(t, doneCh, 50*time.Millisecond, "message not received") 186 } 187 188 // TestBroadcastMessage checks that the broker correctly wraps the message 189 // data in a DKGMessage (with origin and epochCounter), and that it calls the 190 // dkg contract client. 191 func TestBroadcastMessage(t *testing.T) { 192 committee, locals := initCommittee(2) 193 194 // sender 195 sender := NewBroker( 196 unittest.Logger(), 197 dkgInstanceID, 198 committee, 199 locals[orig], 200 orig, 201 []module.DKGContractClient{&mock.DKGContractClient{}, &mock.DKGContractClient{}}, 202 NewBrokerTunnel(), 203 func(config *BrokerConfig) { config.RetryInitialWait = 1 }, // disable waiting between retries for tests 204 ) 205 206 expectedMsg, err := sender.prepareBroadcastMessage(msgb) 207 require.NoError(t, err) 208 209 done := make(chan struct{}) // will be closed after final expected call 210 211 // check that the dkg contract client is called with the expected message 212 contractClient := &mock.DKGContractClient{} 213 contractClient.On("Broadcast", expectedMsg). 214 Return(fmt.Errorf("error")). 215 Twice() 216 sender.dkgContractClients[0] = contractClient 217 218 contractClient2 := &mock.DKGContractClient{} 219 contractClient2.On("Broadcast", expectedMsg). 220 Run(func(_ mocks.Arguments) { 221 close(done) 222 }). 223 Return(nil). 224 Once() 225 sender.dkgContractClients[1] = contractClient2 226 227 sender.Broadcast(msgb) 228 unittest.AssertClosesBefore(t, done, time.Second) 229 230 contractClient.AssertExpectations(t) 231 contractClient2.AssertExpectations(t) 232 } 233 234 // TestPoll checks that the broker correctly calls the smart contract to fetch 235 // broadcast messages, and forwards the messages to the broadcast channel. 236 func TestPoll(t *testing.T) { 237 committee, locals := initCommittee(2) 238 239 sender := NewBroker( 240 zerolog.Logger{}, 241 dkgInstanceID, 242 committee, 243 locals[orig], 244 orig, 245 []module.DKGContractClient{&mock.DKGContractClient{}}, 246 NewBrokerTunnel(), 247 ) 248 249 recipient := NewBroker( 250 zerolog.Logger{}, 251 dkgInstanceID, 252 committee, 253 locals[dest], 254 dest, 255 []module.DKGContractClient{&mock.DKGContractClient{}}, 256 NewBrokerTunnel(), 257 ) 258 259 blockID := unittest.IdentifierFixture() 260 bcastMsgs := []msg.BroadcastDKGMessage{} 261 for i := 0; i < 3; i++ { 262 bmsg, err := sender.prepareBroadcastMessage([]byte(fmt.Sprintf("msg%d", i))) 263 require.NoError(t, err) 264 bmsg.NodeID = committee[0].NodeID 265 bcastMsgs = append(bcastMsgs, bmsg) 266 } 267 268 // check that the dkg contract client is called correctly 269 contractClient := &mock.DKGContractClient{} 270 contractClient.On("ReadBroadcast", recipient.messageOffset, blockID). 271 Return(bcastMsgs, nil). 272 Once() 273 sender.dkgContractClients[0] = contractClient 274 275 // launch a background routine to capture messages forwarded to the msgCh 276 receivedMsgs := []msg.BroadcastDKGMessage{} 277 doneCh := make(chan struct{}) 278 go func() { 279 msgCh := sender.GetBroadcastMsgCh() 280 for { 281 msg := <-msgCh 282 receivedMsgs = append(receivedMsgs, msg) 283 if len(receivedMsgs) == len(bcastMsgs) { 284 close(doneCh) 285 } 286 } 287 }() 288 289 err := sender.Poll(blockID) 290 require.NoError(t, err) 291 292 // check that the contract has been correctly called 293 contractClient.AssertExpectations(t) 294 295 // check that the messages have been received and forwarded to the msgCh 296 unittest.AssertClosesBefore(t, doneCh, time.Second) 297 require.Equal(t, bcastMsgs, receivedMsgs) 298 299 // check that the message offset has been incremented 300 require.Equal(t, uint(len(bcastMsgs)), sender.messageOffset) 301 } 302 303 // TestLogHook checks that the Disqualify and FlagMisbehaviour functions call a 304 // Warn log, and that we can hook a logger to react to such logs. 305 func TestLogHook(t *testing.T) { 306 committee, locals := initCommittee(2) 307 308 hookCalls := 0 309 310 hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { 311 if level == zerolog.WarnLevel { 312 hookCalls++ 313 } 314 }) 315 logger := zerolog.New(os.Stdout).Level(zerolog.WarnLevel).Hook(hook) 316 317 // sender 318 sender := NewBroker( 319 logger, 320 dkgInstanceID, 321 committee, 322 locals[orig], 323 orig, 324 []module.DKGContractClient{&mock.DKGContractClient{}}, 325 NewBrokerTunnel(), 326 ) 327 328 sender.Disqualify(1, "testing") 329 sender.FlagMisbehavior(1, "test") 330 require.Equal(t, 2, hookCalls) 331 } 332 333 // TestProcessPrivateMessage_InvalidOrigin checks that incoming DKG messages are 334 // discarded if the sender is not part of the DKG committee. 335 func TestProcessPrivateMessage_InvalidOrigin(t *testing.T) { 336 committee, locals := initCommittee(2) 337 338 // receiving broker 339 receiver := NewBroker( 340 zerolog.Logger{}, 341 dkgInstanceID, 342 committee, 343 locals[dest], 344 dest, 345 []module.DKGContractClient{&mock.DKGContractClient{}}, 346 NewBrokerTunnel(), 347 ) 348 349 // Launch a background routine to capture messages forwared to the private 350 // message channel. No messages should be received because we are only 351 // sending invalid ones. 352 doneCh := make(chan struct{}) 353 go func() { 354 msgCh := receiver.GetPrivateMsgCh() 355 for { 356 <-msgCh 357 close(doneCh) 358 } 359 }() 360 361 dkgMsg := msg.NewDKGMessage( 362 msgb, 363 dkgInstanceID, 364 ) 365 // simulate receiving an incoming message with an OriginID of a non-committee member 366 receiver.tunnel.SendIn( 367 msg.PrivDKGMessageIn{ 368 DKGMessage: dkgMsg, 369 OriginID: unittest.IdentifierFixture(), 370 }, 371 ) 372 373 unittest.RequireNeverClosedWithin(t, doneCh, 50*time.Millisecond, "no invalid incoming message should be forwarded") 374 }