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  }