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  }