github.com/onflow/flow-go@v0.33.17/consensus/integration/epoch_test.go (about)

     1  package integration_test
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/onflow/flow-go/cmd/bootstrap/run"
    11  	"github.com/onflow/flow-go/model/encodable"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/model/flow/filter"
    14  	"github.com/onflow/flow-go/model/flow/mapfunc"
    15  	"github.com/onflow/flow-go/state/protocol/inmem"
    16  	"github.com/onflow/flow-go/utils/unittest"
    17  )
    18  
    19  // should be able to reach consensus when identity table contains nodes with 0 weight.
    20  func TestUnweightedNode(t *testing.T) {
    21  	// stop after building 2 blocks to ensure we can tolerate 0-weight (joining next
    22  	// epoch) identities, but don't cross an epoch boundary
    23  	// stop after building 2 blocks to ensure we can tolerate 0-weight (joining next
    24  	// epoch) identities, but don't cross an epoch boundary
    25  	stopper := NewStopper(2, 0)
    26  	participantsData := createConsensusIdentities(t, 3)
    27  	rootSnapshot := createRootSnapshot(t, participantsData)
    28  	consensusParticipants := NewConsensusParticipants(participantsData)
    29  
    30  	// add a consensus node to next epoch (it will have 0 weight in the current epoch)
    31  	nextEpochParticipantsData := createConsensusIdentities(t, 1)
    32  	// epoch 2 identities includes:
    33  	// * same collection node from epoch 1, so cluster QCs are consistent
    34  	// * 1 new consensus node, joining at epoch 2
    35  	// * random nodes with other roles
    36  	nextEpochIdentities := unittest.CompleteIdentitySet(
    37  		append(
    38  			rootSnapshot.Encodable().Identities.Filter(filter.HasRole(flow.RoleCollection)),
    39  			nextEpochParticipantsData.Identities()...)...,
    40  	)
    41  	rootSnapshot = withNextEpoch(
    42  		rootSnapshot,
    43  		nextEpochIdentities,
    44  		nextEpochParticipantsData,
    45  		consensusParticipants,
    46  		10_000,
    47  	)
    48  
    49  	nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper)
    50  
    51  	hub.WithFilter(blockNothing)
    52  
    53  	runFor(60 * time.Second)
    54  
    55  	allViews := allFinalizedViews(t, nodes)
    56  	assertSafety(t, allViews)
    57  
    58  	cleanupNodes(nodes)
    59  }
    60  
    61  // test consensus across an epoch boundary, where both epochs have the same identity table.
    62  func TestStaticEpochTransition(t *testing.T) {
    63  	// must finalize 8 blocks, we specify the epoch transition after 4 views
    64  	stopper := NewStopper(8, 0)
    65  	participantsData := createConsensusIdentities(t, 3)
    66  	rootSnapshot := createRootSnapshot(t, participantsData)
    67  	consensusParticipants := NewConsensusParticipants(participantsData)
    68  
    69  	firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter()
    70  	require.NoError(t, err)
    71  
    72  	// set up next epoch beginning in 4 views, with same identities as first epoch
    73  	nextEpochIdentities, err := rootSnapshot.Identities(filter.Any)
    74  	require.NoError(t, err)
    75  	rootSnapshot = withNextEpoch(
    76  		rootSnapshot,
    77  		nextEpochIdentities,
    78  		participantsData,
    79  		consensusParticipants,
    80  		4,
    81  	)
    82  
    83  	nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper)
    84  
    85  	hub.WithFilter(blockNothing)
    86  
    87  	runFor(30 * time.Second)
    88  
    89  	allViews := allFinalizedViews(t, nodes)
    90  	assertSafety(t, allViews)
    91  
    92  	// confirm that we have transitioned to the new epoch
    93  	pstate := nodes[0].state
    94  	afterCounter, err := pstate.Final().Epochs().Current().Counter()
    95  	require.NoError(t, err)
    96  	assert.Equal(t, firstEpochCounter+1, afterCounter)
    97  
    98  	cleanupNodes(nodes)
    99  }
   100  
   101  // test consensus across an epoch boundary, where the identity table changes
   102  // but the new epoch overlaps with the previous epoch.
   103  func TestEpochTransition_IdentitiesOverlap(t *testing.T) {
   104  	// must finalize 8 blocks, we specify the epoch transition after 4 views
   105  	stopper := NewStopper(8, 0)
   106  	privateNodeInfos := createPrivateNodeIdentities(4)
   107  	firstEpochConsensusParticipants := completeConsensusIdentities(t, privateNodeInfos[:3])
   108  	rootSnapshot := createRootSnapshot(t, firstEpochConsensusParticipants)
   109  	consensusParticipants := NewConsensusParticipants(firstEpochConsensusParticipants)
   110  
   111  	firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter()
   112  	require.NoError(t, err)
   113  
   114  	// set up next epoch with 1 new consensus nodes and 2 consensus nodes from first epoch
   115  	// 1 consensus node is removed after the first epoch
   116  	firstEpochIdentities, err := rootSnapshot.Identities(filter.Any)
   117  	require.NoError(t, err)
   118  
   119  	removedIdentity := privateNodeInfos[0].Identity()
   120  	newIdentity := privateNodeInfos[3].Identity()
   121  	nextEpochIdentities := append(
   122  		firstEpochIdentities.Filter(filter.Not(filter.HasNodeID(removedIdentity.NodeID))),
   123  		newIdentity,
   124  	)
   125  
   126  	// generate new identities for next epoch, it will generate new DKG keys for random beacon participants
   127  	nextEpochParticipantData := completeConsensusIdentities(t, privateNodeInfos[1:])
   128  	rootSnapshot = withNextEpoch(
   129  		rootSnapshot,
   130  		nextEpochIdentities,
   131  		nextEpochParticipantData,
   132  		consensusParticipants,
   133  		4,
   134  	)
   135  
   136  	nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper)
   137  
   138  	hub.WithFilter(blockNothing)
   139  
   140  	runFor(30 * time.Second)
   141  
   142  	allViews := allFinalizedViews(t, nodes)
   143  	assertSafety(t, allViews)
   144  
   145  	// confirm that we have transitioned to the new epoch
   146  	pstate := nodes[0].state
   147  	afterCounter, err := pstate.Final().Epochs().Current().Counter()
   148  	require.NoError(t, err)
   149  	assert.Equal(t, firstEpochCounter+1, afterCounter)
   150  
   151  	cleanupNodes(nodes)
   152  }
   153  
   154  // test consensus across an epoch boundary, where the identity table in the new
   155  // epoch is disjoint from the identity table in the first epoch.
   156  func TestEpochTransition_IdentitiesDisjoint(t *testing.T) {
   157  	// must finalize 8 blocks, we specify the epoch transition after 4 views
   158  	stopper := NewStopper(8, 0)
   159  	firstEpochConsensusParticipants := createConsensusIdentities(t, 3)
   160  	rootSnapshot := createRootSnapshot(t, firstEpochConsensusParticipants)
   161  	consensusParticipants := NewConsensusParticipants(firstEpochConsensusParticipants)
   162  
   163  	firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter()
   164  	require.NoError(t, err)
   165  
   166  	// prepare a next epoch with a completely different consensus committee
   167  	// (no overlapping consensus nodes)
   168  	firstEpochIdentities, err := rootSnapshot.Identities(filter.Any)
   169  	require.NoError(t, err)
   170  
   171  	nextEpochParticipantData := createConsensusIdentities(t, 3)
   172  	nextEpochIdentities := append(
   173  		firstEpochIdentities.Filter(filter.Not(filter.HasRole(flow.RoleConsensus))), // remove all consensus nodes
   174  		nextEpochParticipantData.Identities()...,                                    // add new consensus nodes
   175  	)
   176  
   177  	rootSnapshot = withNextEpoch(
   178  		rootSnapshot,
   179  		nextEpochIdentities,
   180  		nextEpochParticipantData,
   181  		consensusParticipants,
   182  		4,
   183  	)
   184  
   185  	nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper)
   186  
   187  	hub.WithFilter(blockNothing)
   188  
   189  	runFor(60 * time.Second)
   190  
   191  	allViews := allFinalizedViews(t, nodes)
   192  	assertSafety(t, allViews)
   193  
   194  	// confirm that we have transitioned to the new epoch
   195  	pstate := nodes[0].state
   196  	afterCounter, err := pstate.Final().Epochs().Current().Counter()
   197  	require.NoError(t, err)
   198  	assert.Equal(t, firstEpochCounter+1, afterCounter)
   199  
   200  	cleanupNodes(nodes)
   201  }
   202  
   203  // withNextEpoch adds a valid next epoch with the given identities to the input
   204  // snapshot. Also sets the length of the first (current) epoch to curEpochViews.
   205  //
   206  // We make the first (current) epoch start in committed phase so we can transition
   207  // to the next epoch upon reaching the appropriate view without any further changes
   208  // to the protocol state.
   209  func withNextEpoch(
   210  	snapshot *inmem.Snapshot,
   211  	nextEpochIdentities flow.IdentityList,
   212  	nextEpochParticipantData *run.ParticipantData,
   213  	participantsCache *ConsensusParticipants,
   214  	curEpochViews uint64,
   215  ) *inmem.Snapshot {
   216  
   217  	// convert to encodable representation for simple modification
   218  	encodableSnapshot := snapshot.Encodable()
   219  
   220  	nextEpochIdentities = nextEpochIdentities.Sort(flow.Canonical)
   221  
   222  	currEpoch := &encodableSnapshot.Epochs.Current                // take pointer so assignments apply
   223  	currEpoch.FinalView = currEpoch.FirstView + curEpochViews - 1 // first epoch lasts curEpochViews
   224  	encodableSnapshot.Epochs.Next = &inmem.EncodableEpoch{
   225  		Counter:           currEpoch.Counter + 1,
   226  		FirstView:         currEpoch.FinalView + 1,
   227  		FinalView:         currEpoch.FinalView + 1 + 10000,
   228  		RandomSource:      unittest.SeedFixture(flow.EpochSetupRandomSourceLength),
   229  		InitialIdentities: nextEpochIdentities,
   230  		// must include info corresponding to EpochCommit event, since we are
   231  		// starting in committed phase
   232  		Clustering: unittest.ClusterList(1, nextEpochIdentities),
   233  		Clusters:   currEpoch.Clusters,
   234  		DKG: &inmem.EncodableDKG{
   235  			GroupKey: encodable.RandomBeaconPubKey{
   236  				PublicKey: nextEpochParticipantData.GroupKey,
   237  			},
   238  			Participants: nextEpochParticipantData.Lookup,
   239  		},
   240  	}
   241  
   242  	participantsCache.Update(encodableSnapshot.Epochs.Next.Counter, nextEpochParticipantData)
   243  
   244  	// we must start the current epoch in committed phase so we can transition to the next epoch
   245  	encodableSnapshot.Phase = flow.EpochPhaseCommitted
   246  	encodableSnapshot.LatestSeal.ResultID = encodableSnapshot.LatestResult.ID()
   247  
   248  	// set identities for root snapshot to include next epoch identities,
   249  	// since we are in committed phase
   250  	encodableSnapshot.Identities = append(
   251  		// all the current epoch identities
   252  		encodableSnapshot.Identities,
   253  		// and all the NEW identities in next epoch, with 0 weight
   254  		nextEpochIdentities.
   255  			Filter(filter.Not(filter.In(encodableSnapshot.Identities))).
   256  			Map(mapfunc.WithWeight(0))...,
   257  	).Sort(flow.Canonical)
   258  
   259  	return inmem.SnapshotFromEncodable(encodableSnapshot)
   260  }