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 }