github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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/flow" 12 "github.com/onflow/flow-go/model/flow/filter" 13 "github.com/onflow/flow-go/state/protocol/inmem" 14 "github.com/onflow/flow-go/state/protocol/protocol_state/kvstore" 15 "github.com/onflow/flow-go/utils/unittest" 16 ) 17 18 // should be able to reach consensus when identity table contains nodes which are joining in next epoch. 19 func TestUnweightedNode(t *testing.T) { 20 // stop after building 2 blocks to ensure we can tolerate 0-weight (joining next 21 // epoch) identities, but don't cross an epoch boundary 22 stopper := NewStopper(2, 0) 23 participantsData := createConsensusIdentities(t, 3) 24 rootSnapshot := createRootSnapshot(t, participantsData) 25 consensusParticipants := NewConsensusParticipants(participantsData) 26 27 // add a consensus node to next epoch (it will have `flow.EpochParticipationStatusJoining` status in the current epoch) 28 nextEpochParticipantsData := createConsensusIdentities(t, 1) 29 // epoch 2 identities includes: 30 // * same collection node from epoch 1, so cluster QCs are consistent 31 // * 1 new consensus node, joining at epoch 2 32 // * random nodes with other roles 33 currentEpochCollectionNodes, err := rootSnapshot.Identities(filter.HasRole[flow.Identity](flow.RoleCollection)) 34 require.NoError(t, err) 35 nextEpochIdentities := unittest.CompleteIdentitySet( 36 append( 37 currentEpochCollectionNodes, 38 nextEpochParticipantsData.Identities()...)..., 39 ) 40 rootSnapshot = withNextEpoch(t, rootSnapshot, nextEpochIdentities, nextEpochParticipantsData, consensusParticipants, 10_000, func(block *flow.Block) *flow.QuorumCertificate { 41 return createRootQC(t, block, participantsData) 42 }) 43 encodableSnap := rootSnapshot.Encodable() 44 encodableSnap.QuorumCertificate = createRootQC(t, rootSnapshot.Encodable().SealingSegment.Highest(), participantsData) 45 rootSnapshot = inmem.SnapshotFromEncodable(encodableSnap) 46 47 nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper) 48 49 hub.WithFilter(blockNothing) 50 51 runFor(60 * time.Second) 52 53 allViews := allFinalizedViews(t, nodes) 54 assertSafety(t, allViews) 55 56 cleanupNodes(nodes) 57 } 58 59 // test consensus across an epoch boundary, where both epochs have the same identity table. 60 func TestStaticEpochTransition(t *testing.T) { 61 // must finalize 8 blocks, we specify the epoch transition after 4 views 62 stopper := NewStopper(8, 0) 63 participantsData := createConsensusIdentities(t, 3) 64 rootSnapshot := createRootSnapshot(t, participantsData) 65 consensusParticipants := NewConsensusParticipants(participantsData) 66 67 firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter() 68 require.NoError(t, err) 69 70 // set up next epoch beginning in 4 views, with same identities as first epoch 71 nextEpochIdentities, err := rootSnapshot.Identities(filter.Any) 72 require.NoError(t, err) 73 rootSnapshot = withNextEpoch(t, rootSnapshot, nextEpochIdentities, participantsData, consensusParticipants, 4, func(block *flow.Block) *flow.QuorumCertificate { 74 return createRootQC(t, block, participantsData) 75 }) 76 77 nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper) 78 79 hub.WithFilter(blockNothing) 80 81 runFor(30 * time.Second) 82 83 allViews := allFinalizedViews(t, nodes) 84 assertSafety(t, allViews) 85 86 // confirm that we have transitioned to the new epoch 87 pstate := nodes[0].state 88 afterCounter, err := pstate.Final().Epochs().Current().Counter() 89 require.NoError(t, err) 90 assert.Equal(t, firstEpochCounter+1, afterCounter) 91 92 cleanupNodes(nodes) 93 } 94 95 // test consensus across an epoch boundary, where the identity table changes 96 // but the new epoch overlaps with the previous epoch. 97 func TestEpochTransition_IdentitiesOverlap(t *testing.T) { 98 // must finalize 8 blocks, we specify the epoch transition after 4 views 99 stopper := NewStopper(8, 0) 100 privateNodeInfos := createPrivateNodeIdentities(4) 101 firstEpochConsensusParticipants := completeConsensusIdentities(t, privateNodeInfos[:3]) 102 rootSnapshot := createRootSnapshot(t, firstEpochConsensusParticipants) 103 consensusParticipants := NewConsensusParticipants(firstEpochConsensusParticipants) 104 105 firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter() 106 require.NoError(t, err) 107 108 // set up next epoch with 1 new consensus nodes and 2 consensus nodes from first epoch 109 // 1 consensus node is removed after the first epoch 110 firstEpochIdentities, err := rootSnapshot.Identities(filter.Any) 111 require.NoError(t, err) 112 113 removedIdentity := privateNodeInfos[0].Identity() 114 newIdentity := privateNodeInfos[3].Identity() 115 nextEpochIdentities := append( 116 firstEpochIdentities.Filter(filter.Not(filter.HasNodeID[flow.Identity](removedIdentity.NodeID))), 117 newIdentity, 118 ) 119 120 // generate new identities for next epoch, it will generate new DKG keys for random beacon participants 121 nextEpochParticipantData := completeConsensusIdentities(t, privateNodeInfos[1:]) 122 rootSnapshot = withNextEpoch(t, rootSnapshot, nextEpochIdentities, nextEpochParticipantData, consensusParticipants, 4, func(block *flow.Block) *flow.QuorumCertificate { 123 return createRootQC(t, block, firstEpochConsensusParticipants) 124 }) 125 126 nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper) 127 128 hub.WithFilter(blockNothing) 129 130 runFor(30 * time.Second) 131 132 allViews := allFinalizedViews(t, nodes) 133 assertSafety(t, allViews) 134 135 // confirm that we have transitioned to the new epoch 136 pstate := nodes[0].state 137 afterCounter, err := pstate.Final().Epochs().Current().Counter() 138 require.NoError(t, err) 139 assert.Equal(t, firstEpochCounter+1, afterCounter) 140 141 cleanupNodes(nodes) 142 } 143 144 // test consensus across an epoch boundary, where the identity table in the new 145 // epoch is disjoint from the identity table in the first epoch. 146 func TestEpochTransition_IdentitiesDisjoint(t *testing.T) { 147 // must finalize 8 blocks, we specify the epoch transition after 4 views 148 stopper := NewStopper(8, 0) 149 firstEpochConsensusParticipants := createConsensusIdentities(t, 3) 150 rootSnapshot := createRootSnapshot(t, firstEpochConsensusParticipants) 151 consensusParticipants := NewConsensusParticipants(firstEpochConsensusParticipants) 152 153 firstEpochCounter, err := rootSnapshot.Epochs().Current().Counter() 154 require.NoError(t, err) 155 156 // prepare a next epoch with a completely different consensus committee 157 // (no overlapping consensus nodes) 158 firstEpochIdentities, err := rootSnapshot.Identities(filter.Any) 159 require.NoError(t, err) 160 161 nextEpochParticipantData := createConsensusIdentities(t, 3) 162 nextEpochIdentities := append( 163 firstEpochIdentities.Filter(filter.Not(filter.HasRole[flow.Identity](flow.RoleConsensus))), // remove all consensus nodes 164 nextEpochParticipantData.Identities()..., // add new consensus nodes 165 ) 166 167 rootSnapshot = withNextEpoch(t, rootSnapshot, nextEpochIdentities, nextEpochParticipantData, consensusParticipants, 4, func(block *flow.Block) *flow.QuorumCertificate { 168 return createRootQC(t, block, firstEpochConsensusParticipants) 169 }) 170 171 nodes, hub, runFor := createNodes(t, consensusParticipants, rootSnapshot, stopper) 172 173 hub.WithFilter(blockNothing) 174 175 runFor(60 * time.Second) 176 177 allViews := allFinalizedViews(t, nodes) 178 assertSafety(t, allViews) 179 180 // confirm that we have transitioned to the new epoch 181 pstate := nodes[0].state 182 afterCounter, err := pstate.Final().Epochs().Current().Counter() 183 require.NoError(t, err) 184 assert.Equal(t, firstEpochCounter+1, afterCounter) 185 186 cleanupNodes(nodes) 187 } 188 189 // withNextEpoch adds a valid next epoch with the given identities to the input 190 // snapshot. Also sets the length of the first (current) epoch to curEpochViews. 191 // 192 // We make the first (current) epoch start in committed phase so we can transition 193 // to the next epoch upon reaching the appropriate view without any further changes 194 // to the protocol state. 195 func withNextEpoch( 196 t *testing.T, 197 snapshot *inmem.Snapshot, 198 nextEpochIdentities flow.IdentityList, 199 nextEpochParticipantData *run.ParticipantData, 200 participantsCache *ConsensusParticipants, 201 curEpochViews uint64, 202 createQC func(block *flow.Block) *flow.QuorumCertificate, 203 ) *inmem.Snapshot { 204 nextEpochIdentities = nextEpochIdentities.Sort(flow.Canonical[flow.Identity]) 205 206 // convert to encodable representation for simple modification 207 encodableSnapshot := snapshot.Encodable() 208 209 rootProtocolState := encodableSnapshot.SealingSegment.LatestProtocolStateEntry() 210 epochProtocolState := rootProtocolState.EpochEntry 211 currEpochSetup := epochProtocolState.CurrentEpochSetup 212 currEpochCommit := epochProtocolState.CurrentEpochCommit 213 214 // Set current epoch length 215 currEpochSetup.FinalView = currEpochSetup.FirstView + curEpochViews - 1 216 epochProtocolState.CurrentEpoch.SetupID = currEpochSetup.ID() 217 218 // Construct events for next epoch 219 nextEpochSetup := &flow.EpochSetup{ 220 Counter: currEpochSetup.Counter + 1, 221 FirstView: currEpochSetup.FinalView + 1, 222 FinalView: currEpochSetup.FinalView + 1 + 10_000, 223 RandomSource: unittest.SeedFixture(flow.EpochSetupRandomSourceLength), 224 Participants: nextEpochIdentities.ToSkeleton(), 225 Assignments: unittest.ClusterAssignment(1, nextEpochIdentities.ToSkeleton()), 226 } 227 nextEpochCommit := &flow.EpochCommit{ 228 Counter: nextEpochSetup.Counter, 229 ClusterQCs: currEpochCommit.ClusterQCs, 230 DKGParticipantKeys: nextEpochParticipantData.PublicBeaconKeys(), 231 DKGGroupKey: nextEpochParticipantData.GroupKey, 232 } 233 epochProtocolState.NextEpoch = &flow.EpochStateContainer{ 234 SetupID: nextEpochSetup.ID(), 235 CommitID: nextEpochCommit.ID(), 236 ActiveIdentities: flow.DynamicIdentityEntryListFromIdentities(nextEpochIdentities), 237 } 238 // Re-construct epoch protocol state with modified events (constructs ActiveIdentity fields) 239 epochProtocolState, err := flow.NewRichProtocolStateEntry( 240 epochProtocolState.ProtocolStateEntry, 241 epochProtocolState.PreviousEpochSetup, epochProtocolState.PreviousEpochCommit, 242 currEpochSetup, currEpochCommit, 243 nextEpochSetup, nextEpochCommit) 244 require.NoError(t, err) 245 246 // Store the modified epoch protocol state entry and corresponding KV store entry 247 rootKVStore := kvstore.NewDefaultKVStore(epochProtocolState.ID()) 248 protocolVersion, encodedKVStore, err := rootKVStore.VersionedEncode() 249 require.NoError(t, err) 250 encodableSnapshot.SealingSegment.ProtocolStateEntries = map[flow.Identifier]*flow.ProtocolStateEntryWrapper{ 251 rootKVStore.ID(): { 252 KVStore: flow.PSKeyValueStoreData{ 253 Version: protocolVersion, 254 Data: encodedKVStore, 255 }, 256 EpochEntry: epochProtocolState, 257 }, 258 } 259 260 // Since we modified the root protocol state, we need to update the root block's ProtocolStateID field. 261 rootBlock := encodableSnapshot.SealingSegment.Blocks[0] 262 rootBlockPayload := rootBlock.Payload 263 rootBlockPayload.ProtocolStateID = rootKVStore.ID() 264 rootBlock.SetPayload(*rootBlockPayload) 265 // Since we changed the root block, we need to update the QC, root result, and root seal. 266 encodableSnapshot.LatestResult.BlockID = rootBlock.ID() 267 encodableSnapshot.LatestSeal.ResultID = encodableSnapshot.LatestResult.ID() 268 encodableSnapshot.LatestSeal.BlockID = rootBlock.ID() 269 encodableSnapshot.SealingSegment.LatestSeals = map[flow.Identifier]flow.Identifier{ 270 rootBlock.ID(): encodableSnapshot.LatestSeal.ID(), 271 } 272 encodableSnapshot.QuorumCertificate = createQC(rootBlock) 273 274 participantsCache.Update(nextEpochSetup.Counter, nextEpochParticipantData) 275 276 return inmem.SnapshotFromEncodable(encodableSnapshot) 277 }