code.vegaprotocol.io/vega@v0.79.0/core/governance/checkpoint_test.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package governance_test 17 18 import ( 19 "bytes" 20 "context" 21 "testing" 22 "time" 23 24 "code.vegaprotocol.io/vega/core/assets" 25 "code.vegaprotocol.io/vega/core/assets/builtin" 26 "code.vegaprotocol.io/vega/core/events" 27 "code.vegaprotocol.io/vega/core/governance" 28 "code.vegaprotocol.io/vega/core/types" 29 "code.vegaprotocol.io/vega/libs/proto" 30 "code.vegaprotocol.io/vega/libs/ptr" 31 vgrand "code.vegaprotocol.io/vega/libs/rand" 32 vegapb "code.vegaprotocol.io/vega/protos/vega" 33 checkpointpb "code.vegaprotocol.io/vega/protos/vega/checkpoint/v1" 34 35 "github.com/golang/mock/gomock" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 ) 39 40 func TestCheckpoint(t *testing.T) { 41 t.Run("Basic test -> get checkpoints at various points in time, load checkpoint", testCheckpointSuccess) 42 t.Run("Loading with missing rationale shouldn't be a problem", testCheckpointLoadingWithMissingRationaleShouldNotBeProblem) 43 } 44 45 func testCheckpointSuccess(t *testing.T) { 46 eng := getTestEngine(t, time.Now()) 47 48 // when 49 proposer := eng.newValidParty("proposer", 1) 50 voter1 := eng.newValidPartyTimes("voter-1", 7, 2) 51 voter2 := eng.newValidPartyTimes("voter2", 1, 0) 52 53 now := eng.tsvc.GetTimeNow().Add(48 * time.Hour) 54 termTimeAfterEnact := now.Add(4 * 48 * time.Hour).Add(1 * time.Second) 55 filter, binding := produceTimeTriggeredDataSourceSpec(termTimeAfterEnact) 56 proposal := eng.newProposalForNewMarket(proposer.Id, now, filter, binding, true) 57 ctx := context.Background() 58 59 // setup 60 eng.ensureStakingAssetTotalSupply(t, 9) 61 eng.ensureAllAssetEnabled(t) 62 eng.expectOpenProposalEvent(t, proposer.Id, proposal.ID) 63 64 // when 65 _, err := eng.submitProposal(t, proposal) 66 67 // then 68 require.NoError(t, err) 69 70 // setup 71 eng.expectVoteEvent(t, voter1.Id, proposal.ID) 72 73 // then 74 err = eng.addYesVote(t, voter1.Id, proposal.ID) 75 76 // then 77 require.NoError(t, err) 78 79 // given 80 afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second) 81 82 // setup 83 eng.expectPassedProposalEvent(t, proposal.ID) 84 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 85 86 // checkpoint should be empty at this point 87 data, err := eng.Checkpoint() 88 require.NoError(t, err) 89 require.Empty(t, data) 90 91 eng.expectGetMarketState(t, proposal.ID) 92 // when 93 eng.OnTick(ctx, afterClosing) 94 95 // the proposal should already be in the checkpoint 96 data, err = eng.Checkpoint() 97 require.NoError(t, err) 98 require.NotEmpty(t, data) 99 100 // when 101 err = eng.addNoVote(t, voter2.Id, proposal.ID) 102 103 // then 104 assert.Error(t, err) 105 assert.EqualError(t, err, governance.ErrProposalNotOpenForVotes.Error()) 106 107 // given 108 afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second) 109 110 // when 111 // no calculations, no state change, simply removed from governance engine 112 toBeEnacted, closed := eng.OnTick(ctx, afterEnactment) 113 114 // then 115 require.NotEmpty(t, toBeEnacted) 116 require.Empty(t, closed) 117 assert.Equal(t, proposal.ID, toBeEnacted[0].Proposal().ID) 118 119 // Now take the checkpoint 120 data, err = eng.Checkpoint() 121 require.NoError(t, err) 122 require.NotEmpty(t, data) 123 124 eng2 := getTestEngine(t, time.Now()) 125 defer eng2.ctrl.Finish() 126 127 eng2.broker.EXPECT().SendBatch(gomock.Any()).Times(1) 128 129 eng2.assets.EXPECT().Get(gomock.Any()).AnyTimes().DoAndReturn(func(id string) (*assets.Asset, error) { 130 ret := assets.NewAsset(builtin.New(id, &types.AssetDetails{})) 131 return ret, nil 132 }) 133 eng2.assets.EXPECT().IsEnabled(gomock.Any()).Return(true).AnyTimes() 134 eng2.markets.EXPECT().RestoreMarket(gomock.Any(), gomock.Any()).Return(nil).Times(1) 135 eng2.markets.EXPECT().StartOpeningAuction(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() 136 eng2.expectGetMarketState(t, proposal.ID) 137 138 // Load checkpoint 139 require.NoError(t, eng2.Load(ctx, data)) 140 141 // check that it matches what we took before in eng1 142 cp2, err := eng2.Checkpoint() 143 require.NoError(t, err) 144 require.True(t, bytes.Equal(cp2, data)) 145 146 data = append(data, []byte("foo")...) 147 require.Error(t, eng2.Load(ctx, data)) 148 } 149 150 func enactNewProposal(t *testing.T, eng *tstEngine) types.Proposal { 151 t.Helper() 152 proposer := eng.newValidPartyTimes("proposer", 1, 0) 153 voter1 := eng.newValidPartyTimes("voter-1", 7, 0) 154 proposalTime := eng.tsvc.GetTimeNow().Add(48 * time.Hour) 155 termTimeAfterEnact := proposalTime.Add(4 * 48 * time.Hour).Add(1 * time.Second) 156 filter, binding := produceTimeTriggeredDataSourceSpec(termTimeAfterEnact) 157 proposal := eng.newProposalForNewMarket(proposer.Id, proposalTime, filter, binding, true) 158 159 // setup 160 eng.ensureStakingAssetTotalSupply(t, 9) 161 eng.ensureAllAssetEnabled(t) 162 eng.expectOpenProposalEvent(t, proposer.Id, proposal.ID) 163 164 // when 165 _, err := eng.submitProposal(t, proposal) 166 167 // then 168 require.NoError(t, err) 169 170 // setup 171 eng.expectVoteEvent(t, voter1.Id, proposal.ID) 172 173 // then 174 err = eng.addYesVote(t, voter1.Id, proposal.ID) 175 176 // then 177 require.NoError(t, err) 178 179 return proposal 180 } 181 182 func TestCheckpointSavingAndLoadingWithDroppedMarkets(t *testing.T) { 183 eng := getTestEngine(t, time.Now()) 184 185 // enact a proposal for market 1,2,3 186 proposals := make([]types.Proposal, 0, 3) 187 // balance itself shouldn't matter, just make sure it's a non-nil value 188 for i := 0; i < 3; i++ { 189 proposals = append(proposals, enactNewProposal(t, eng)) 190 } 191 192 // market 1 has already been dropped of the execution engine, so it is removed from active and not added to enacted 193 // market 2 has trading terminated 194 // market 3 is there and should be saved to the checkpoint 195 eng.markets.EXPECT().GetMarketState(proposals[0].ID).Times(1).Return(types.MarketStateUnspecified, types.ErrInvalidMarketID) 196 eng.markets.EXPECT().GetMarketState(proposals[1].ID).Times(2).Return(types.MarketStateTradingTerminated, nil) 197 eng.markets.EXPECT().GetMarketState(proposals[2].ID).Times(2).Return(types.MarketStateActive, nil) 198 199 afterEnactment := time.Unix(proposals[2].Terms.EnactmentTimestamp, 0).Add(time.Second) 200 201 eng.expectPassedProposalEvent(t, proposals[0].ID) 202 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 203 204 eng.expectPassedProposalEvent(t, proposals[1].ID) 205 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 206 207 eng.expectPassedProposalEvent(t, proposals[2].ID) 208 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 209 210 // when 211 eng.OnTick(context.Background(), afterEnactment) 212 213 // the proposal2 should already be in the checkpoint, proposal 0,1 should be ignored 214 data, err := eng.Checkpoint() 215 require.NoError(t, err) 216 require.NotEmpty(t, data) 217 eng2 := getTestEngine(t, time.Now()) 218 defer eng2.ctrl.Finish() 219 220 var counter int 221 eng2.broker.EXPECT().SendBatch(gomock.Any()).Times(1).DoAndReturn(func(es []events.Event) { counter = len(es) }) 222 223 eng2.assets.EXPECT().Get(gomock.Any()).AnyTimes().DoAndReturn(func(id string) (*assets.Asset, error) { 224 ret := assets.NewAsset(builtin.New(id, &types.AssetDetails{})) 225 return ret, nil 226 }) 227 eng2.assets.EXPECT().IsEnabled(gomock.Any()).Return(true).AnyTimes() 228 eng2.markets.EXPECT().RestoreMarket(gomock.Any(), gomock.Any()).Return(nil).Times(1) 229 eng2.markets.EXPECT().StartOpeningAuction(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() 230 231 // Load checkpoint 232 require.NoError(t, eng2.Load(context.Background(), data)) 233 // there should be only one proposal in there 234 require.Equal(t, 1, counter) 235 } 236 237 func testCheckpointLoadingWithMissingRationaleShouldNotBeProblem(t *testing.T) { 238 eng := getTestEngine(t, time.Now()) 239 240 now := eng.tsvc.GetTimeNow() 241 // given 242 proposalWithoutRationale := &vegapb.Proposal{ 243 Id: vgrand.RandomStr(5), 244 Reference: vgrand.RandomStr(5), 245 PartyId: vgrand.RandomStr(5), 246 State: types.ProposalStateEnacted, 247 Timestamp: 123456789, 248 Terms: &vegapb.ProposalTerms{ 249 ClosingTimestamp: now.Add(10 * time.Minute).Unix(), 250 EnactmentTimestamp: now.Add(30 * time.Minute).Unix(), 251 ValidationTimestamp: 0, 252 Change: &vegapb.ProposalTerms_NewFreeform{}, 253 }, 254 Reason: ptr.From(vegapb.ProposalError_PROPOSAL_ERROR_UNSPECIFIED), 255 ErrorDetails: ptr.From(""), 256 Rationale: nil, 257 } 258 data := marshalProposal(t, proposalWithoutRationale) 259 260 // setup 261 eng.expectRestoredProposals(t, []string{proposalWithoutRationale.Id}) 262 263 // when 264 err := eng.Load(context.Background(), data) 265 266 // then 267 require.NoError(t, err) 268 } 269 270 func marshalProposal(t *testing.T, proposal *vegapb.Proposal) []byte { 271 t.Helper() 272 proposals := &checkpointpb.Proposals{ 273 Proposals: []*vegapb.Proposal{proposal}, 274 } 275 276 data, err := proto.Marshal(proposals) 277 if err != nil { 278 t.Fatalf("couldn't marshal proposals for tests: %v", err) 279 } 280 281 return data 282 } 283 284 func enactUpdateProposal(t *testing.T, eng *tstEngine, marketID string) string { 285 t.Helper() 286 proposer := eng.newValidPartyTimes("proposer", 1, 0) 287 voter1 := eng.newValidPartyTimes("voter-1", 7, 0) 288 now := eng.tsvc.GetTimeNow() 289 termTimeAfterEnact := now.Add(4 * 48 * time.Hour).Add(1 * time.Second) 290 filter, binding := produceTimeTriggeredDataSourceSpec(termTimeAfterEnact) 291 proposal := eng.newProposalForMarketUpdate(marketID, proposer.Id, eng.tsvc.GetTimeNow(), filter, binding, true) 292 ctx := context.Background() 293 294 // setup 295 eng.ensureStakingAssetTotalSupply(t, 9) 296 eng.ensureAllAssetEnabled(t) 297 eng.expectOpenProposalEvent(t, proposer.Id, proposal.ID) 298 eng.ensureEquityLikeShareForMarketAndParty(t, marketID, proposer.Id, 0.1) 299 eng.ensureEquityLikeShareForMarketAndParty(t, marketID, voter1.Id, 0.1) 300 301 // when 302 _, err := eng.submitProposal(t, proposal) 303 304 // then 305 require.NoError(t, err) 306 307 // setup 308 eng.expectVoteEvent(t, voter1.Id, proposal.ID) 309 310 // then 311 err = eng.addYesVote(t, voter1.Id, proposal.ID) 312 313 // then 314 require.NoError(t, err) 315 316 // given 317 afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second) 318 319 // setup 320 eng.expectPassedProposalEvent(t, proposal.ID) 321 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 322 323 // when 324 eng.OnTick(ctx, afterEnactment) 325 return proposal.ID 326 } 327 328 func TestCheckpointWithMarketUpdateProposals(t *testing.T) { 329 eng := getTestEngine(t, time.Now()) 330 331 // enact a proposal for market 1,2,3 332 proposal := enactNewProposal(t, eng) 333 // given 334 afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second) 335 336 // setup 337 eng.expectPassedProposalEvent(t, proposal.ID) 338 eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7") 339 340 eng.markets.EXPECT().GetMarketState(proposal.ID).AnyTimes().Return(types.MarketStateActive, nil) 341 342 // when 343 eng.OnTick(context.Background(), afterEnactment) 344 proposalID := proposal.ID 345 346 eng.markets.EXPECT().MarketExists(proposalID).AnyTimes().Return(true) 347 eng.ensureGetMarketFuture(t, proposalID) 348 349 expectedMarket := types.Market{ 350 ID: proposalID, 351 TradableInstrument: &types.TradableInstrument{ 352 Instrument: &types.Instrument{ 353 Name: vgrand.RandomStr(10), 354 Product: &types.InstrumentFuture{ 355 Future: &types.Future{ 356 SettlementAsset: "BTC", 357 }, 358 }, 359 }, 360 }, 361 DecimalPlaces: 3, 362 PositionDecimalPlaces: 4, 363 OpeningAuction: &types.AuctionDuration{ 364 Duration: 42, 365 }, 366 } 367 368 // setup 369 eng.ensureGetMarket(t, proposalID, expectedMarket) 370 enactUpdateProposal(t, eng, proposalID) 371 372 // the proposal2 should already be in the checkpoint, proposal 0,1 should be ignored 373 data, err := eng.Checkpoint() 374 require.NoError(t, err) 375 require.NotEmpty(t, data) 376 377 eng2 := getTestEngine(t, time.Now()) 378 defer eng2.ctrl.Finish() 379 380 var counter int 381 eng2.broker.EXPECT().SendBatch(gomock.Any()).AnyTimes().DoAndReturn(func(es []events.Event) { 382 counter = len(es) 383 }) 384 385 eng2.assets.EXPECT().Get(gomock.Any()).AnyTimes().DoAndReturn(func(id string) (*assets.Asset, error) { 386 ret := assets.NewAsset(builtin.New(id, &types.AssetDetails{})) 387 return ret, nil 388 }) 389 eng2.assets.EXPECT().IsEnabled(gomock.Any()).Return(true).AnyTimes() 390 eng2.markets.EXPECT().RestoreMarket(gomock.Any(), gomock.Any()).Return(nil).Times(1) 391 eng2.markets.EXPECT().StartOpeningAuction(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() 392 393 // Load checkpoint 394 eng2.markets.EXPECT(). 395 GetMarket(proposalID, gomock.Any()). 396 AnyTimes(). 397 Return(expectedMarket, true) 398 eng2.markets.EXPECT().UpdateMarket(gomock.Any(), gomock.Any()).Times(1) 399 require.NoError(t, eng2.Load(context.Background(), data)) 400 401 eng.markets.EXPECT().GetMarketState(proposal.ID).AnyTimes().Return(types.MarketStateActive, nil) 402 eng2.markets.EXPECT().GetMarketState(proposal.ID).AnyTimes().Return(types.MarketStateActive, nil) 403 404 cp1, _ := eng.Checkpoint() 405 cp2, _ := eng2.Checkpoint() 406 require.True(t, bytes.Equal(cp1, cp2)) 407 require.Equal(t, 2, counter) 408 }