diff --git a/config/camino.go b/config/camino.go index ffa857411388..b567d80c21a2 100644 --- a/config/camino.go +++ b/config/camino.go @@ -12,17 +12,17 @@ import ( ) const ( - DaoProposalBondAmountKey = "dao-proposal-bond-amount" + DACProposalBondAmountKey = "dac-proposal-bond-amount" ) func addCaminoFlags(fs *flag.FlagSet) { - // Bond amount required to place a DAO proposal on the Primary Network - fs.Uint64(DaoProposalBondAmountKey, genesis.LocalParams.CaminoConfig.DaoProposalBondAmount, "Amount, in nAVAX, required to place a DAO proposal") + // Bond amount required to place a DAC proposal on the Primary Network + fs.Uint64(DACProposalBondAmountKey, genesis.LocalParams.CaminoConfig.DACProposalBondAmount, "Amount, in nAVAX, required to place a DAC proposal") } func getCaminoPlatformConfig(v *viper.Viper) caminoconfig.Config { conf := caminoconfig.Config{ - DaoProposalBondAmount: v.GetUint64(DaoProposalBondAmountKey), + DACProposalBondAmount: v.GetUint64(DACProposalBondAmountKey), } return conf } diff --git a/database/linkeddb/mock_linkeddb.go b/database/linkeddb/mock_linkeddb.go new file mode 100644 index 000000000000..6a634d4bd390 --- /dev/null +++ b/database/linkeddb/mock_linkeddb.go @@ -0,0 +1,170 @@ +// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/database (interfaces: Database) + +// Package database is a generated GoMock package. +package linkeddb + +import ( + reflect "reflect" + + database "github.com/ava-labs/avalanchego/database" + gomock "github.com/golang/mock/gomock" +) + +// MockLinkedDB is a mock of LinkedDB interface. +type MockLinkedDB struct { + ctrl *gomock.Controller + recorder *MockLinkedDBMockRecorder +} + +// MockLinkedDBMockRecorder is the mock recorder for MockLinkedDB. +type MockLinkedDBMockRecorder struct { + mock *MockLinkedDB +} + +// NewMockLinkedDB creates a new mock instance. +func NewMockLinkedDB(ctrl *gomock.Controller) *MockLinkedDB { + mock := &MockLinkedDB{ctrl: ctrl} + mock.recorder = &MockLinkedDBMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLinkedDB) EXPECT() *MockLinkedDBMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockLinkedDB) Delete(arg0 []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockLinkedDBMockRecorder) Delete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockLinkedDB)(nil).Delete), arg0) +} + +// Get mocks base method. +func (m *MockLinkedDB) Get(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockLinkedDBMockRecorder) Get(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockLinkedDB)(nil).Get), arg0) +} + +// Has mocks base method. +func (m *MockLinkedDB) Has(arg0 []byte) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Has", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Has indicates an expected call of Has. +func (mr *MockLinkedDBMockRecorder) Has(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockLinkedDB)(nil).Has), arg0) +} + +// NewIterator mocks base method. +func (m *MockLinkedDB) NewIterator() database.Iterator { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewIterator") + ret0, _ := ret[0].(database.Iterator) + return ret0 +} + +// NewIterator indicates an expected call of NewIterator. +func (mr *MockLinkedDBMockRecorder) NewIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewIterator", reflect.TypeOf((*MockLinkedDB)(nil).NewIterator)) +} + +// NewIteratorWithStart mocks base method. +func (m *MockLinkedDB) NewIteratorWithStart(arg0 []byte) database.Iterator { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewIteratorWithStart", arg0) + ret0, _ := ret[0].(database.Iterator) + return ret0 +} + +// NewIteratorWithStart indicates an expected call of NewIteratorWithStart. +func (mr *MockLinkedDBMockRecorder) NewIteratorWithStart(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewIteratorWithStart", reflect.TypeOf((*MockLinkedDB)(nil).NewIteratorWithStart), arg0) +} + +// Put mocks base method. +func (m *MockLinkedDB) Put(arg0, arg1 []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Put indicates an expected call of Put. +func (mr *MockLinkedDBMockRecorder) Put(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockLinkedDB)(nil).Put), arg0, arg1) +} + +// IsEmpty mocks base method. +func (m *MockLinkedDB) IsEmpty() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsEmpty") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsEmpty indicates an expected call of IsEmpty. +func (mr *MockLinkedDBMockRecorder) IsEmpty() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEmpty", reflect.TypeOf((*MockLinkedDB)(nil).IsEmpty)) +} + +// HeadKey mocks base method. +func (m *MockLinkedDB) HeadKey() ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeadKey") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeadKey indicates an expected call of HeadKey. +func (mr *MockLinkedDBMockRecorder) HeadKey() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadKey", reflect.TypeOf((*MockLinkedDB)(nil).HeadKey)) +} + +// Head mocks base method. +func (m *MockLinkedDB) Head() ([]byte, []byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Head") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].([]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Head indicates an expected call of Head. +func (mr *MockLinkedDBMockRecorder) Head() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Head", reflect.TypeOf((*MockLinkedDB)(nil).Head)) +} diff --git a/genesis/genesis_camino.go b/genesis/genesis_camino.go index 6fcb087b6f20..fa639eae2e5c 100644 --- a/genesis/genesis_camino.go +++ b/genesis/genesis_camino.go @@ -40,7 +40,7 @@ var ( SupplyCap: 1000 * units.MegaAvax, }, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 1 * units.KiloAvax, + DACProposalBondAmount: 1 * units.KiloAvax, }, }, } diff --git a/genesis/genesis_columbus.go b/genesis/genesis_columbus.go index 010408290780..d087ed807d3a 100644 --- a/genesis/genesis_columbus.go +++ b/genesis/genesis_columbus.go @@ -40,7 +40,7 @@ var ( SupplyCap: 1000 * units.MegaAvax, }, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, }, } diff --git a/genesis/genesis_kopernikus.go b/genesis/genesis_kopernikus.go index 526886be0b44..278cca29dbb1 100644 --- a/genesis/genesis_kopernikus.go +++ b/genesis/genesis_kopernikus.go @@ -46,7 +46,7 @@ var ( SupplyCap: 1000 * units.MegaAvax, }, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, }, } diff --git a/genesis/genesis_local.go b/genesis/genesis_local.go index c8047245cca7..a2b64e73bade 100644 --- a/genesis/genesis_local.go +++ b/genesis/genesis_local.go @@ -73,7 +73,7 @@ var ( SupplyCap: 720 * units.MegaAvax, }, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, }, } diff --git a/version/constants.go b/version/constants.go index 2d7ccd96218d..020e2650b34c 100644 --- a/version/constants.go +++ b/version/constants.go @@ -119,6 +119,13 @@ var ( } AthensPhaseDefaultTime = time.Date(2023, time.July, 1, 8, 0, 0, 0, time.UTC) + BerlinPhaseTimes = map[uint32]time.Time{ + constants.KopernikusID: time.Date(2023, time.July, 4, 13, 0, 0, 0, time.UTC), + constants.ColumbusID: time.Date(2023, time.July, 7, 8, 0, 0, 0, time.UTC), + constants.CaminoID: time.Date(2023, time.July, 17, 8, 0, 0, 0, time.UTC), + } + BerlinPhaseDefaultTime = time.Date(2023, time.July, 1, 8, 0, 0, 0, time.UTC) + // TODO: update this before release CortinaTimes = map[uint32]time.Time{ constants.MainnetID: time.Date(10000, time.December, 1, 0, 0, 0, 0, time.UTC), @@ -204,6 +211,13 @@ func GetAthensPhaseTime(networkID uint32) time.Time { return AthensPhaseDefaultTime } +func GetBerlinPhaseTime(networkID uint32) time.Time { + if upgradeTime, exists := BerlinPhaseTimes[networkID]; exists { + return upgradeTime + } + return BerlinPhaseDefaultTime +} + func GetCortinaTime(networkID uint32) time.Time { if upgradeTime, exists := CortinaTimes[networkID]; exists { return upgradeTime diff --git a/vms/platformvm/blocks/builder/camino_builder.go b/vms/platformvm/blocks/builder/camino_builder.go index 4840e474bd31..4e923829e3e4 100644 --- a/vms/platformvm/blocks/builder/camino_builder.go +++ b/vms/platformvm/blocks/builder/camino_builder.go @@ -72,6 +72,7 @@ func caminoBuildBlock( return nil, nil } + // Ulocking expired deposits depositsTxIDs, shouldUnlock, err := getNextDepositsToUnlock(parentState, timestamp) if err != nil { return nil, fmt.Errorf("could not find next deposits to unlock: %w", err) @@ -90,6 +91,31 @@ func caminoBuildBlock( ) } + // Finishing expired and early finished proposals + expiredProposalIDs, err := getExpiredProposals(parentState, timestamp) + if err != nil { + return nil, fmt.Errorf("could not find expired proposals: %w", err) + } + earlyFinishedProposalIDs, err := parentState.GetProposalIDsToFinish() + if err != nil { + return nil, fmt.Errorf("could not find successful proposals: %w", err) + } + if len(expiredProposalIDs) > 0 || len(earlyFinishedProposalIDs) > 0 { + finishProposalsTx, err := txBuilder.FinishProposalsTx(parentState, earlyFinishedProposalIDs, expiredProposalIDs) + if err != nil { + return nil, fmt.Errorf("could not build tx to finish proposals: %w", err) + } + + // FinishProposalsTx should never be in block with addVoteTx, + // because it can affect state of proposals. + return blocks.NewBanffStandardBlock( + timestamp, + parentID, + height, + []*txs.Tx{finishProposalsTx}, + ) + } + return nil, nil } @@ -133,3 +159,25 @@ func getNextDepositsToUnlock( return nextDeposits, nextDepositsEndtime.Equal(chainTime), nil } + +func getExpiredProposals( + preferredState state.Chain, + chainTime time.Time, +) ([]ids.ID, error) { + if !chainTime.Before(mockable.MaxTime) { + return nil, errEndOfTime + } + + nextProposals, nextProposalsEndtime, err := preferredState.GetNextToExpireProposalIDsAndTime(nil) + if err == database.ErrNotFound { + return nil, nil + } else if err != nil { + return nil, err + } + + if nextProposalsEndtime.Equal(chainTime) { + return nextProposals, nil + } + + return nil, nil +} diff --git a/vms/platformvm/blocks/executor/proposal_block_test.go b/vms/platformvm/blocks/executor/proposal_block_test.go index 7cda6de9ec3c..471c63a8086b 100644 --- a/vms/platformvm/blocks/executor/proposal_block_test.go +++ b/vms/platformvm/blocks/executor/proposal_block_test.go @@ -253,7 +253,8 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetDeferredStakerIterator().Return(deferredStakersIt, nil).AnyTimes() onParentAccept.EXPECT().GetNextToUnlockDepositTime(nil).Return(time.Time{}, database.ErrNotFound).AnyTimes() - onParentAccept.EXPECT().GetNextToUnlockDepositIDsAndTime(nil).Return(nil, time.Time{}, database.ErrNotFound).AnyTimes() + onParentAccept.EXPECT().GetNextProposalExpirationTime(nil).Return(time.Time{}, database.ErrNotFound).AnyTimes() + onParentAccept.EXPECT().GetProposalIDsToFinish().Return(nil, nil).AnyTimes() env.mockedState.EXPECT().GetUptime(gomock.Any(), gomock.Any()).Return( time.Duration(1000), /*upDuration*/ diff --git a/vms/platformvm/blocks/executor/standard_block_test.go b/vms/platformvm/blocks/executor/standard_block_test.go index 2d1f6414b5bf..f17deca29aec 100644 --- a/vms/platformvm/blocks/executor/standard_block_test.go +++ b/vms/platformvm/blocks/executor/standard_block_test.go @@ -161,7 +161,8 @@ func TestBanffStandardBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetDeferredStakerIterator().Return(deferredStakersIt, nil).AnyTimes() onParentAccept.EXPECT().GetNextToUnlockDepositTime(nil).Return(time.Time{}, database.ErrNotFound).AnyTimes() - onParentAccept.EXPECT().GetNextToUnlockDepositIDsAndTime(nil).Return(nil, time.Time{}, database.ErrNotFound).AnyTimes() + onParentAccept.EXPECT().GetNextProposalExpirationTime(nil).Return(time.Time{}, database.ErrNotFound).AnyTimes() + onParentAccept.EXPECT().GetProposalIDsToFinish().Return(nil, nil).AnyTimes() onParentAccept.EXPECT().GetTimestamp().Return(chainTime).AnyTimes() diff --git a/vms/platformvm/camino_helpers_test.go b/vms/platformvm/camino_helpers_test.go index 3b6c264a3053..82c7b08e2b05 100644 --- a/vms/platformvm/camino_helpers_test.go +++ b/vms/platformvm/camino_helpers_test.go @@ -30,7 +30,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/caminoconfig" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis" "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/stretchr/testify/require" ) @@ -45,14 +47,18 @@ var ( _, caminoPreFundedNodeIDs = nodeid.LoadLocalCaminoNodeKeysAndIDs(localStakingPath) ) -func newCaminoVM(genesisConfig api.Camino, genesisUTXOs []api.UTXO) *VM { +func newCaminoVM(genesisConfig api.Camino, genesisUTXOs []api.UTXO, startTime *time.Time) *VM { vm := &VM{Config: defaultCaminoConfig(true)} baseDBManager := manager.NewMemDB(version.Semantic1_0_0) chainDBManager := baseDBManager.NewPrefixDBManager([]byte{0}) atomicDB := prefixdb.New([]byte{1}, baseDBManager.Current().Database) - vm.clock.Set(banffForkTime.Add(time.Second)) + if startTime == nil { + defaultStartTime := banffForkTime.Add(time.Second) + startTime = &defaultStartTime + } + vm.clock.Set(*startTime) msgChan := make(chan common.Message, 1) ctx := defaultContext() @@ -64,7 +70,7 @@ func newCaminoVM(genesisConfig api.Camino, genesisUTXOs []api.UTXO) *VM { ctx.Lock.Lock() defer ctx.Lock.Unlock() - _, genesisBytes := newCaminoGenesisWithUTXOs(genesisConfig, genesisUTXOs) + _, genesisBytes := newCaminoGenesisWithUTXOs(genesisConfig, genesisUTXOs, startTime) appSender := &common.SenderTest{} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { @@ -140,7 +146,7 @@ func defaultCaminoConfig(postBanff bool) config.Config { ApricotPhase5Time: defaultValidateEndTime, BanffTime: banffTime, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, } } @@ -148,9 +154,12 @@ func defaultCaminoConfig(postBanff bool) config.Config { // Returns: // 1) The genesis state // 2) The byte representation of the default genesis for tests -func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []api.UTXO) (*api.BuildGenesisArgs, []byte) { +func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []api.UTXO, starttime *time.Time) (*api.BuildGenesisArgs, []byte) { hrp := constants.NetworkIDToHRP[testNetworkID] + if starttime == nil { + starttime = &defaultValidateStartTime + } caminoGenesisConfig.UTXODeposits = make([]api.UTXODeposit, len(genesisUTXOs)) caminoGenesisConfig.ValidatorDeposits = make([][]api.UTXODeposit, len(caminoPreFundedKeys)) caminoGenesisConfig.ValidatorConsortiumMembers = make([]ids.ShortID, len(caminoPreFundedKeys)) @@ -163,8 +172,8 @@ func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []ap } genesisValidators[i] = api.PermissionlessValidator{ Staker: api.Staker{ - StartTime: json.Uint64(defaultValidateStartTime.Unix()), - EndTime: json.Uint64(defaultValidateEndTime.Unix()), + StartTime: json.Uint64(starttime.Unix()), + EndTime: json.Uint64(starttime.Add(10 * defaultMinStakingDuration).Unix()), NodeID: caminoPreFundedNodeIDs[i], }, RewardOwner: &api.Owner{ @@ -178,6 +187,10 @@ func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []ap } caminoGenesisConfig.ValidatorDeposits[i] = make([]api.UTXODeposit, 1) caminoGenesisConfig.ValidatorConsortiumMembers[i] = key.Address() + caminoGenesisConfig.AddressStates = append(caminoGenesisConfig.AddressStates, genesis.AddressState{ + Address: key.Address(), + State: txs.AddressStateConsortiumMember, + }) } buildGenesisArgs := api.BuildGenesisArgs{ @@ -207,6 +220,7 @@ func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []ap } func generateKeyAndOwner(t *testing.T) (*secp256k1.PrivateKey, ids.ShortID, secp256k1fx.OutputOwners) { + t.Helper() key, err := testKeyFactory.NewPrivateKey() require.NoError(t, err) addr := key.Address() diff --git a/vms/platformvm/camino_service.go b/vms/platformvm/camino_service.go index ebd68f725f58..b682d8ca1f39 100644 --- a/vms/platformvm/camino_service.go +++ b/vms/platformvm/camino_service.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/keystore" @@ -1012,6 +1013,7 @@ func apiOfferFromOffer(offer *deposit.Offer) *APIDepositOffer { type GetUpgradePhasesReply struct { AthensPhase utilsjson.Uint32 `json:"athensPhase"` + BerlinPhase utilsjson.Uint32 `json:"berlinPhase"` } func (s *CaminoService) GetUpgradePhases(_ *http.Request, _ *struct{}, response *GetUpgradePhasesReply) error { @@ -1020,5 +1022,53 @@ func (s *CaminoService) GetUpgradePhases(_ *http.Request, _ *struct{}, response if s.vm.Config.IsAthensPhaseActivated(s.vm.state.GetTimestamp()) { response.AthensPhase = 1 } + if s.vm.Config.IsBerlinPhaseActivated(s.vm.state.GetTimestamp()) { + response.BerlinPhase = 1 + } + return nil +} + +type ConsortiumMemberValidator struct { + ValidatorWeight utilsjson.Uint64 `json:"validatorWeight"` + ConsortiumMemberAddress string `json:"consortiumMemberAddress"` +} + +type GetValidatorsAtReply2 struct { + Validators map[ids.NodeID]ConsortiumMemberValidator `json:"validators"` +} + +// Overrides avax service GetValidatorsAt +func (s *CaminoService) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, reply *GetValidatorsAtReply2) error { + height := uint64(args.Height) + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getValidatorsAt"), + zap.Uint64("height", height), + zap.Stringer("subnetID", args.SubnetID), + ) + + ctx := r.Context() + var err error + vdrs, err := s.vm.GetValidatorSet(ctx, height, args.SubnetID) + if err != nil { + return fmt.Errorf("failed to get validator set: %w", err) + } + reply.Validators = make(map[ids.NodeID]ConsortiumMemberValidator, len(vdrs)) + for _, vdr := range vdrs { + cMemberAddr, err := s.vm.state.GetShortIDLink(ids.ShortID(vdr.NodeID), state.ShortLinkKeyRegisterNode) + if err != nil { + return fmt.Errorf("failed to get consortium member address: %w", err) + } + + addrStr, err := address.Format("P", constants.GetHRP(s.vm.ctx.NetworkID), cMemberAddr[:]) + if err != nil { + return fmt.Errorf("failed to format consortium member address: %w", err) + } + + reply.Validators[vdr.NodeID] = ConsortiumMemberValidator{ + ValidatorWeight: utilsjson.Uint64(vdr.Weight), + ConsortiumMemberAddress: addrStr, + } + } return nil } diff --git a/vms/platformvm/camino_service_test.go b/vms/platformvm/camino_service_test.go index 13cbd35ccaf0..1cc160bae778 100644 --- a/vms/platformvm/camino_service_test.go +++ b/vms/platformvm/camino_service_test.go @@ -173,7 +173,7 @@ func TestGetCaminoBalance(t *testing.T) { } func defaultCaminoService(t *testing.T, camino api.Camino, utxos []api.UTXO) *CaminoService { - vm := newCaminoVM(camino, utxos) + vm := newCaminoVM(camino, utxos, nil) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() diff --git a/vms/platformvm/camino_vm_test.go b/vms/platformvm/camino_vm_test.go index 66041fead6d0..e2bea9d718a6 100644 --- a/vms/platformvm/camino_vm_test.go +++ b/vms/platformvm/camino_vm_test.go @@ -21,8 +21,10 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/blocks" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/genesis" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/reward" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -64,7 +66,7 @@ func TestRemoveDeferredValidator(t *testing.T) { }, } - vm := newCaminoVM(caminoGenesisConf, genesisUTXOs) + vm := newCaminoVM(caminoGenesisConf, genesisUTXOs, nil) vm.ctx.Lock.Lock() defer func() { require.NoError(vm.Shutdown(context.Background())) @@ -262,7 +264,7 @@ func TestRemoveReactivatedValidator(t *testing.T) { }, } - vm := newCaminoVM(caminoGenesisConf, genesisUTXOs) + vm := newCaminoVM(caminoGenesisConf, genesisUTXOs, nil) vm.ctx.Lock.Lock() defer func() { require.NoError(vm.Shutdown(context.Background())) @@ -475,7 +477,7 @@ func TestDepositsAutoUnlock(t *testing.T) { vm := newCaminoVM(caminoGenesisConf, []api.UTXO{{ Amount: json.Uint64(depositOffer.MinAmount + defaultTxFee), Address: depositOwnerAddrBech32, - }}) + }}, nil) vm.ctx.Lock.Lock() defer func() { require.NoError(vm.Shutdown(context.Background())) }() //nolint:lint @@ -528,7 +530,202 @@ func TestDepositsAutoUnlock(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) } +func TestProposals(t *testing.T) { + proposerKey, proposerAddr, _ := generateKeyAndOwner(t) + proposerAddrStr, err := address.FormatBech32(constants.NetworkIDToHRP[testNetworkID], proposerAddr.Bytes()) + require.NoError(t, err) + caminoPreFundedKey0AddrStr, err := address.FormatBech32(constants.NetworkIDToHRP[testNetworkID], caminoPreFundedKeys[0].Address().Bytes()) + require.NoError(t, err) + + defaultConfig := defaultCaminoConfig(true) + proposalBondAmount := defaultConfig.CaminoConfig.DACProposalBondAmount + newFee := (defaultTxFee + 7) * 10 + + type vote struct { + option uint32 + success bool // proposal is successful after this vote + } + + tests := map[string]struct { + feeOptions []uint64 + winningOption uint32 + earlyFinish bool + votes []vote // no more than 5 votes, cause we have only 5 validators + }{ + "Early success: 1|3 votes": { + feeOptions: []uint64{1, newFee}, + winningOption: 1, + earlyFinish: true, + votes: []vote{ + {option: 1}, + {option: 1}, + {option: 0, success: true}, + {option: 1, success: true}, + }, + }, + "Early fail: 2|2|1 votes, not reaching mostVoted threshold and being ambiguous": { + feeOptions: []uint64{1, 2, 3}, + earlyFinish: true, + votes: []vote{ + {option: 0}, + {option: 0}, + {option: 1, success: true}, + {option: 1}, + {option: 2}, + }, + }, + "Success: 0|2|1 votes": { + feeOptions: []uint64{1, newFee, 17}, + winningOption: 1, + votes: []vote{ + {option: 1}, + {option: 1}, + {option: 2, success: true}, + }, + }, + "Fail: 0 votes": { + feeOptions: []uint64{1}, + votes: []vote{}, + }, + "Fail: 2|1|1 votes, not reaching mostVoted threshold": { + feeOptions: []uint64{1, 2, 3}, + votes: []vote{ + {option: 0}, + {option: 0}, + {option: 1, success: true}, + {option: 2}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + balance := proposalBondAmount + defaultTxFee*(uint64(len(tt.votes))+1) + newFee + + // Prepare vm + vm := newCaminoVM(api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + InitialAdmin: caminoPreFundedKeys[0].Address(), + }, []api.UTXO{ + { + Amount: json.Uint64(balance), + Address: proposerAddrStr, + }, + { + Amount: json.Uint64(defaultTxFee), + Address: caminoPreFundedKey0AddrStr, + }, + }, &defaultConfig.BanffTime) + vm.ctx.Lock.Lock() + defer func() { require.NoError(vm.Shutdown(context.Background())) }() //nolint:lint + checkBalance(t, vm.state, proposerAddr, + balance, // total + 0, 0, 0, balance, // unlocked + ) + + fee := defaultTxFee + burnedAmt := uint64(0) + + // Give proposer address role to make proposals + addrStateTx, err := vm.txBuilder.NewAddressStateTx( + proposerAddr, + false, + txs.AddressStateBitCaminoProposer, + []*secp256k1.PrivateKey{caminoPreFundedKeys[0]}, + nil, + ) + require.NoError(err) + blk := buildAndAcceptBlock(t, vm, addrStateTx) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), addrStateTx.ID()) + + // Add proposal + chainTime := vm.state.GetTimestamp() + proposalTx := buildBaseFeeProposalTx(t, vm, proposerKey, proposalBondAmount, fee, + proposerKey, tt.feeOptions, chainTime.Add(100*time.Second), chainTime.Add(200*time.Second)) + proposalState, nextProposalIDsToExpire, nexExpirationTime, proposalIDsToFinish := makeProposalWithTx(t, vm, proposalTx) + require.EqualValues(5, proposalState.TotalAllowedVoters) // all 5 validators must vote + require.Equal([]ids.ID{proposalTx.ID()}, nextProposalIDsToExpire) // we have only one proposal + require.Equal(proposalState.EndTime(), nexExpirationTime) + require.Empty(proposalIDsToFinish) // no early-finished proposals + burnedAmt += fee + checkBalance(t, vm.state, proposerAddr, + balance-burnedAmt, // total + proposalBondAmount, // bonded + 0, 0, balance-proposalBondAmount-burnedAmt, // unlocked + ) + + // Fast-forward clock to time a bit forward, but still before proposals start + // Try to vote on proposal, expect to fail + vm.clock.Set(proposalState.StartTime().Add(-time.Second)) + addVoteTx := buildSimpleVoteTx(t, vm, proposerKey, fee, proposalTx.ID(), caminoPreFundedKeys[0], 0) + require.Error(vm.Builder.AddUnverifiedTx(addVoteTx)) + vm.clock.Set(proposalState.StartTime()) + + optionWeights := make([]uint32, len(proposalState.Options)) + for i, vote := range tt.votes { + optionWeights[vote.option]++ + voteTx := buildSimpleVoteTx(t, vm, proposerKey, fee, proposalTx.ID(), caminoPreFundedKeys[i], vote.option) + proposalIDsToFinish, proposalState = voteOnBaseFeeWithTx(t, vm, voteTx, proposalTx.ID(), optionWeights) + if tt.earlyFinish && i == len(tt.votes)-1 { + require.Equal([]ids.ID{proposalTx.ID()}, proposalIDsToFinish) // proposal has finished early + } else { + require.Empty(proposalIDsToFinish) // no early-finished proposals + } + nextProposalIDsToExpire, nexExpirationTime, err := vm.state.GetNextToExpireProposalIDsAndTime(nil) + require.NoError(err) + require.Equal([]ids.ID{proposalTx.ID()}, nextProposalIDsToExpire) + require.Equal(proposalState.EndTime(), nexExpirationTime) + require.Equal(tt.earlyFinish && i == len(tt.votes)-1, proposalState.CanBeFinished()) + require.Equal(vote.success, proposalState.IsSuccessful()) + burnedAmt += fee + checkBalance(t, vm.state, proposerAddr, + balance-burnedAmt, // total + proposalBondAmount, // bonded + 0, 0, balance-proposalBondAmount-burnedAmt, // unlocked + ) + } + + if !tt.earlyFinish { // no early finish + vm.clock.Set(proposalState.EndTime()) + } + + blk = buildAndAcceptBlock(t, vm, nil) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), blk.Txs()[0].ID()) + _, err = vm.state.GetProposal(proposalTx.ID()) + require.ErrorIs(err, database.ErrNotFound) + _, _, err = vm.state.GetNextToExpireProposalIDsAndTime(nil) + require.ErrorIs(err, database.ErrNotFound) + proposalIDsToFinish, err = vm.state.GetProposalIDsToFinish() + require.NoError(err) + require.Empty(proposalIDsToFinish) + checkBalance(t, vm.state, proposerAddr, + balance-burnedAmt, // total + 0, 0, 0, balance-burnedAmt, // unlocked + ) + + if len(tt.votes) != 0 && tt.votes[len(tt.votes)-1].success { // last vote + fee = tt.feeOptions[tt.winningOption] + baseFee, err := vm.state.GetBaseFee() + require.NoError(err) + require.Equal(fee, baseFee) // fee has changed + } + + // Create arbitrary tx to verify which fee is used + buildAndAcceptBaseTx(t, vm, proposerKey, fee) + burnedAmt += fee + checkBalance(t, vm.state, proposerAddr, + balance-burnedAmt, // total + 0, 0, 0, balance-burnedAmt, // unlocked + ) + }) + } +} + func buildAndAcceptBlock(t *testing.T, vm *VM, tx *txs.Tx) blocks.Block { + t.Helper() if tx != nil { require.NoError(t, vm.Builder.AddUnverifiedTx(tx)) } @@ -544,6 +741,7 @@ func buildAndAcceptBlock(t *testing.T, vm *VM, tx *txs.Tx) blocks.Block { } func getUnlockedBalance(t *testing.T, db avax.UTXOReader, addr ids.ShortID) uint64 { + t.Helper() utxos, err := avax.GetAllUTXOs(db, set.Set[ids.ShortID]{addr: struct{}{}}) require.NoError(t, err) balance := uint64(0) @@ -554,3 +752,210 @@ func getUnlockedBalance(t *testing.T, db avax.UTXOReader, addr ids.ShortID) uint } return balance } + +func getBalance(t *testing.T, db avax.UTXOReader, addr ids.ShortID) (total, bonded, deposited, depositBonded, unlocked uint64) { + t.Helper() + utxos, err := avax.GetAllUTXOs(db, set.Set[ids.ShortID]{addr: struct{}{}}) + require.NoError(t, err) + for _, utxo := range utxos { + if out, ok := utxo.Out.(*secp256k1fx.TransferOutput); ok { + unlocked += out.Amount() + total += out.Amount() + } else { + out, ok := utxo.Out.(*locked.Out) + require.True(t, ok) + switch out.LockState() { + case locked.StateDepositedBonded: + depositBonded += out.Amount() + case locked.StateDeposited: + deposited += out.Amount() + case locked.StateBonded: + bonded += out.Amount() + } + total += out.Amount() + } + } + return +} + +func checkBalance( + t *testing.T, db avax.UTXOReader, addr ids.ShortID, + expectedTotal, expectedBonded, expectedDeposited, expectedDepositBonded, expectedUnlocked uint64, //nolint:unparam +) { + t.Helper() + total, bonded, deposited, depositBonded, unlocked := getBalance(t, db, addr) + require.Equal(t, expectedTotal, total) + require.Equal(t, expectedBonded, bonded) + require.Equal(t, expectedDeposited, deposited) + require.Equal(t, expectedDepositBonded, depositBonded) + require.Equal(t, expectedUnlocked, unlocked) +} + +func checkTx(t *testing.T, vm *VM, blkID, txID ids.ID) { + t.Helper() + state, ok := vm.manager.GetState(blkID) + require.True(t, ok) + _, txStatus, err := state.GetTx(txID) + require.NoError(t, err) + require.Equal(t, status.Committed, txStatus) + _, txStatus, err = vm.state.GetTx(txID) + require.NoError(t, err) + require.Equal(t, status.Committed, txStatus) +} + +func buildBaseFeeProposalTx( + t *testing.T, + vm *VM, + fundsKey *secp256k1.PrivateKey, + amountToBond uint64, + amountToBurn uint64, + proposerKey *secp256k1.PrivateKey, + options []uint64, + startTime time.Time, + endTime time.Time, +) *txs.Tx { + t.Helper() + ins, outs, signers, _, err := vm.txBuilder.Lock( + vm.state, + []*secp256k1.PrivateKey{fundsKey}, + amountToBond, + amountToBurn, + locked.StateBonded, + nil, nil, 0, + ) + require.NoError(t, err) + proposal := &txs.ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Options: options, + }} + proposalBytes, err := txs.Codec.Marshal(txs.Version, proposal) + require.NoError(t, err) + proposalTx, err := txs.NewSigned(&txs.AddProposalTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: outs, + }}, + ProposalPayload: proposalBytes, + ProposerAddress: proposerKey.Address(), + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + }, txs.Codec, append(signers, []*secp256k1.PrivateKey{proposerKey})) + require.NoError(t, err) + return proposalTx +} + +func makeProposalWithTx( + t *testing.T, + vm *VM, + tx *txs.Tx, +) ( + proposal *dac.BaseFeeProposalState, + nextProposalIDsToExpire []ids.ID, + nexExpirationTime time.Time, + proposalIDsToFinish []ids.ID, +) { + t.Helper() + blk := buildAndAcceptBlock(t, vm, tx) + require.Len(t, blk.Txs(), 1) + checkTx(t, vm, blk.ID(), tx.ID()) + proposalState, err := vm.state.GetProposal(tx.ID()) + require.NoError(t, err) + baseFeeProposalState, ok := proposalState.(*dac.BaseFeeProposalState) + require.True(t, ok) + nextProposalIDsToExpire, nexExpirationTime, err = vm.state.GetNextToExpireProposalIDsAndTime(nil) + require.NoError(t, err) + proposalIDsToFinish, err = vm.state.GetProposalIDsToFinish() + require.NoError(t, err) + return baseFeeProposalState, nextProposalIDsToExpire, nexExpirationTime, proposalIDsToFinish +} + +func buildSimpleVoteTx( + t *testing.T, + vm *VM, + fundsKey *secp256k1.PrivateKey, + amountToBurn uint64, + proposalID ids.ID, + voterKey *secp256k1.PrivateKey, + votedOption uint32, +) *txs.Tx { + t.Helper() + ins, outs, signers, _, err := vm.txBuilder.Lock( + vm.state, + []*secp256k1.PrivateKey{fundsKey}, + 0, + amountToBurn, + locked.StateUnlocked, + nil, nil, 0, + ) + require.NoError(t, err) + voteBytes, err := txs.Codec.Marshal(txs.Version, &txs.VoteWrapper{Vote: &dac.SimpleVote{OptionIndex: votedOption}}) + require.NoError(t, err) + addVoteTx, err := txs.NewSigned(&txs.AddVoteTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: outs, + }}, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterKey.Address(), + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + }, txs.Codec, append(signers, []*secp256k1.PrivateKey{voterKey})) + require.NoError(t, err) + return addVoteTx +} + +func voteOnBaseFeeWithTx( + t *testing.T, + vm *VM, + tx *txs.Tx, + proposalID ids.ID, + expectedVoteWeights []uint32, +) (proposalIDsToFinish []ids.ID, baseFeeProposalState *dac.BaseFeeProposalState) { + t.Helper() + blk := buildAndAcceptBlock(t, vm, tx) + require.Len(t, blk.Txs(), 1) + checkTx(t, vm, blk.ID(), tx.ID()) + proposalState, err := vm.state.GetProposal(proposalID) + require.NoError(t, err) + baseFeeProposalState, ok := proposalState.(*dac.BaseFeeProposalState) + require.True(t, ok) + require.Len(t, baseFeeProposalState.Options, len(expectedVoteWeights)) + for i := range baseFeeProposalState.Options { + require.Equal(t, expectedVoteWeights[i], baseFeeProposalState.Options[i].Weight) + } + proposalIDsToFinish, err = vm.state.GetProposalIDsToFinish() + require.NoError(t, err) + return proposalIDsToFinish, baseFeeProposalState +} + +func buildAndAcceptBaseTx( + t *testing.T, + vm *VM, + fundsKey *secp256k1.PrivateKey, + amountToBurn uint64, +) { + t.Helper() + ins, outs, signers, _, err := vm.txBuilder.Lock( + vm.state, + []*secp256k1.PrivateKey{fundsKey}, + 0, + amountToBurn, + locked.StateUnlocked, + nil, nil, 0, + ) + require.NoError(t, err) + feeTestingTx, err := txs.NewSigned(&txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: outs, + }}, txs.Codec, signers) + require.NoError(t, err) + blk := buildAndAcceptBlock(t, vm, feeTestingTx) + require.Len(t, blk.Txs(), 1) + checkTx(t, vm, blk.ID(), feeTestingTx.ID()) +} diff --git a/vms/platformvm/caminoconfig/camino_config.go b/vms/platformvm/caminoconfig/camino_config.go index ae306befc52f..b3772ad4bbbc 100644 --- a/vms/platformvm/caminoconfig/camino_config.go +++ b/vms/platformvm/caminoconfig/camino_config.go @@ -4,5 +4,5 @@ package caminoconfig type Config struct { - DaoProposalBondAmount uint64 + DACProposalBondAmount uint64 } diff --git a/vms/platformvm/config/config.go b/vms/platformvm/config/config.go index 1b3e35f38dcc..2af7966c39bc 100644 --- a/vms/platformvm/config/config.go +++ b/vms/platformvm/config/config.go @@ -112,6 +112,9 @@ type Config struct { // Time of the Athens Phase network upgrade AthensPhaseTime time.Time + // Time of the BerlinPhase network upgrade + BerlinPhaseTime time.Time + // Subnet ID --> Minimum portion of the subnet's stake this node must be // connected to in order to report healthy. // [constants.PrimaryNetworkID] is always a key in this map. @@ -149,6 +152,10 @@ func (c *Config) IsAthensPhaseActivated(timestamp time.Time) bool { return !timestamp.Before(c.AthensPhaseTime) } +func (c *Config) IsBerlinPhaseActivated(timestamp time.Time) bool { + return !timestamp.Before(c.BerlinPhaseTime) +} + func (c *Config) GetCreateBlockchainTxFee(timestamp time.Time) uint64 { if c.IsApricotPhase3Activated(timestamp) { return c.CreateBlockchainTxFee diff --git a/vms/platformvm/dac/camino_change_base_fee_proposal.go b/vms/platformvm/dac/camino_change_base_fee_proposal.go new file mode 100644 index 000000000000..dba54fdbe0dc --- /dev/null +++ b/vms/platformvm/dac/camino_change_base_fee_proposal.go @@ -0,0 +1,193 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "bytes" + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/ids" + "golang.org/x/exp/slices" +) + +const baseFeeProposalMaxOptionsCount = 3 + +var ( + _ Proposal = (*BaseFeeProposal)(nil) + _ ProposalState = (*BaseFeeProposalState)(nil) + + errZeroFee = errors.New("base-fee option is zero") + errWrongOptionsCount = errors.New("wrong options count") +) + +type BaseFeeProposal struct { + Options []uint64 `serialize:"true"` // New base fee options + Start uint64 `serialize:"true"` // Start time of proposal + End uint64 `serialize:"true"` // End time of proposal +} + +func (p *BaseFeeProposal) StartTime() time.Time { + return time.Unix(int64(p.Start), 0) +} + +func (p *BaseFeeProposal) EndTime() time.Time { + return time.Unix(int64(p.End), 0) +} + +func (p *BaseFeeProposal) GetOptions() any { + return p.Options +} + +func (p *BaseFeeProposal) Verify() error { + switch { + case len(p.Options) > baseFeeProposalMaxOptionsCount: + return fmt.Errorf("%w (expected: no more than %d, actual: %d)", errWrongOptionsCount, baseFeeProposalMaxOptionsCount, len(p.Options)) + case p.Start >= p.End: + return errEndNotAfterStart + } + for _, fee := range p.Options { + if fee == 0 { + return errZeroFee + } + } + return nil +} + +func (p *BaseFeeProposal) CreateProposalState(allowedVoters []ids.ShortID) ProposalState { + stateProposal := &BaseFeeProposalState{ + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: make([]SimpleVoteOption[uint64], len(p.Options)), + }, + Start: p.Start, + End: p.End, + AllowedVoters: allowedVoters, + TotalAllowedVoters: uint32(len(allowedVoters)), + } + for i := range p.Options { + stateProposal.Options[i].Value = p.Options[i] + } + return stateProposal +} + +func (p *BaseFeeProposal) Visit(visitor VerifierVisitor) error { + return visitor.BaseFeeProposal(p) +} + +type BaseFeeProposalState struct { + SimpleVoteOptions[uint64] `serialize:"true"` // New base fee options + // Start time of proposal + Start uint64 `serialize:"true"` + // End time of proposal + End uint64 `serialize:"true"` + // Addresses that are allowed to vote for this proposal + AllowedVoters []ids.ShortID `serialize:"true"` + // Number of addresses that were initially allowed to vote for this proposal. + // This is used to calculate thresholds like "half of total voters". + TotalAllowedVoters uint32 `serialize:"true"` +} + +func (p *BaseFeeProposalState) StartTime() time.Time { + return time.Unix(int64(p.Start), 0) +} + +func (p *BaseFeeProposalState) EndTime() time.Time { + return time.Unix(int64(p.End), 0) +} + +func (p *BaseFeeProposalState) IsActiveAt(time time.Time) bool { + timestamp := uint64(time.Unix()) + return p.Start <= timestamp && timestamp <= p.End +} + +func (p *BaseFeeProposalState) CanBeFinished() bool { + mostVotedWeight, _, unambiguous := p.GetMostVoted() + voted := p.Voted() + return voted == p.TotalAllowedVoters || unambiguous && mostVotedWeight > p.TotalAllowedVoters/2 +} + +func (p *BaseFeeProposalState) IsSuccessful() bool { + mostVotedWeight, _, unambiguous := p.GetMostVoted() + voted := p.Voted() + return unambiguous && voted > p.TotalAllowedVoters/2 && mostVotedWeight > voted/2 +} + +func (p *BaseFeeProposalState) Outcome() any { + _, mostVotedOptionIndex, unambiguous := p.GetMostVoted() + if !unambiguous { + return -1 + } + return mostVotedOptionIndex +} + +// Votes must be valid for this proposal, could panic otherwise. +func (p *BaseFeeProposalState) Result() (uint64, uint32, bool) { + mostVotedWeight, mostVotedOptionIndex, unambiguous := p.GetMostVoted() + return p.Options[mostVotedOptionIndex].Value, mostVotedWeight, unambiguous +} + +// Will return modified proposal with added vote, original proposal will not be modified! +func (p *BaseFeeProposalState) AddVote(voterAddress ids.ShortID, voteIntf Vote) (ProposalState, error) { + vote, ok := voteIntf.(*SimpleVote) + if !ok { + return nil, ErrWrongVote + } + if int(vote.OptionIndex) >= len(p.Options) { + return nil, ErrWrongVote + } + + voterAddrPos, allowedToVote := slices.BinarySearchFunc(p.AllowedVoters, voterAddress, func(id, other ids.ShortID) int { + return bytes.Compare(id[:], other[:]) + }) + if !allowedToVote { + return nil, ErrNotAllowedToVoteOnProposal + } + + updatedProposal := &BaseFeeProposalState{ + Start: p.Start, + End: p.End, + AllowedVoters: make([]ids.ShortID, len(p.AllowedVoters)-1), + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: make([]SimpleVoteOption[uint64], len(p.Options)), + }, + TotalAllowedVoters: p.TotalAllowedVoters, + } + // we can't use the same slice, cause we need to change its elements + copy(updatedProposal.AllowedVoters, p.AllowedVoters[:voterAddrPos]) + updatedProposal.AllowedVoters = append(updatedProposal.AllowedVoters[:voterAddrPos], p.AllowedVoters[voterAddrPos+1:]...) + // we can't use the same slice, cause we need to change its element + copy(updatedProposal.Options, p.Options) + updatedProposal.Options[vote.OptionIndex].Weight++ + return updatedProposal, nil +} + +// Will return modified proposal with added vote ignoring allowed voters, original proposal will not be modified! +func (p *BaseFeeProposalState) ForceAddVote(voterAddress ids.ShortID, voteIntf Vote) (ProposalState, error) { //nolint:revive + vote, ok := voteIntf.(*SimpleVote) + if !ok { + return nil, ErrWrongVote + } + if int(vote.OptionIndex) >= len(p.Options) { + return nil, ErrWrongVote + } + + updatedProposal := &BaseFeeProposalState{ + Start: p.Start, + End: p.End, + AllowedVoters: p.AllowedVoters, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: make([]SimpleVoteOption[uint64], len(p.Options)), + }, + TotalAllowedVoters: p.TotalAllowedVoters, + } + // we can't use the same slice, cause we need to change its element + copy(updatedProposal.Options, p.Options) + updatedProposal.Options[vote.OptionIndex].Weight++ + return updatedProposal, nil +} + +func (p *BaseFeeProposalState) Visit(visitor ExecutorVisitor) error { + return visitor.BaseFeeProposal(p) +} diff --git a/vms/platformvm/dac/camino_change_base_fee_proposal_test.go b/vms/platformvm/dac/camino_change_base_fee_proposal_test.go new file mode 100644 index 000000000000..a7a353a623bc --- /dev/null +++ b/vms/platformvm/dac/camino_change_base_fee_proposal_test.go @@ -0,0 +1,324 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/stretchr/testify/require" +) + +func TestBaseFeeProposalCreateProposalState(t *testing.T) { + tests := map[string]struct { + proposal *BaseFeeProposal + allowedVoters []ids.ShortID + expectedProposalState ProposalState + expectedProposal *BaseFeeProposal + }{ + "OK: even number of allowed voters": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{123, 555, 7}, + }, + allowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}}, + expectedProposalState: &BaseFeeProposalState{ + Start: 100, + End: 101, + AllowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 123}, + {Value: 555}, + {Value: 7}, + }, + }, + TotalAllowedVoters: 4, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{123, 555, 7}, + }, + }, + "OK: odd number of allowed voters": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{123, 555, 7}, + }, + allowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}, {5}}, + expectedProposalState: &BaseFeeProposalState{ + Start: 100, + End: 101, + AllowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}, {5}}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 123}, + {Value: 555}, + {Value: 7}, + }, + }, + TotalAllowedVoters: 5, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{123, 555, 7}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + proposalState := tt.proposal.CreateProposalState(tt.allowedVoters) + require.Equal(t, tt.expectedProposal, tt.proposal) + require.Equal(t, tt.expectedProposalState, proposalState) + }) + } +} + +func TestBaseFeeProposalStateAddVote(t *testing.T) { + voterAddr1 := ids.ShortID{1} + voterAddr2 := ids.ShortID{1} + voterAddr3 := ids.ShortID{1} + + tests := map[string]struct { + proposal *BaseFeeProposalState + voterAddr ids.ShortID + vote Vote + expectedUpdatedProposal ProposalState + expectedOriginalProposal *BaseFeeProposalState + expectedErr error + }{ + "Wrong vote type": { + proposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &DummyVote{}, // not *SimpleVote + expectedOriginalProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + expectedErr: ErrWrongVote, + }, + "Wrong vote option index": { + proposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: ids.ShortID{3}, + vote: &SimpleVote{OptionIndex: 3}, + expectedOriginalProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + expectedErr: ErrWrongVote, + }, + "Not allowed to vote on this proposal": { + proposal: &BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{{1}, {2}}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{{Value: 1}}, + }, + }, + voterAddr: ids.ShortID{3}, + vote: &SimpleVote{OptionIndex: 0}, + expectedOriginalProposal: &BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{{1}, {2}}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{{Value: 1}}, + }, + }, + expectedErr: ErrNotAllowedToVoteOnProposal, + }, + "OK: adding vote to not voted option": { + proposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &SimpleVote{OptionIndex: 1}, + expectedUpdatedProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 1}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + }, + }, + expectedOriginalProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + }, + "OK: adding vote to already voted option": { + proposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &SimpleVote{OptionIndex: 2}, + expectedUpdatedProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 2}, // 2 + }, + }, + }, + expectedOriginalProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 10, Weight: 2}, // 0 + {Value: 20, Weight: 0}, // 1 + {Value: 30, Weight: 1}, // 2 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + }, + "OK: voter addr in the middle of allowedVoters array": { + proposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{{Value: 1}}, + }, + }, + voterAddr: voterAddr2, + vote: &SimpleVote{OptionIndex: 0}, + expectedUpdatedProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{{Value: 1, Weight: 1}}, + }, + }, + expectedOriginalProposal: &BaseFeeProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{{Value: 1}}, + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + updatedProposal, err := tt.proposal.AddVote(tt.voterAddr, tt.vote) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedUpdatedProposal, updatedProposal) + require.Equal(t, tt.expectedOriginalProposal, tt.proposal) + }) + } +} diff --git a/vms/platformvm/dac/camino_proposal.go b/vms/platformvm/dac/camino_proposal.go new file mode 100644 index 000000000000..8f368fbf26c9 --- /dev/null +++ b/vms/platformvm/dac/camino_proposal.go @@ -0,0 +1,54 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "errors" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/verify" +) + +var ( + errEndNotAfterStart = errors.New("proposal end-time is not after start-time") + ErrWrongVote = errors.New("this proposal can't be voted with this vote") + ErrNotAllowedToVoteOnProposal = errors.New("this address has already voted or not allowed to vote on this proposal") +) + +type VerifierVisitor interface { + BaseFeeProposal(*BaseFeeProposal) error +} + +type ExecutorVisitor interface { + BaseFeeProposal(*BaseFeeProposalState) error +} + +type Proposal interface { + verify.Verifiable + + StartTime() time.Time + EndTime() time.Time + GetOptions() any + CreateProposalState(allowedVoters []ids.ShortID) ProposalState + Visit(VerifierVisitor) error +} + +type ProposalState interface { + verify.Verifiable + + EndTime() time.Time + IsActiveAt(time time.Time) bool + // Once a proposal has become Finishable, it cannot be undone by adding more votes. + CanBeFinished() bool + IsSuccessful() bool // should be called only for finished proposals + Outcome() any // should be called only for finished successful proposals + Visit(ExecutorVisitor) error + // Will return modified ProposalState with added vote, original ProposalState will not be modified! + AddVote(voterAddress ids.ShortID, vote Vote) (ProposalState, error) + + // Will return modified proposal with added vote ignoring allowed voters, original proposal will not be modified! + // (used in magellan) + ForceAddVote(voterAddress ids.ShortID, voteIntf Vote) (ProposalState, error) +} diff --git a/vms/platformvm/dac/camino_simple_vote.go b/vms/platformvm/dac/camino_simple_vote.go new file mode 100644 index 000000000000..c72d7f14c798 --- /dev/null +++ b/vms/platformvm/dac/camino_simple_vote.go @@ -0,0 +1,92 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "errors" + + "github.com/ava-labs/avalanchego/utils/set" +) + +var ( + _ Vote = (*SimpleVote)(nil) + + errNoOptions = errors.New("no options") + errNotUniqueOption = errors.New("not unique option") +) + +type SimpleVote struct { + OptionIndex uint32 `serialize:"true"` // Index of voted option +} + +func (v *SimpleVote) VotedOptions() any { + return []uint32{v.OptionIndex} +} + +func (*SimpleVote) Verify() error { + return nil +} + +type SimpleVoteOption[T any] struct { + Value T `serialize:"true"` // Value that this option represents + Weight uint32 `serialize:"true"` // How much this option was voted +} + +type SimpleVoteOptions[T comparable] struct { + Options []SimpleVoteOption[T] `serialize:"true"` + mostVotedWeight uint32 // Weight of most voted option + mostVotedOptionIndex uint32 // Index of most voted option + unambiguous bool // True, if there is an option with weight > then other options weight +} + +func (p *SimpleVoteOptions[T]) Verify() error { + if len(p.Options) == 0 { + return errNoOptions + } + unique := set.NewSet[T](len(p.Options)) + for _, option := range p.Options { + if unique.Contains(option.Value) { + return errNotUniqueOption + } + unique.Add(option.Value) + } + return nil +} + +func (p SimpleVoteOptions[T]) GetMostVoted() ( + mostVotedWeight uint32, + mostVotedIndex uint32, + unambiguous bool, +) { + if p.mostVotedWeight != 0 { + return p.mostVotedWeight, p.mostVotedOptionIndex, p.unambiguous + } + + unambiguous = true + mostVotedIndexInt := 0 + weights := make([]int, len(p.Options)) + for optionIndex := range p.Options { + weights[optionIndex] += int(p.Options[optionIndex].Weight) + if optionIndex != mostVotedIndexInt && weights[optionIndex] == weights[mostVotedIndexInt] { + unambiguous = false + } else if weights[optionIndex] > weights[mostVotedIndexInt] { + mostVotedIndexInt = optionIndex + unambiguous = true + } + } + + p.mostVotedWeight = uint32(weights[mostVotedIndexInt]) + p.mostVotedOptionIndex = uint32(mostVotedIndexInt) + p.unambiguous = unambiguous && p.mostVotedWeight > 0 + + return p.mostVotedWeight, p.mostVotedOptionIndex, p.unambiguous +} + +func (p SimpleVoteOptions[T]) Voted() uint32 { + voted := uint32(0) + for i := range p.Options { + voted += p.Options[i].Weight + } + return voted +} diff --git a/vms/platformvm/dac/camino_simple_vote_test.go b/vms/platformvm/dac/camino_simple_vote_test.go new file mode 100644 index 000000000000..b06f48fa52f8 --- /dev/null +++ b/vms/platformvm/dac/camino_simple_vote_test.go @@ -0,0 +1,189 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSimpleVoteOptionsGetMostVoted(t *testing.T) { + tests := map[string]struct { + proposal *SimpleVoteOptions[uint64] + expectedProposal *SimpleVoteOptions[uint64] + expectedMostVotedWeight uint32 + expectedMostVotedIndex uint32 + expectedUnambiguous bool + }{ + "OK: 3 different weights, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 4}, // 0 + {Value: 2, Weight: 7}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 4}, // 0 + {Value: 2, Weight: 7}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedMostVotedWeight: 7, + expectedMostVotedIndex: 1, + expectedUnambiguous: true, + }, + "OK: 2 equal and 1 higher weight, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 5}, // 0 + {Value: 2, Weight: 7}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 5}, // 0 + {Value: 2, Weight: 7}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedMostVotedWeight: 7, + expectedMostVotedIndex: 1, + expectedUnambiguous: true, + }, + "OK: 2 equal and 1 lower weight, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 4}, // 0 + {Value: 2, Weight: 5}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 4}, // 0 + {Value: 2, Weight: 5}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedMostVotedWeight: 5, + expectedMostVotedIndex: 1, + expectedUnambiguous: false, + }, + "OK: 3 equal weights, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 5}, // 0 + {Value: 2, Weight: 5}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 5}, // 0 + {Value: 2, Weight: 5}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedMostVotedWeight: 5, + expectedMostVotedIndex: 0, + expectedUnambiguous: false, + }, + "OK: 3 different weights, first is highest, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 7}, // 0 + {Value: 2, Weight: 4}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 7}, // 0 + {Value: 2, Weight: 4}, // 1 + {Value: 3, Weight: 5}, // 2 + }, + }, + expectedMostVotedWeight: 7, + expectedMostVotedIndex: 0, + expectedUnambiguous: true, + }, + "OK: 1 non-zero weight, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 1}, // 0 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 1}, // 0 + }, + }, + expectedMostVotedWeight: 1, + expectedMostVotedIndex: 0, + expectedUnambiguous: true, + }, + "OK: 1 zero weight, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 0}, // 0 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 0}, // 0 + }, + }, + expectedMostVotedWeight: 0, + expectedMostVotedIndex: 0, + expectedUnambiguous: false, + }, + "OK: 3 zero weight, no cache": { + proposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 0}, // 0 + {Value: 2, Weight: 0}, // 0 + {Value: 3, Weight: 0}, // 0 + }, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + Options: []SimpleVoteOption[uint64]{ + {Value: 1, Weight: 0}, // 0 + {Value: 2, Weight: 0}, // 0 + {Value: 3, Weight: 0}, // 0 + }, + }, + expectedMostVotedWeight: 0, + expectedMostVotedIndex: 0, + expectedUnambiguous: false, + }, + "OK: cached result": { + proposal: &SimpleVoteOptions[uint64]{ + mostVotedWeight: 5, + mostVotedOptionIndex: 5, + unambiguous: false, + }, + expectedProposal: &SimpleVoteOptions[uint64]{ + mostVotedWeight: 5, + mostVotedOptionIndex: 5, + unambiguous: false, + }, + expectedMostVotedWeight: 5, + expectedMostVotedIndex: 5, + expectedUnambiguous: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + mostVotedWeight, mostVotedIndex, unambiguous := tt.proposal.GetMostVoted() + require.Equal(t, tt.expectedProposal, tt.proposal) + require.Equal(t, tt.expectedMostVotedWeight, mostVotedWeight) + require.Equal(t, tt.expectedMostVotedIndex, mostVotedIndex) + require.Equal(t, tt.expectedUnambiguous, unambiguous) + }) + } +} diff --git a/vms/platformvm/dac/camino_vote.go b/vms/platformvm/dac/camino_vote.go new file mode 100644 index 000000000000..b29e0fed9f01 --- /dev/null +++ b/vms/platformvm/dac/camino_vote.go @@ -0,0 +1,36 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/verify" +) + +var _ Vote = (*DummyVote)(nil) + +type Vote interface { + verify.Verifiable + + VotedOptions() any +} +type VoteWithAddr struct { + Vote `serialize:"true"` + VoterAddress ids.ShortID `serialize:"true"` +} + +type DummyVote struct { + ErrorStr string `serialize:"true"` +} + +func (v *DummyVote) Verify() error { + if v.ErrorStr != "" { + return errors.New(v.ErrorStr) + } + return nil +} + +func (*DummyVote) VotedOptions() any { return nil } diff --git a/vms/platformvm/fx/mock_fx.go b/vms/platformvm/fx/mock_fx.go index ceae71ca734d..fa027eaeea05 100644 --- a/vms/platformvm/fx/mock_fx.go +++ b/vms/platformvm/fx/mock_fx.go @@ -272,4 +272,4 @@ func (m *MockOwner) Verify() error { func (mr *MockOwnerMockRecorder) Verify() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOwner)(nil).Verify)) -} \ No newline at end of file +} diff --git a/vms/platformvm/locked/camino_lock.go b/vms/platformvm/locked/camino_lock.go index 24336b23b47d..cc765349774f 100644 --- a/vms/platformvm/locked/camino_lock.go +++ b/vms/platformvm/locked/camino_lock.go @@ -167,6 +167,13 @@ func (out *Out) Verify() error { return out.TransferableOut.Verify() } +// Used in vms/platformvm/txs/executor/camino_tx_executor.go func outputsAreEqual +func (out *Out) Equal(to any) bool { + toOut, typeAreEq := to.(*Out) + outEq, innerIsEq := out.TransferableOut.(interface{ Equal(any) bool }) + return typeAreEq && innerIsEq && out.IDs == toOut.IDs && outEq.Equal(toOut.TransferableOut) +} + type In struct { IDs `serialize:"true" json:"lockIDs"` avax.TransferableIn `serialize:"true" json:"input"` @@ -178,3 +185,10 @@ func (in *In) Verify() error { } return in.TransferableIn.Verify() } + +// Used in vms/platformvm/txs/executor/camino_tx_executor.go func inputsAreEqual +func (in *In) Equal(to any) bool { + toIn, typeAreEq := to.(*In) + inEq, innerIsEq := in.TransferableIn.(interface{ Equal(any) bool }) + return typeAreEq && innerIsEq && in.IDs == toIn.IDs && inEq.Equal(toIn.TransferableIn) +} diff --git a/vms/platformvm/metrics/camino_tx_metrics.go b/vms/platformvm/metrics/camino_tx_metrics.go index f1dec6ccf408..66829b50c0b7 100644 --- a/vms/platformvm/metrics/camino_tx_metrics.go +++ b/vms/platformvm/metrics/camino_tx_metrics.go @@ -21,7 +21,10 @@ type caminoTxMetrics struct { numRewardsImportTxs, numBaseTxs, numMultisigAliasTxs, - numAddDepositOfferTxs prometheus.Counter + numAddDepositOfferTxs, + numAddProposalTxs, + numAddVoteTxs, + numFinishProposalsTxs prometheus.Counter } func newCaminoTxMetrics( @@ -46,6 +49,9 @@ func newCaminoTxMetrics( numBaseTxs: newTxMetric(namespace, "base", registerer, &errs), numMultisigAliasTxs: newTxMetric(namespace, "multisig_alias", registerer, &errs), numAddDepositOfferTxs: newTxMetric(namespace, "add_deposit_offer", registerer, &errs), + numAddProposalTxs: newTxMetric(namespace, "add_proposal", registerer, &errs), + numAddVoteTxs: newTxMetric(namespace, "add_vote", registerer, &errs), + numFinishProposalsTxs: newTxMetric(namespace, "finish_proposals", registerer, &errs), } return m, errs.Err } @@ -88,6 +94,18 @@ func (*txMetrics) AddDepositOfferTx(*txs.AddDepositOfferTx) error { return nil } +func (*txMetrics) AddProposalTx(*txs.AddProposalTx) error { + return nil +} + +func (*txMetrics) AddVoteTx(*txs.AddVoteTx) error { + return nil +} + +func (*txMetrics) FinishProposalsTx(*txs.FinishProposalsTx) error { + return nil +} + // camino metrics func (m *caminoTxMetrics) AddressStateTx(*txs.AddressStateTx) error { @@ -134,3 +152,18 @@ func (m *caminoTxMetrics) AddDepositOfferTx(*txs.AddDepositOfferTx) error { m.numAddDepositOfferTxs.Inc() return nil } + +func (m *caminoTxMetrics) AddProposalTx(*txs.AddProposalTx) error { + m.numAddProposalTxs.Inc() + return nil +} + +func (m *caminoTxMetrics) AddVoteTx(*txs.AddVoteTx) error { + m.numAddVoteTxs.Inc() + return nil +} + +func (m *caminoTxMetrics) FinishProposalsTx(*txs.FinishProposalsTx) error { + m.numFinishProposalsTxs.Inc() + return nil +} diff --git a/vms/platformvm/state/camino.go b/vms/platformvm/state/camino.go index 8b6e4d2dfe04..8c5227c0d15a 100644 --- a/vms/platformvm/state/camino.go +++ b/vms/platformvm/state/camino.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/database/linkeddb" "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/ids" - choices "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" @@ -23,6 +23,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/genesis" "github.com/ava-labs/avalanchego/vms/platformvm/locked" @@ -37,19 +38,23 @@ const ( shortLinksCacheSize = 1024 msigOwnersCacheSize = 16_384 claimablesCacheSize = 1024 + proposalsCacheSize = 1024 ) var ( _ CaminoState = (*caminoState)(nil) - caminoPrefix = []byte("camino") - addressStatePrefix = []byte("addressState") - depositOffersPrefix = []byte("depositOffers") - depositsPrefix = []byte("deposits") - depositIDsByEndtimePrefix = []byte("depositIDsByEndtime") - multisigOwnersPrefix = []byte("multisigOwners") - shortLinksPrefix = []byte("shortLinks") - claimablesPrefix = []byte("claimables") + caminoPrefix = []byte("camino") + addressStatePrefix = []byte("addressState") + depositOffersPrefix = []byte("depositOffers") + depositsPrefix = []byte("deposits") + depositIDsByEndtimePrefix = []byte("depositIDsByEndtime") + multisigOwnersPrefix = []byte("multisigOwners") + shortLinksPrefix = []byte("shortLinks") + claimablesPrefix = []byte("claimables") + proposalsPrefix = []byte("proposals") + proposalIDsByEndtimePrefix = []byte("proposalIDsByEndtime") + proposalIDsToFinishPrefix = []byte("proposalIDsToFinish") // Used for prefixing the validatorsDB deferredPrefix = []byte("deferred") @@ -57,6 +62,7 @@ var ( nodeSignatureKey = []byte("nodeSignature") depositBondModeKey = []byte("depositBondMode") notDistributedValidatorRewardKey = []byte("notDistributedValidatorReward") + baseFeeKey = []byte("baseFee") errWrongTxType = errors.New("unexpected tx type") errNonExistingOffer = errors.New("deposit offer doesn't exist") @@ -68,6 +74,10 @@ type CaminoApply interface { } type CaminoDiff interface { + // Singletones + GetBaseFee() (uint64, error) + SetBaseFee(uint64) + // Address State SetAddressStates(ids.ShortID, txs.AddressState) @@ -114,6 +124,22 @@ type CaminoDiff interface { PutDeferredValidator(staker *Staker) DeleteDeferredValidator(staker *Staker) GetDeferredStakerIterator() (StakerIterator, error) + + // DAC proposals and votes + + // proposal should never be nil + AddProposal(proposalID ids.ID, proposal dac.ProposalState) + // proposal start and end time should never be modified, proposal should never be nil + ModifyProposal(proposalID ids.ID, proposal dac.ProposalState) + // proposal start and end time should never be modified, proposal should never be nil + RemoveProposal(proposalID ids.ID, proposal dac.ProposalState) + GetProposal(proposalID ids.ID) (dac.ProposalState, error) + GetProposalIterator() (ProposalsIterator, error) + AddProposalIDToFinish(proposalID ids.ID) + GetProposalIDsToFinish() ([]ids.ID, error) + RemoveProposalIDToFinish(ids.ID) + GetNextProposalExpirationTime(removedProposalIDs set.Set[ids.ID]) (time.Time, error) + GetNextToExpireProposalIDsAndTime(removedProposalIDs set.Set[ids.ID]) ([]ids.ID, time.Time, error) } // For state and diff @@ -149,7 +175,10 @@ type caminoDiff struct { modifiedMultisigAliases map[ids.ShortID]*multisig.AliasWithNonce modifiedShortLinks map[ids.ID]*ids.ShortID modifiedClaimables map[ids.ID]*Claimable + modifiedProposals map[ids.ID]*proposalDiff + modifiedProposalIDsToFinish map[ids.ID]bool modifiedNotDistributedValidatorReward *uint64 + modifiedBaseFee *uint64 } type caminoState struct { @@ -159,6 +188,7 @@ type caminoState struct { genesisSynced bool verifyNodeSignature bool lockModeBondDeposit bool + baseFee uint64 // Deferred Stakers deferredStakers *baseStakers @@ -192,16 +222,26 @@ type caminoState struct { notDistributedValidatorReward uint64 claimablesDB database.Database claimablesCache cache.Cacher[ids.ID, *Claimable] + + proposalsNextExpirationTime *time.Time + proposalsNextToExpireIDs []ids.ID + proposalIDsToFinish []ids.ID + proposalsCache cache.Cacher[ids.ID, dac.ProposalState] + proposalsDB database.Database + proposalIDsByEndtimeDB database.Database + proposalIDsToFinishDB database.Database } func newCaminoDiff() *caminoDiff { return &caminoDiff{ - modifiedAddressStates: make(map[ids.ShortID]txs.AddressState), - modifiedDepositOffers: make(map[ids.ID]*deposit.Offer), - modifiedDeposits: make(map[ids.ID]*depositDiff), - modifiedMultisigAliases: make(map[ids.ShortID]*multisig.AliasWithNonce), - modifiedShortLinks: make(map[ids.ID]*ids.ShortID), - modifiedClaimables: make(map[ids.ID]*Claimable), + modifiedAddressStates: make(map[ids.ShortID]txs.AddressState), + modifiedDepositOffers: make(map[ids.ID]*deposit.Offer), + modifiedDeposits: make(map[ids.ID]*depositDiff), + modifiedMultisigAliases: make(map[ids.ShortID]*multisig.AliasWithNonce), + modifiedShortLinks: make(map[ids.ID]*ids.ShortID), + modifiedClaimables: make(map[ids.ID]*Claimable), + modifiedProposals: make(map[ids.ID]*proposalDiff), + modifiedProposalIDsToFinish: make(map[ids.ID]bool), } } @@ -251,6 +291,15 @@ func newCaminoState(baseDB, validatorsDB database.Database, metricsReg prometheu return nil, err } + proposalsCache, err := metercacher.New[ids.ID, dac.ProposalState]( + "proposals_cache", + metricsReg, + &cache.LRU[ids.ID, dac.ProposalState]{Size: proposalsCacheSize}, + ) + if err != nil { + return nil, err + } + deferredValidatorsDB := prefixdb.New(deferredPrefix, validatorsDB) return &caminoState{ @@ -284,6 +333,11 @@ func newCaminoState(baseDB, validatorsDB database.Database, metricsReg prometheu deferredValidatorsDB: deferredValidatorsDB, deferredValidatorList: linkeddb.NewDefault(deferredValidatorsDB), + proposalsCache: proposalsCache, + proposalsDB: prefixdb.New(proposalsPrefix, baseDB), + proposalIDsByEndtimeDB: prefixdb.New(proposalIDsByEndtimePrefix, baseDB), + proposalIDsToFinishDB: prefixdb.New(proposalIDsToFinishPrefix, baseDB), + caminoDB: prefixdb.New(caminoPrefix, baseDB), caminoDiff: newCaminoDiff(), }, nil @@ -487,12 +541,27 @@ func (cs *caminoState) Load(s *state) error { } cs.lockModeBondDeposit = mode + baseFee, err := database.GetUInt64(cs.caminoDB, baseFeeKey) + if err == database.ErrNotFound { + // if baseFee is not in db yet, than its first time when we access it + // and it should be equal to config base fee + config, err := s.Config() + if err != nil { + return err + } + baseFee = config.TxFee + } else if err != nil { + return err + } + cs.baseFee = baseFee + errs := wrappers.Errs{} errs.Add( cs.loadDepositOffers(), cs.loadDeposits(), cs.loadValidatorRewards(), cs.loadDeferredValidators(s), + cs.loadProposals(), ) return errs.Err } @@ -507,6 +576,7 @@ func (cs *caminoState) Write() error { ) } errs.Add( + database.PutUInt64(cs.caminoDB, baseFeeKey, cs.baseFee), cs.writeAddressStates(), cs.writeDepositOffers(), cs.writeDeposits(), @@ -514,6 +584,7 @@ func (cs *caminoState) Write() error { cs.writeShortLinks(), cs.writeClaimableAndValidatorRewards(), cs.writeDeferredStakers(), + cs.writeProposals(), ) return errs.Err } @@ -530,6 +601,17 @@ func (cs *caminoState) Close() error { cs.shortLinksDB.Close(), cs.claimablesDB.Close(), cs.deferredValidatorsDB.Close(), + cs.proposalsDB.Close(), + cs.proposalIDsByEndtimeDB.Close(), + cs.proposalIDsToFinishDB.Close(), ) return errs.Err } + +func (cs *caminoState) GetBaseFee() (uint64, error) { + return cs.baseFee, nil +} + +func (cs *caminoState) SetBaseFee(baseFee uint64) { + cs.baseFee = baseFee +} diff --git a/vms/platformvm/state/camino_diff.go b/vms/platformvm/state/camino_diff.go index baf6c6d93301..9b507820da2e 100644 --- a/vms/platformvm/state/camino_diff.go +++ b/vms/platformvm/state/camino_diff.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -390,11 +391,257 @@ func (d *diff) GetDeferredStakerIterator() (StakerIterator, error) { return d.caminoDiff.deferredStakerDiffs.GetStakerIterator(parentIterator), nil } +func (d *diff) AddProposal(proposalID ids.ID, proposal dac.ProposalState) { + d.caminoDiff.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal, added: true} +} + +func (d *diff) ModifyProposal(proposalID ids.ID, proposal dac.ProposalState) { + d.caminoDiff.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal} +} + +func (d *diff) RemoveProposal(proposalID ids.ID, proposal dac.ProposalState) { + d.caminoDiff.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal, removed: true} +} + +func (d *diff) GetProposal(proposalID ids.ID) (dac.ProposalState, error) { + if proposalDiff, ok := d.caminoDiff.modifiedProposals[proposalID]; ok { + if proposalDiff.removed { + return nil, database.ErrNotFound + } + return proposalDiff.Proposal, nil + } + + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + return parentState.GetProposal(proposalID) +} + +func (d *diff) AddProposalIDToFinish(proposalID ids.ID) { + d.caminoDiff.modifiedProposalIDsToFinish[proposalID] = true +} + +func (d *diff) RemoveProposalIDToFinish(proposalID ids.ID) { + d.caminoDiff.modifiedProposalIDsToFinish[proposalID] = false +} + +func (d *diff) GetProposalIDsToFinish() ([]ids.ID, error) { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + parentProposalIDsToFinish, err := parentState.GetProposalIDsToFinish() + if err != nil { + return nil, err + } + + if len(d.caminoDiff.modifiedProposalIDsToFinish) == 0 { + return parentProposalIDsToFinish, nil + } + + uniqueProposalIDsToFinish := set.Set[ids.ID]{} + + for _, proposalID := range parentProposalIDsToFinish { + if notRemoved, ok := d.caminoDiff.modifiedProposalIDsToFinish[proposalID]; !ok || notRemoved { + uniqueProposalIDsToFinish.Add(proposalID) + } + } + for proposalID, added := range d.caminoDiff.modifiedProposalIDsToFinish { + if added { + uniqueProposalIDsToFinish.Add(proposalID) + } + } + + proposalIDsToFinish := uniqueProposalIDsToFinish.List() + utils.Sort(proposalIDsToFinish) + + return proposalIDsToFinish, nil +} + +func (d *diff) GetNextProposalExpirationTime(removedProposalIDs set.Set[ids.ID]) (time.Time, error) { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return time.Time{}, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + if proposalDiff.removed { + removedProposalIDs.Add(proposalID) + } + } + + nextExpirationTime, err := parentState.GetNextProposalExpirationTime(removedProposalIDs) + if err != nil && err != database.ErrNotFound { + return time.Time{}, err + } + + // calculating earliest expiration time from added proposals and parent expiration time + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + proposalEndtime := proposalDiff.Proposal.EndTime() + if proposalDiff.added && proposalEndtime.Before(nextExpirationTime) && !removedProposalIDs.Contains(proposalID) { + nextExpirationTime = proposalEndtime + } + } + + // no proposals + if nextExpirationTime.Equal(mockable.MaxTime) { + return mockable.MaxTime, database.ErrNotFound + } + + return nextExpirationTime, nil +} + +func (d *diff) GetNextToExpireProposalIDsAndTime(removedProposalIDs set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return nil, time.Time{}, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + if proposalDiff.removed { + removedProposalIDs.Add(proposalID) + } + } + + parentNextProposalIDs, parentNextExpirationTime, err := parentState.GetNextToExpireProposalIDsAndTime(removedProposalIDs) + if err != nil && err != database.ErrNotFound { + return nil, time.Time{}, err + } + + // calculating earliest expiration time from added proposals and parent expiration time + nextExpirationTime := parentNextExpirationTime + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + proposalEndtime := proposalDiff.Proposal.EndTime() + if proposalDiff.added && proposalEndtime.Before(nextExpirationTime) && !removedProposalIDs.Contains(proposalID) { + nextExpirationTime = proposalEndtime + } + } + + // no proposals + if nextExpirationTime.Equal(mockable.MaxTime) { + return nil, mockable.MaxTime, database.ErrNotFound + } + + var nextProposalIDs []ids.ID + if !parentNextExpirationTime.After(nextExpirationTime) { + nextProposalIDs = parentNextProposalIDs + } + + // getting added proposals with endtime matching nextExpirationTime + needSort := false + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + if proposalDiff.added && proposalDiff.Proposal.EndTime().Equal(nextExpirationTime) && !removedProposalIDs.Contains(proposalID) { + nextProposalIDs = append(nextProposalIDs, proposalID) + needSort = true + } + } + + if needSort { + utils.Sort(nextProposalIDs) + } + + return nextProposalIDs, nextExpirationTime, nil +} + +func (d *diff) GetProposalIterator() (ProposalsIterator, error) { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + parentIterator, err := parentState.GetProposalIterator() + if err != nil { + return nil, err + } + + return &diffProposalsIterator{ + parentIterator: parentIterator, + modifiedProposals: d.caminoDiff.modifiedProposals, + }, nil +} + +var _ ProposalsIterator = (*diffProposalsIterator)(nil) + +type diffProposalsIterator struct { + parentIterator ProposalsIterator + modifiedProposals map[ids.ID]*proposalDiff + err error +} + +func (it *diffProposalsIterator) Next() bool { + for it.parentIterator.Next() { + proposalID, err := it.parentIterator.key() + if err != nil { // should never happen + it.err = err + return false + } + if proposalDiff, ok := it.modifiedProposals[proposalID]; !ok || !proposalDiff.removed { + return true + } + } + return false +} + +func (it *diffProposalsIterator) Value() (dac.ProposalState, error) { + proposalID, err := it.parentIterator.key() + if err != nil { // should never happen + return nil, err + } + if proposalDiff, ok := it.modifiedProposals[proposalID]; ok { + return proposalDiff.Proposal, nil + } + return it.parentIterator.Value() +} + +func (it *diffProposalsIterator) Error() error { + parentIteratorErr := it.parentIterator.Error() + switch { + case parentIteratorErr != nil && it.err != nil: + return fmt.Errorf("%w, %s", it.err, parentIteratorErr) + case parentIteratorErr == nil && it.err != nil: + return it.err + case parentIteratorErr != nil && it.err == nil: + return parentIteratorErr + } + return nil +} + +func (it *diffProposalsIterator) Release() { + it.parentIterator.Release() +} + +func (it *diffProposalsIterator) key() (ids.ID, error) { + return it.parentIterator.key() // err should never happen +} + +func (d *diff) GetBaseFee() (uint64, error) { + if d.caminoDiff.modifiedBaseFee != nil { + return *d.caminoDiff.modifiedBaseFee, nil + } + + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return 0, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + return parentState.GetBaseFee() +} + +func (d *diff) SetBaseFee(baseFee uint64) { + d.caminoDiff.modifiedBaseFee = &baseFee +} + // Finally apply all changes func (d *diff) ApplyCaminoState(baseState State) { if d.caminoDiff.modifiedNotDistributedValidatorReward != nil { baseState.SetNotDistributedValidatorReward(*d.caminoDiff.modifiedNotDistributedValidatorReward) } + if d.caminoDiff.modifiedBaseFee != nil { + baseState.SetBaseFee(*d.caminoDiff.modifiedBaseFee) + } for k, v := range d.caminoDiff.modifiedAddressStates { baseState.SetAddressStates(k, v) @@ -428,6 +675,25 @@ func (d *diff) ApplyCaminoState(baseState State) { baseState.SetClaimable(ownerID, claimable) } + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + switch { + case proposalDiff.added: + baseState.AddProposal(proposalID, proposalDiff.Proposal) + case proposalDiff.removed: + baseState.RemoveProposal(proposalID, proposalDiff.Proposal) + default: + baseState.ModifyProposal(proposalID, proposalDiff.Proposal) + } + } + + for proposalID, added := range d.caminoDiff.modifiedProposalIDsToFinish { + if added { + baseState.AddProposalIDToFinish(proposalID) + } else { + baseState.RemoveProposalIDToFinish(proposalID) + } + } + for _, validatorDiffs := range d.caminoDiff.deferredStakerDiffs.validatorDiffs { for _, validatorDiff := range validatorDiffs { switch validatorDiff.validatorStatus { diff --git a/vms/platformvm/state/camino_diff_test.go b/vms/platformvm/state/camino_diff_test.go index 71a263f79786..a85ddf3a7412 100644 --- a/vms/platformvm/state/camino_diff_test.go +++ b/vms/platformvm/state/camino_diff_test.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -3046,8 +3047,2084 @@ func TestDiffGetDeferredStakerIterator(t *testing.T) { } } +func TestDiffGetProposal(t *testing.T) { + parentStateID := ids.ID{1} + proposalID := ids.ID{1, 1} + proposal := &dac.BaseFeeProposalState{} + testErr := errors.New("test err") + + tests := map[string]struct { + diff func(*gomock.Controller) *diff + proposalID ids.ID + expectedDiff func(*diff) *diff + expectedProposal dac.ProposalState + expectedErr error + }{ + "Fail: proposal removed": { + diff: func(c *gomock.Controller) *diff { + return &diff{ + stateVersions: NewMockVersions(c), + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, removed: true}, + }, + }, + } + }, + proposalID: proposalID, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, removed: true}, + }, + }, + } + }, + expectedErr: database.ErrNotFound, + }, + "OK: proposal modified": { + diff: func(c *gomock.Controller) *diff { + return &diff{ + stateVersions: NewMockVersions(c), + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal}, + }, + }, + } + }, + proposalID: proposalID, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal}, + }, + }, + } + }, + expectedProposal: proposal, + }, + "OK: proposal added": { + diff: func(c *gomock.Controller) *diff { + return &diff{ + stateVersions: NewMockVersions(c), + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, added: true}, + }, + }, + } + }, + proposalID: proposalID, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, added: true}, + }, + }, + } + }, + expectedProposal: proposal, + }, + "OK: proposal in parent state": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposal(proposalID).Return(proposal, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedProposal: proposal, + }, + "Fail: parent errored": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposal(proposalID).Return(nil, testErr) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedErr: testErr, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualDiff := tt.diff(ctrl) + actualProposal, err := actualDiff.GetProposal(proposalID) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedProposal, actualProposal) + require.Equal(t, tt.expectedDiff(actualDiff), actualDiff) + }) + } +} + +func TestDiffAddProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + diff *diff + proposalID ids.ID + proposal dac.ProposalState + expectedDiff *diff + }{ + "OK": { + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }}, + proposalID: proposalID, + proposal: proposal, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, added: true}, + }, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.AddProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + +func TestDiffModifyProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + diff *diff + proposalID ids.ID + proposal dac.ProposalState + expectedDiff *diff + }{ + "OK": { + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }}, + proposalID: proposalID, + proposal: proposal, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal}, + }, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.ModifyProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + +func TestDiffRemoveProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + diff *diff + proposalID ids.ID + proposal dac.ProposalState + expectedDiff *diff + }{ + "OK": { + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }}, + proposalID: proposalID, + proposal: proposal, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, removed: true}, + }, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.RemoveProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + +func TestDiffAddProposalIDToFinish(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + + tests := map[string]struct { + diff *diff + proposalID ids.ID + expectedDiff *diff + }{ + "OK": { + proposalID: proposalID3, + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }}, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + proposalID3: true, + }, + }}, + }, + "OK: already exist": { + proposalID: proposalID2, + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }}, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.AddProposalIDToFinish(tt.proposalID) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + +func TestDiffRemoveProposalIDToFinish(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + + tests := map[string]struct { + diff *diff + proposalID ids.ID + expectedDiff *diff + }{ + "OK": { + proposalID: proposalID3, + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }}, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + proposalID3: false, + }, + }}, + }, + "OK: not exist": { + proposalID: proposalID2, + diff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }}, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.RemoveProposalIDToFinish(tt.proposalID) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + +func TestDiffGetProposalIDsToFinish(t *testing.T) { + parentStateID := ids.ID{1, 1} + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + proposalID4 := ids.ID{4} + proposalID5 := ids.ID{5} + proposalID6 := ids.ID{6} + testErr := errors.New("test err") + + tests := map[string]struct { + diff func(*gomock.Controller) *diff + expectedDiff func(actualDiff *diff) *diff + expectedProposalIDsToFinish []ids.ID + expectedErr error + }{ + "Parent errored": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposalIDsToFinish().Return(nil, testErr) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedErr: testErr, + }, + "OK: no proposals to finish": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposalIDsToFinish().Return(nil, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedProposalIDsToFinish: nil, + }, + "OK: only parent proposals to finish": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{proposalID1, proposalID2}, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID2}, + }, + "OK: only diff proposals to finish": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposalIDsToFinish().Return(nil, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: false, + proposalID3: true, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: false, + proposalID3: true, + }, + }, + } + }, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID3}, + }, + "OK": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{proposalID1, proposalID2, proposalID3}, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID2: false, + proposalID4: true, + proposalID5: false, + proposalID6: true, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID2: false, + proposalID4: true, + proposalID5: false, + proposalID6: true, + }, + }, + } + }, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID3, proposalID4, proposalID6}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + diff := tt.diff(ctrl) + proposalIDsToFinish, err := diff.GetProposalIDsToFinish() + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedProposalIDsToFinish, proposalIDsToFinish) + require.Equal(t, tt.expectedDiff(diff), diff) + }) + } +} + +func TestDiffGetNextProposalExpirationTime(t *testing.T) { + parentStateID := ids.ID{1} + earlyProposalTxID1 := ids.ID{1, 1} + earlyProposalTxID2 := ids.ID{1, 2} + midProposalTxID := ids.ID{1, 11} + lateProposalTxID1 := ids.ID{1, 101} + lateProposalTxID2 := ids.ID{1, 102} + earlyProposal := &dac.BaseFeeProposalState{End: 101} + midProposal := &dac.BaseFeeProposalState{End: 102} + lateProposal := &dac.BaseFeeProposalState{End: 103} + testErr := errors.New("test err") + + tests := map[string]struct { + diff func(*gomock.Controller, set.Set[ids.ID]) *diff + removedProposalIDs set.Set[ids.ID] + expectedNextExpirationTime time.Time + expectedDiff func(*diff) *diff + expectedErr error + }{ + "Fail: parent errored": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(nil).Return(time.Time{}, testErr) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: time.Time{}, + expectedErr: testErr, + }, + "Fail: no proposals": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(nil).Return(mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "Fail: proposals in parent state only, but all removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1, earlyProposalTxID2) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK: proposals in parent state only, but one removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but all parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but one parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but one removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added only": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in parent state only": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early, mid) and parent state (late)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + midProposalTxID: {Proposal: midProposal, added: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + midProposalTxID: {Proposal: midProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "Fail: proposals in parent state only, but all removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "Fail: proposals in parent state only, but all removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK: proposals in added (late) and parent state (early), but all parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but all parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but some parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but some parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but some removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but some removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextProposalExpirationTime(removedProposalIDs). + Return(lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualDiff := tt.diff(ctrl, tt.removedProposalIDs) + nextExpirationTime, err := actualDiff.GetNextProposalExpirationTime(tt.removedProposalIDs) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedNextExpirationTime, nextExpirationTime) + require.Equal(t, tt.expectedDiff(actualDiff), actualDiff) + }) + } +} + +func TestDiffGetNextToExpireProposalIDsAndTime(t *testing.T) { + parentStateID := ids.ID{1} + earlyProposalTxID1 := ids.ID{1, 1} + earlyProposalTxID2 := ids.ID{1, 2} + earlyProposalTxID3 := ids.ID{1, 3} + midProposalTxID := ids.ID{1, 10} + lateProposalTxID1 := ids.ID{1, 101} + lateProposalTxID2 := ids.ID{1, 102} + lateProposalTxID3 := ids.ID{1, 103} + earlyProposal := &dac.BaseFeeProposalState{End: 101} + midProposal := &dac.BaseFeeProposalState{End: 102} + lateProposal := &dac.BaseFeeProposalState{End: 103} + testErr := errors.New("test err") + + tests := map[string]struct { + diff func(*gomock.Controller, set.Set[ids.ID]) *diff + removedProposalIDs set.Set[ids.ID] + expectedDiff func(*diff) *diff + expectedNextToExpireIDs []ids.ID + expectedNextExpirationTime time.Time + expectedErr error + }{ + "Fail: parent errored": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return(nil, time.Time{}, testErr) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: time.Time{}, + expectedErr: testErr, + }, + "Fail: no proposals": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "Fail: proposals in parent state only, but all removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1, earlyProposalTxID2) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK: proposals in parent state only, but one removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID2}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID2}, + }, + "OK: proposals in added (late) and parent state (early), but all parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1, earlyProposalTxID2) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{lateProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + earlyProposalTxID2: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but one parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID2}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID2}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1, lateProposalTxID2) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + lateProposalTxID2: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + lateProposalTxID2: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but one parent removed": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{lateProposalTxID2}, lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added only (early, late)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs).Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in parent state only": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs).Return( + []ids.ID{earlyProposalTxID1, earlyProposalTxID2}, + earlyProposal.EndTime(), + nil, + ) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1, earlyProposalTxID2}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs).Return( + []ids.ID{earlyProposalTxID1}, + earlyProposal.EndTime(), + nil, + ) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early, mid) and parent state (late)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs).Return( + []ids.ID{lateProposalTxID1}, + lateProposal.EndTime(), + nil, + ) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + midProposalTxID: {Proposal: midProposal, added: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + midProposalTxID: {Proposal: midProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early1, late) and parent state (early2)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs).Return( + []ids.ID{earlyProposalTxID2}, + earlyProposal.EndTime(), + nil, + ) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1, earlyProposalTxID2}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "Fail: proposals in parent state only, but all removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK: proposals in parent state only, but one removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID2}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID2}, + }, + "OK: proposals in added (late) and parent state (early), but all parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{lateProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but one parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID2}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID1: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID2}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID1: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but one parent removed in arg": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{lateProposalTxID2}, lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID1: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "Fail: proposals in parent state only, but all removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK: proposals in parent state only, but some removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID3}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID3}, + }, + "OK: proposals in added (late) and parent state (early), but all parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, database.ErrNotFound) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{lateProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: lateProposal.EndTime(), + }, + "OK: proposals in added (late) and parent state (early), but some parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(earlyProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{earlyProposalTxID3}, earlyProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + earlyProposalTxID2: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID3}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + lateProposalTxID1: {Proposal: lateProposal, added: true}, + earlyProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but all parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return(nil, mockable.MaxTime, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID2: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + "OK: proposals in added (early) and parent state (late), but some parent removed (one in arg, one in diff)": { + diff: func(c *gomock.Controller, removedProposalIDs set.Set[ids.ID]) *diff { + removedProposalIDs.Add(lateProposalTxID1) + parentState := NewMockChain(c) + parentState.EXPECT().GetNextToExpireProposalIDsAndTime(removedProposalIDs). + Return([]ids.ID{lateProposalTxID3}, lateProposal.EndTime(), nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + lateProposalTxID2: struct{}{}, + }, + expectedNextToExpireIDs: []ids.ID{earlyProposalTxID1}, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + earlyProposalTxID1: {Proposal: earlyProposal, added: true}, + lateProposalTxID1: {Proposal: lateProposal, removed: true}, + }, + }, + } + }, + expectedNextExpirationTime: earlyProposal.EndTime(), + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualDiff := tt.diff(ctrl, tt.removedProposalIDs) + nextToExpireIDs, nextExpirationTime, err := actualDiff.GetNextToExpireProposalIDsAndTime(tt.removedProposalIDs) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedNextExpirationTime, nextExpirationTime) + require.Equal(t, tt.expectedNextToExpireIDs, nextToExpireIDs) + require.Equal(t, tt.expectedDiff(actualDiff), actualDiff) + }) + } +} + +func TestDiffGetBaseFee(t *testing.T) { + parentStateID := ids.ID{123} + baseFee := uint64(123) + testErr := errors.New("test err") + + tests := map[string]struct { + diff func(*gomock.Controller) *diff + expectedDiff func(actualDiff *diff) *diff + expectedBaseFee uint64 + expectedErr error + }{ + "OK: modified": { + diff: func(c *gomock.Controller) *diff { + return &diff{caminoDiff: &caminoDiff{ + modifiedBaseFee: &baseFee, + }} + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{caminoDiff: &caminoDiff{ + modifiedBaseFee: &baseFee, + }} + }, + expectedBaseFee: baseFee, + }, + "OK: in parent": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetBaseFee().Return(baseFee, nil) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + caminoDiff: &caminoDiff{}, + parentID: parentStateID, + stateVersions: actualDiff.stateVersions, + } + }, + expectedBaseFee: baseFee, + }, + "Fail: parent errored": { + diff: func(c *gomock.Controller) *diff { + parentState := NewMockChain(c) + parentState.EXPECT().GetBaseFee().Return(uint64(0), testErr) + return &diff{ + stateVersions: newMockStateVersions(c, parentStateID, parentState), + parentID: parentStateID, + caminoDiff: &caminoDiff{}, + } + }, + expectedDiff: func(actualDiff *diff) *diff { + return &diff{ + stateVersions: actualDiff.stateVersions, + parentID: actualDiff.parentID, + caminoDiff: &caminoDiff{}, + } + }, + expectedErr: testErr, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualDiff := tt.diff(ctrl) + baseFee, err := actualDiff.GetBaseFee() + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedBaseFee, baseFee) + require.Equal(t, tt.expectedDiff(actualDiff), actualDiff) + }) + } +} + +func TestDiffSetBaseFee(t *testing.T) { + baseFee := uint64(123) + + tests := map[string]struct { + diff *diff + baseFee uint64 + expectedDiff *diff + }{ + "OK": { + diff: &diff{caminoDiff: &caminoDiff{}}, + baseFee: baseFee, + expectedDiff: &diff{caminoDiff: &caminoDiff{ + modifiedBaseFee: &baseFee, + }}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.diff.SetBaseFee(tt.baseFee) + require.Equal(t, tt.expectedDiff, tt.diff) + }) + } +} + func TestDiffApplyCaminoState(t *testing.T) { reward := uint64(12345) + baseFee := uint64(6789) tests := map[string]struct { diff *diff state func(*gomock.Controller, *diff) *MockState @@ -3080,12 +5157,36 @@ func TestDiffApplyCaminoState(t *testing.T) { {12}: {ValidatorReward: 112}, {13}: nil, }, + modifiedProposals: map[ids.ID]*proposalDiff{ + {14}: {Proposal: &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1, Weight: 1}}, + }, + }}, + {15}: {Proposal: &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{{115}}, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 2}}, + }, + }, added: true}, + {16}: {Proposal: &dac.BaseFeeProposalState{ + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 3}}, + }, + }, removed: true}, + }, + modifiedProposalIDsToFinish: map[ids.ID]bool{ + {17}: true, {18}: false, {19}: true, {20}: false, + }, modifiedNotDistributedValidatorReward: &reward, + modifiedBaseFee: &baseFee, deferredStakerDiffs: diffStakers{}, }}, state: func(c *gomock.Controller, d *diff) *MockState { s := NewMockState(c) s.EXPECT().SetNotDistributedValidatorReward(*d.caminoDiff.modifiedNotDistributedValidatorReward) + s.EXPECT().SetBaseFee(*d.caminoDiff.modifiedBaseFee) for k, v := range d.caminoDiff.modifiedAddressStates { s.EXPECT().SetAddressStates(k, v) } @@ -3112,6 +5213,23 @@ func TestDiffApplyCaminoState(t *testing.T) { for ownerID, claimable := range d.caminoDiff.modifiedClaimables { s.EXPECT().SetClaimable(ownerID, claimable) } + for proposalID, proposalDiff := range d.caminoDiff.modifiedProposals { + switch { + case proposalDiff.added: + s.EXPECT().AddProposal(proposalID, proposalDiff.Proposal) + case proposalDiff.removed: + s.EXPECT().RemoveProposal(proposalID, proposalDiff.Proposal) + default: + s.EXPECT().ModifyProposal(proposalID, proposalDiff.Proposal) + } + } + for proposalID, added := range d.caminoDiff.modifiedProposalIDsToFinish { + if added { + s.EXPECT().AddProposalIDToFinish(proposalID) + } else { + s.EXPECT().RemoveProposalIDToFinish(proposalID) + } + } for _, validatorDiffs := range d.caminoDiff.deferredStakerDiffs.validatorDiffs { for _, validatorDiff := range validatorDiffs { switch validatorDiff.validatorStatus { @@ -3150,8 +5268,31 @@ func TestDiffApplyCaminoState(t *testing.T) { {12}: {ValidatorReward: 112}, {13}: nil, }, + modifiedProposals: map[ids.ID]*proposalDiff{ + {14}: {Proposal: &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1, Weight: 1}}, + }, + }}, + {15}: {Proposal: &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{{115}}, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 2}}, + }, + }, added: true}, + {16}: {Proposal: &dac.BaseFeeProposalState{ + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 3}}, + }, + }, removed: true}, + }, + modifiedProposalIDsToFinish: map[ids.ID]bool{ + {17}: true, {18}: false, {19}: true, {20}: false, + }, deferredStakerDiffs: diffStakers{}, modifiedNotDistributedValidatorReward: &reward, + modifiedBaseFee: &baseFee, }}, }, } diff --git a/vms/platformvm/state/camino_helpers_test.go b/vms/platformvm/state/camino_helpers_test.go index 112b6d471bdf..f7aa7aa632bd 100644 --- a/vms/platformvm/state/camino_helpers_test.go +++ b/vms/platformvm/state/camino_helpers_test.go @@ -55,8 +55,7 @@ func generateBaseTx(assetID ids.ID, amount uint64, outputOwners secp256k1fx.Outp func newEmptyState(t *testing.T) *state { vdrs := validators.NewManager() - primaryVdrs := validators.NewSet() - _ = vdrs.Add(constants.PrimaryNetworkID, primaryVdrs) + _ = vdrs.Add(constants.PrimaryNetworkID, validators.NewSet()) newState, err := new( memdb.New(), metrics.Noop, diff --git a/vms/platformvm/state/camino_proposal.go b/vms/platformvm/state/camino_proposal.go new file mode 100644 index 000000000000..b7792c03ec27 --- /dev/null +++ b/vms/platformvm/state/camino_proposal.go @@ -0,0 +1,415 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "encoding/binary" + "fmt" + "math" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/platformvm/blocks" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +type proposalStateWrapper struct { + dac.ProposalState `serialize:"true"` +} + +type proposalDiff struct { + Proposal dac.ProposalState + added, removed bool +} + +func (cs *caminoState) AddProposal(proposalID ids.ID, proposal dac.ProposalState) { + cs.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal, added: true} +} + +func (cs *caminoState) ModifyProposal(proposalID ids.ID, proposal dac.ProposalState) { + cs.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal} + cs.proposalsCache.Evict(proposalID) +} + +func (cs *caminoState) RemoveProposal(proposalID ids.ID, proposal dac.ProposalState) { + cs.modifiedProposals[proposalID] = &proposalDiff{Proposal: proposal, removed: true} + cs.proposalsCache.Evict(proposalID) +} + +func (cs *caminoState) GetProposal(proposalID ids.ID) (dac.ProposalState, error) { + if proposalDiff, ok := cs.modifiedProposals[proposalID]; ok { + if proposalDiff.removed { + return nil, database.ErrNotFound + } + return proposalDiff.Proposal, nil + } + + if proposal, ok := cs.proposalsCache.Get(proposalID); ok { + if proposal == nil { + return nil, database.ErrNotFound + } + return proposal, nil + } + + proposalBytes, err := cs.proposalsDB.Get(proposalID[:]) + if err == database.ErrNotFound { + cs.proposalsCache.Put(proposalID, nil) + return nil, err + } else if err != nil { + return nil, err + } + + proposal := &proposalStateWrapper{} + if _, err := txs.Codec.Unmarshal(proposalBytes, proposal); err != nil { + return nil, err + } + + cs.proposalsCache.Put(proposalID, proposal.ProposalState) + + return proposal.ProposalState, nil +} + +func (cs *caminoState) AddProposalIDToFinish(proposalID ids.ID) { + cs.modifiedProposalIDsToFinish[proposalID] = true +} + +func (cs *caminoState) RemoveProposalIDToFinish(proposalID ids.ID) { + cs.modifiedProposalIDsToFinish[proposalID] = false +} + +func (cs *caminoState) GetProposalIDsToFinish() ([]ids.ID, error) { + if len(cs.modifiedProposalIDsToFinish) == 0 { + return cs.proposalIDsToFinish, nil + } + + uniqueProposalIDsToFinish := set.Set[ids.ID]{} + for _, proposalID := range cs.proposalIDsToFinish { + if notRemoved, ok := cs.modifiedProposalIDsToFinish[proposalID]; !ok || notRemoved { + uniqueProposalIDsToFinish.Add(proposalID) + } + } + for proposalID, added := range cs.modifiedProposalIDsToFinish { + if added { + uniqueProposalIDsToFinish.Add(proposalID) + } + } + + proposalIDsToFinish := uniqueProposalIDsToFinish.List() + utils.Sort(proposalIDsToFinish) + return proposalIDsToFinish, nil +} + +func (cs *caminoState) GetNextProposalExpirationTime(removedProposalIDs set.Set[ids.ID]) (time.Time, error) { + if cs.proposalsNextExpirationTime == nil { + return mockable.MaxTime, database.ErrNotFound + } + + for _, proposalID := range cs.proposalsNextToExpireIDs { + if !removedProposalIDs.Contains(proposalID) { + return *cs.proposalsNextExpirationTime, nil + } + } + + _, nextExpirationTime, err := cs.getNextToExpireProposalIDsAndTimeFromDB(removedProposalIDs) + return nextExpirationTime, err +} + +func (cs *caminoState) GetNextToExpireProposalIDsAndTime(removedProposalIDs set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + if cs.proposalsNextExpirationTime == nil { + return nil, mockable.MaxTime, database.ErrNotFound + } + + var nextToExpireIDs []ids.ID + for _, proposalID := range cs.proposalsNextToExpireIDs { + if !removedProposalIDs.Contains(proposalID) { + nextToExpireIDs = append(nextToExpireIDs, proposalID) + } + } + if len(nextToExpireIDs) > 0 { + return nextToExpireIDs, *cs.proposalsNextExpirationTime, nil + } + + return cs.getNextToExpireProposalIDsAndTimeFromDB(removedProposalIDs) +} + +func (cs *caminoState) GetProposalIterator() (ProposalsIterator, error) { + return &proposalsIterator{ + dbIterator: cs.proposalsDB.NewIterator(), + caminoState: cs, + }, nil +} + +func (cs *caminoState) writeProposals() error { + // checking if all current proposals were removed + nextIDsIsEmpty := true + for _, proposalID := range cs.proposalsNextToExpireIDs { + if proposalDiff, ok := cs.modifiedProposals[proposalID]; !ok || !proposalDiff.removed { + nextIDsIsEmpty = false + break + } + } + + // if not all current proposals were removed, we can try to update without peeking into db + var nextToExpireIDs []ids.ID + if !nextIDsIsEmpty { + // calculating earliest next unlock time + nextExpirationTime := *cs.proposalsNextExpirationTime + for _, proposalDiff := range cs.modifiedProposals { + if endtime := proposalDiff.Proposal.EndTime(); proposalDiff.added && endtime.Before(nextExpirationTime) { + nextExpirationTime = endtime + } + } + // adding current proposals + if nextExpirationTime.Equal(*cs.proposalsNextExpirationTime) { + for _, proposalID := range cs.proposalsNextToExpireIDs { + if proposalDiff, ok := cs.modifiedProposals[proposalID]; !ok || !proposalDiff.removed { + nextToExpireIDs = append(nextToExpireIDs, proposalID) + } + } + } + // adding new proposals + needSort := false // proposalIDs from db are already sorted + for proposalID, proposalDiff := range cs.modifiedProposals { + if proposalDiff.added && proposalDiff.Proposal.EndTime().Equal(nextExpirationTime) { + nextToExpireIDs = append(nextToExpireIDs, proposalID) + needSort = true + } + } + if needSort { + utils.Sort(nextToExpireIDs) + } + cs.proposalsNextToExpireIDs = nextToExpireIDs + cs.proposalsNextExpirationTime = &nextExpirationTime + } + + // adding new proposals to db, deleting removed proposals from db + for proposalID, proposalDiff := range cs.modifiedProposals { + delete(cs.modifiedProposals, proposalID) + if proposalDiff.removed { + if err := cs.proposalsDB.Delete(proposalID[:]); err != nil { + return err + } + if err := cs.proposalIDsByEndtimeDB.Delete(proposalToKey(proposalID[:], proposalDiff.Proposal)); err != nil { + return err + } + } else { + proposalBytes, err := txs.Codec.Marshal(blocks.Version, &proposalStateWrapper{ProposalState: proposalDiff.Proposal}) + if err != nil { + return fmt.Errorf("failed to serialize deposit: %w", err) + } + if err := cs.proposalsDB.Put(proposalID[:], proposalBytes); err != nil { + return err + } + if proposalDiff.added { + if err := cs.proposalIDsByEndtimeDB.Put(proposalToKey(proposalID[:], proposalDiff.Proposal), nil); err != nil { + return err + } + } + } + } + + // getting earliest proposals from db if proposalsNextToExpireIDs is empty + if len(nextToExpireIDs) == 0 { + nextToExpireIDs, nextExpirationTime, err := cs.getNextToExpireProposalIDsAndTimeFromDB(nil) + switch { + case err == database.ErrNotFound: + cs.proposalsNextToExpireIDs = nil + cs.proposalsNextExpirationTime = nil + case err != nil: + return err + default: + cs.proposalsNextToExpireIDs = nextToExpireIDs + cs.proposalsNextExpirationTime = &nextExpirationTime + } + } + + // updating db of finished proposal ids + + proposalIDsToFinish, err := cs.GetProposalIDsToFinish() + if err != nil { + return err // Won't happen cause err is always nil here. Just in case if we'll change that + } + + for proposalID, add := range cs.modifiedProposalIDsToFinish { + if add { + if err := cs.proposalIDsToFinishDB.Put(proposalID[:], nil); err != nil { + return err + } + } else { + if err := cs.proposalIDsToFinishDB.Delete(proposalID[:]); err != nil { + return err + } + } + delete(cs.modifiedProposalIDsToFinish, proposalID) + } + + cs.proposalIDsToFinish = proposalIDsToFinish + + return nil +} + +func (cs *caminoState) loadProposals() error { + cs.proposalsNextToExpireIDs = nil + cs.proposalsNextExpirationTime = nil + proposalsNextToExpireIDs, proposalsNextExpirationTime, err := cs.getNextToExpireProposalIDsAndTimeFromDB(nil) + if err == database.ErrNotFound { + return nil + } else if err != nil { + return err + } + cs.proposalsNextToExpireIDs = proposalsNextToExpireIDs + cs.proposalsNextExpirationTime = &proposalsNextExpirationTime + + // reading from db proposalIDs that are ready for execution + proposalsToFinishIterator := cs.proposalIDsToFinishDB.NewIterator() + defer proposalsToFinishIterator.Release() + + for proposalsToFinishIterator.Next() { + proposalID, err := ids.ToID(proposalsToFinishIterator.Key()) + if err != nil { + return err + } + cs.proposalIDsToFinish = append(cs.proposalIDsToFinish, proposalID) + } + + if err := proposalsToFinishIterator.Error(); err != nil { + return err + } + + return nil +} + +func (cs caminoState) getNextToExpireProposalIDsAndTimeFromDB(removedProposalIDs set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + proposalsIterator := cs.proposalIDsByEndtimeDB.NewIterator() + defer proposalsIterator.Release() + + var nextProposalIDs []ids.ID + nextProposalsEndTimestamp := uint64(math.MaxUint64) + + for proposalsIterator.Next() { + proposalID, proposalEndtime, err := bytesToProposalIDAndEndtime(proposalsIterator.Key()) + if err != nil { + return nil, time.Time{}, err + } + + if removedProposalIDs.Contains(proposalID) { + continue + } + + // we expect values to be sorted by endtime in ascending order + if proposalEndtime > nextProposalsEndTimestamp { + break + } + if proposalEndtime < nextProposalsEndTimestamp { + nextProposalsEndTimestamp = proposalEndtime + } + nextProposalIDs = append(nextProposalIDs, proposalID) + } + + if err := proposalsIterator.Error(); err != nil { + return nil, time.Time{}, err + } + + if len(nextProposalIDs) == 0 { + return nil, mockable.MaxTime, database.ErrNotFound + } + + return nextProposalIDs, time.Unix(int64(nextProposalsEndTimestamp), 0), nil +} + +// proposalID must be ids.ID 32 bytes +func proposalToKey(proposalID []byte, proposal dac.ProposalState) []byte { + proposalSortKey := make([]byte, 8+32) + binary.BigEndian.PutUint64(proposalSortKey, uint64(proposal.EndTime().Unix())) + copy(proposalSortKey[8:], proposalID) + return proposalSortKey +} + +// proposalID must be ids.ID 32 bytes +func bytesToProposalIDAndEndtime(proposalSortKeyBytes []byte) (ids.ID, uint64, error) { + proposalID, err := ids.ToID(proposalSortKeyBytes[8:]) + if err != nil { + return ids.Empty, 0, err + } + return proposalID, binary.BigEndian.Uint64(proposalSortKeyBytes[:8]), nil +} + +var _ ProposalsIterator = (*proposalsIterator)(nil) + +type ProposalsIterator interface { + Next() bool + Value() (dac.ProposalState, error) + Error() error + Release() + + key() (ids.ID, error) +} + +type proposalsIterator struct { + caminoState *caminoState + dbIterator database.Iterator + err error +} + +func (it *proposalsIterator) Next() bool { + for it.dbIterator.Next() { + proposalID, err := ids.ToID(it.dbIterator.Key()) + if err != nil { // should never happen + it.err = err + return false + } + if proposalDiff, ok := it.caminoState.modifiedProposals[proposalID]; !ok || !proposalDiff.removed { + return true + } + } + return false +} + +func (it *proposalsIterator) Value() (dac.ProposalState, error) { + proposalID, err := ids.ToID(it.dbIterator.Key()) + if err != nil { // should never happen + return nil, err + } + + if proposalDiff, ok := it.caminoState.modifiedProposals[proposalID]; ok { + return proposalDiff.Proposal, nil + } + + if proposal, ok := it.caminoState.proposalsCache.Get(proposalID); ok { + return proposal, nil + } + + proposal := &proposalStateWrapper{} + if _, err := txs.Codec.Unmarshal(it.dbIterator.Value(), proposal); err != nil { + return nil, err + } + + return proposal.ProposalState, nil +} + +func (it *proposalsIterator) Error() error { + dbIteratorErr := it.dbIterator.Error() + switch { + case dbIteratorErr != nil && it.err != nil: + return fmt.Errorf("%w, %s", it.err, dbIteratorErr) + case dbIteratorErr == nil && it.err != nil: + return it.err + case dbIteratorErr != nil && it.err == nil: + return dbIteratorErr + } + return nil +} + +func (it *proposalsIterator) Release() { + it.dbIterator.Release() +} + +func (it *proposalsIterator) key() (ids.ID, error) { + return ids.ToID(it.dbIterator.Key()) // err should never happen +} diff --git a/vms/platformvm/state/camino_proposal_test.go b/vms/platformvm/state/camino_proposal_test.go new file mode 100644 index 000000000000..38a18bc35043 --- /dev/null +++ b/vms/platformvm/state/camino_proposal_test.go @@ -0,0 +1,1149 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + "testing" + "time" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/platformvm/blocks" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +func TestGetProposal(t *testing.T) { + proposalID := ids.ID{1} + wrapper := &proposalStateWrapper{ + ProposalState: &dac.BaseFeeProposalState{ + Start: 100, + End: 100, + AllowedVoters: []ids.ShortID{{11}}, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1234, Weight: 1}}, + }, + TotalAllowedVoters: 5, + }, + } + proposalBytes, err := blocks.GenesisCodec.Marshal(blocks.Version, wrapper) + require.NoError(t, err) + testError := errors.New("test error") + + tests := map[string]struct { + caminoState func(*gomock.Controller) *caminoState + proposalID ids.ID + expectedCaminoState func(*caminoState) *caminoState + expectedProposal dac.ProposalState + expectedErr error + }{ + "Fail: proposal removed": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState, removed: true}, + }, + }, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState, removed: true}, + }, + }, + } + }, + proposalID: proposalID, + expectedErr: database.ErrNotFound, + }, + "Fail: proposal in cache, but removed": { + caminoState: func(c *gomock.Controller) *caminoState { + cache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + cache.EXPECT().Get(proposalID).Return(nil, true) + return &caminoState{ + proposalsCache: cache, + caminoDiff: &caminoDiff{}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedErr: database.ErrNotFound, + }, + "OK: proposal added": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState, added: true}, + }, + }, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState, added: true}, + }, + }, + } + }, + proposalID: proposalID, + expectedProposal: wrapper.ProposalState, + }, + "OK: proposal modified": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState}, + }, + }, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: wrapper.ProposalState}, + }, + }, + } + }, + proposalID: proposalID, + expectedProposal: wrapper.ProposalState, + }, + "OK: proposal in cache": { + caminoState: func(c *gomock.Controller) *caminoState { + cache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + cache.EXPECT().Get(proposalID).Return(wrapper.ProposalState, true) + return &caminoState{ + proposalsCache: cache, + caminoDiff: &caminoDiff{}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedProposal: wrapper.ProposalState, + }, + "OK: proposal in db": { + caminoState: func(c *gomock.Controller) *caminoState { + cache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + cache.EXPECT().Get(proposalID).Return(nil, false) + cache.EXPECT().Put(proposalID, wrapper.ProposalState) + db := database.NewMockDatabase(c) + db.EXPECT().Get(proposalID[:]).Return(proposalBytes, nil) + return &caminoState{ + proposalsDB: db, + proposalsCache: cache, + caminoDiff: &caminoDiff{}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsDB: actualCaminoState.proposalsDB, + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedProposal: wrapper.ProposalState, + }, + "Fail: db error": { + caminoState: func(c *gomock.Controller) *caminoState { + cache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + cache.EXPECT().Get(proposalID).Return(nil, false) + db := database.NewMockDatabase(c) + db.EXPECT().Get(proposalID[:]).Return(nil, testError) + return &caminoState{ + proposalsDB: db, + proposalsCache: cache, + caminoDiff: &caminoDiff{}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsDB: actualCaminoState.proposalsDB, + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{}, + } + }, + proposalID: proposalID, + expectedErr: testError, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + caminoState := tt.caminoState(ctrl) + actualProposal, err := caminoState.GetProposal(tt.proposalID) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedProposal, actualProposal) + require.Equal(t, tt.expectedCaminoState(caminoState), caminoState) + }) + } +} + +func TestAddProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + caminoState *caminoState + proposalID ids.ID + proposal dac.ProposalState + expectedCaminoState *caminoState + }{ + "OK": { + caminoState: &caminoState{ + caminoDiff: &caminoDiff{modifiedProposals: map[ids.ID]*proposalDiff{}}, + }, + proposalID: proposalID, + proposal: proposal, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, added: true}, + }, + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.caminoState.AddProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} + +func TestModifyProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal1 := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + caminoState func(*gomock.Controller) *caminoState + proposalID ids.ID + proposal dac.ProposalState + expectedCaminoState func(*caminoState) *caminoState + }{ + "OK": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsCache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + proposalsCache.EXPECT().Evict(proposalID) + return &caminoState{ + proposalsCache: proposalsCache, + caminoDiff: &caminoDiff{modifiedProposals: map[ids.ID]*proposalDiff{}}, + } + }, + proposalID: proposalID, + proposal: proposal1, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal1}, + }, + }, + } + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualCaminoState := tt.caminoState(ctrl) + actualCaminoState.ModifyProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedCaminoState(actualCaminoState), actualCaminoState) + }) + } +} + +func TestRemoveProposal(t *testing.T) { + proposalID := ids.ID{1} + proposal := &dac.BaseFeeProposalState{} + + tests := map[string]struct { + caminoState func(*gomock.Controller) *caminoState + proposalID ids.ID + proposal dac.ProposalState + expectedCaminoState func(*caminoState) *caminoState + }{ + "OK": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsCache := cache.NewMockCacher[ids.ID, dac.ProposalState](c) + proposalsCache.EXPECT().Evict(proposalID) + return &caminoState{ + proposalsCache: proposalsCache, + caminoDiff: &caminoDiff{modifiedProposals: map[ids.ID]*proposalDiff{}}, + } + }, + proposalID: proposalID, + proposal: proposal, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsCache: actualCaminoState.proposalsCache, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID: {Proposal: proposal, removed: true}, + }, + }, + } + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualCaminoState := tt.caminoState(ctrl) + actualCaminoState.RemoveProposal(tt.proposalID, tt.proposal) + require.Equal(t, tt.expectedCaminoState(actualCaminoState), actualCaminoState) + }) + } +} + +func TestAddProposalIDToFinish(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + + tests := map[string]struct { + caminoState *caminoState + proposalID ids.ID + expectedCaminoState *caminoState + }{ + "OK": { + proposalID: proposalID3, + caminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + proposalID3: true, + }, + }, + }, + }, + "OK: already exist": { + proposalID: proposalID2, + caminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.caminoState.AddProposalIDToFinish(tt.proposalID) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} + +func TestRemoveProposalIDToFinish(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + + tests := map[string]struct { + caminoState *caminoState + proposalID ids.ID + expectedCaminoState *caminoState + }{ + "OK": { + proposalID: proposalID3, + caminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + proposalID3: false, + }, + }, + }, + }, + "OK: not exist": { + proposalID: proposalID2, + caminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: false, + proposalID2: false, + }, + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.caminoState.RemoveProposalIDToFinish(tt.proposalID) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} + +func TestGetProposalIDsToFinish(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + proposalID4 := ids.ID{4} + proposalID5 := ids.ID{5} + proposalID6 := ids.ID{6} + + tests := map[string]struct { + caminoState *caminoState + expectedCaminoState *caminoState + expectedProposalIDsToFinish []ids.ID + expectedErr error + }{ + "OK: no proposals to finish": { + caminoState: &caminoState{caminoDiff: &caminoDiff{}}, + expectedCaminoState: &caminoState{caminoDiff: &caminoDiff{}}, + expectedProposalIDsToFinish: nil, + }, + "OK: no new proposals to finish": { + caminoState: &caminoState{ + caminoDiff: &caminoDiff{}, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID2}, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{}, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID2}, + }, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID2}, + }, + "OK: only new proposals to finish": { + caminoState: &caminoState{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }}, + expectedCaminoState: &caminoState{caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID1: true, + proposalID2: true, + }, + }}, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID2}, + }, + "OK": { + caminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID2: false, + proposalID4: true, + proposalID5: false, + proposalID6: true, + }, + }, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID2, proposalID3}, + }, + expectedCaminoState: &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID2: false, + proposalID4: true, + proposalID5: false, + proposalID6: true, + }, + }, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID2, proposalID3}, + }, + expectedProposalIDsToFinish: []ids.ID{proposalID1, proposalID3, proposalID4, proposalID6}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + proposalIDsToFinish, err := tt.caminoState.GetProposalIDsToFinish() + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedProposalIDsToFinish, proposalIDsToFinish) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} + +func TestGetNextProposalExpirationTime(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID31 := ids.ID{3} + proposalID32 := ids.ID{4} + proposal2 := &dac.BaseFeeProposalState{ + End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1}}, + }, + } + proposal31 := &dac.BaseFeeProposalState{ + End: 103, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 2}}, + }, + } + proposal32 := &dac.BaseFeeProposalState{ + End: 103, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 3}}, + }, + } + proposal1Endtime := time.Unix(100, 0) + + tests := map[string]struct { + caminoState func(c *gomock.Controller) *caminoState + removedProposalIDs set.Set[ids.ID] + expectedCaminoState func(*caminoState) *caminoState + expectedNextExpirationTime time.Time + expectedErr error + }{ + "Fail: no proposals": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{} + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{} + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "Fail: no proposals (all removed)": { + caminoState: func(c *gomock.Controller) *caminoState { + it := database.NewMockIterator(c) + it.EXPECT().Next().Return(false) + it.EXPECT().Error().Return(nil) + it.EXPECT().Release() + + db := database.NewMockDatabase(c) + db.EXPECT().NewIterator().Return(it) + + return &caminoState{ + proposalIDsByEndtimeDB: db, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + removedProposalIDs: set.Set[ids.ID]{proposalID1: struct{}{}}, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: proposal1Endtime, + }, + "Ok: in-mem proposals removed, but db has some": { + caminoState: func(c *gomock.Controller) *caminoState { + it := database.NewMockIterator(c) + it.EXPECT().Next().Return(true).Times(3) + it.EXPECT().Key().Return(proposalToKey(proposalID2[:], proposal2)) + it.EXPECT().Key().Return(proposalToKey(proposalID31[:], proposal31)) + it.EXPECT().Key().Return(proposalToKey(proposalID32[:], proposal32)) + it.EXPECT().Next().Return(false) + it.EXPECT().Error().Return(nil) + it.EXPECT().Release() + + db := database.NewMockDatabase(c) + db.EXPECT().NewIterator().Return(it) + + return &caminoState{ + proposalIDsByEndtimeDB: db, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + proposalID1: struct{}{}, + proposalID2: struct{}{}, + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: proposal31.EndTime(), + }, + "OK: some proposals removed": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + proposalID1: struct{}{}, + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + } + }, + expectedNextExpirationTime: proposal1Endtime, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + caminoState := tt.caminoState(ctrl) + nextExpirationTime, err := caminoState.GetNextProposalExpirationTime(tt.removedProposalIDs) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedNextExpirationTime, nextExpirationTime) + require.Equal(t, tt.expectedCaminoState(caminoState), caminoState) + }) + } +} + +func TestGetNextToExpireProposalIDsAndTime(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID31 := ids.ID{3} + proposalID32 := ids.ID{4} + proposal2 := &dac.BaseFeeProposalState{ + End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1}}, + }, + } + proposal31 := &dac.BaseFeeProposalState{ + End: 103, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 2}}, + }, + } + proposal32 := &dac.BaseFeeProposalState{ + End: 103, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 3}}, + }, + } + proposal1Endtime := time.Unix(100, 0) + + tests := map[string]struct { + caminoState func(c *gomock.Controller) *caminoState + removedProposalIDs set.Set[ids.ID] + expectedCaminoState func(*caminoState) *caminoState + expectedNextExpirationTime time.Time + expectedNextToExpireIDs []ids.ID + expectedErr error + }{ + "Fail: no proposals": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{} + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{} + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "Fail: no proposals (all removed)": { + caminoState: func(c *gomock.Controller) *caminoState { + it := database.NewMockIterator(c) + it.EXPECT().Next().Return(false) + it.EXPECT().Error().Return(nil) + it.EXPECT().Release() + + db := database.NewMockDatabase(c) + db.EXPECT().NewIterator().Return(it) + + return &caminoState{ + proposalIDsByEndtimeDB: db, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + removedProposalIDs: set.Set[ids.ID]{proposalID1: struct{}{}}, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: mockable.MaxTime, + expectedErr: database.ErrNotFound, + }, + "OK": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: proposal1Endtime, + expectedNextToExpireIDs: []ids.ID{proposalID1}, + }, + "Ok: in-mem proposals removed, but db has some": { + caminoState: func(c *gomock.Controller) *caminoState { + it := database.NewMockIterator(c) + it.EXPECT().Next().Return(true).Times(3) + it.EXPECT().Key().Return(proposalToKey(proposalID2[:], proposal2)) + it.EXPECT().Key().Return(proposalToKey(proposalID31[:], proposal31)) + it.EXPECT().Key().Return(proposalToKey(proposalID32[:], proposal32)) + it.EXPECT().Next().Return(false) + it.EXPECT().Error().Return(nil) + it.EXPECT().Release() + + db := database.NewMockDatabase(c) + db.EXPECT().NewIterator().Return(it) + + return &caminoState{ + proposalIDsByEndtimeDB: db, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + proposalID1: struct{}{}, + proposalID2: struct{}{}, + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + expectedNextExpirationTime: proposal31.EndTime(), + expectedNextToExpireIDs: []ids.ID{proposalID31, proposalID32}, + }, + "OK: some proposals removed": { + caminoState: func(c *gomock.Controller) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + } + }, + removedProposalIDs: set.Set[ids.ID]{ + proposalID1: struct{}{}, + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalsNextExpirationTime: &proposal1Endtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + } + }, + expectedNextExpirationTime: proposal1Endtime, + expectedNextToExpireIDs: []ids.ID{proposalID2}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + caminoState := tt.caminoState(ctrl) + nextToExpireIDs, nextExpirationTime, err := caminoState.GetNextToExpireProposalIDsAndTime(tt.removedProposalIDs) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedNextExpirationTime, nextExpirationTime) + require.Equal(t, tt.expectedNextToExpireIDs, nextToExpireIDs) + require.Equal(t, tt.expectedCaminoState(caminoState), caminoState) + }) + } +} + +func TestWriteProposals(t *testing.T) { + testError := errors.New("test error") + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + proposalID4 := ids.ID{4} + proposalID5 := ids.ID{5} + proposalID6 := ids.ID{6} + + proposalWrapper1 := &proposalStateWrapper{ProposalState: &dac.BaseFeeProposalState{ + End: 10, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 1}}, + }, + }} + proposalWrapper2 := &proposalStateWrapper{ProposalState: &dac.BaseFeeProposalState{ + End: 10, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 2}}, + }, + }} + proposalWrapper3 := &proposalStateWrapper{ProposalState: &dac.BaseFeeProposalState{ + End: 11, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{ + Options: []dac.SimpleVoteOption[uint64]{{Value: 3}}, + }, + }} + + proposalEndtime := proposalWrapper2.EndTime() + proposal1Bytes, err := blocks.GenesisCodec.Marshal(blocks.Version, proposalWrapper1) + require.NoError(t, err) + proposal2Bytes, err := blocks.GenesisCodec.Marshal(blocks.Version, proposalWrapper2) + require.NoError(t, err) + + tests := map[string]struct { + caminoState func(*gomock.Controller) *caminoState + expectedCaminoState func(*caminoState) *caminoState + expectedErr error + }{ + "Fail: db errored on modified proposal Put": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsDB := database.NewMockDatabase(c) + proposalsDB.EXPECT().Put(proposalID1[:], proposal1Bytes).Return(testError) + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID1: {Proposal: proposalWrapper1.ProposalState}, + }, + }, + proposalsDB: proposalsDB, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }, + proposalsDB: actualCaminoState.proposalsDB, + } + }, + expectedErr: testError, + }, + "Fail: db errored on added proposal Put": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsDB := database.NewMockDatabase(c) + proposalsDB.EXPECT().Put(proposalID1[:], proposal1Bytes).Return(testError) + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID1: {Proposal: proposalWrapper1.ProposalState, added: true}, + }, + }, + proposalsDB: proposalsDB, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }, + proposalsDB: actualCaminoState.proposalsDB, + } + }, + expectedErr: testError, + }, + "Fail: db errored on removed proposal Delete": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsDB := database.NewMockDatabase(c) + proposalsDB.EXPECT().Delete(proposalID1[:]).Return(testError) + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID1: {Proposal: proposalWrapper1.ProposalState, removed: true}, + }, + }, + proposalsDB: proposalsDB, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }, + proposalsDB: actualCaminoState.proposalsDB, + } + }, + expectedErr: testError, + }, + "OK: add or remove proposals-to-finish": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalIDsToFinishDB := database.NewMockDatabase(c) + proposalIDsToFinishDB.EXPECT().Delete(proposalID2[:]).Return(nil) + proposalIDsToFinishDB.EXPECT().Put(proposalID4[:], nil).Return(nil) + proposalIDsToFinishDB.EXPECT().Delete(proposalID5[:]).Return(nil) + proposalIDsToFinishDB.EXPECT().Put(proposalID6[:], nil).Return(nil) + + proposalsIterator := database.NewMockIterator(c) + proposalsIterator.EXPECT().Next().Return(false) + proposalsIterator.EXPECT().Error().Return(nil) + proposalsIterator.EXPECT().Release() + + proposalIDsByEndtimeDB := database.NewMockDatabase(c) + proposalIDsByEndtimeDB.EXPECT().NewIterator().Return(proposalsIterator) + + return &caminoState{ + proposalIDsToFinishDB: proposalIDsToFinishDB, + proposalIDsByEndtimeDB: proposalIDsByEndtimeDB, + caminoDiff: &caminoDiff{ + modifiedProposalIDsToFinish: map[ids.ID]bool{ + proposalID2: false, + proposalID4: true, + proposalID5: false, + proposalID6: true, + }, + }, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID2, proposalID3}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsToFinishDB: actualCaminoState.proposalIDsToFinishDB, + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + caminoDiff: &caminoDiff{modifiedProposalIDsToFinish: map[ids.ID]bool{}}, + proposalIDsToFinish: []ids.ID{proposalID1, proposalID3, proposalID4, proposalID6}, + } + }, + }, + "OK: add, modify and delete; nextExpiration partial removal, added new": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsDB := database.NewMockDatabase(c) + proposalsDB.EXPECT().Put(proposalID1[:], proposal1Bytes).Return(nil) + proposalsDB.EXPECT().Put(proposalID2[:], proposal2Bytes).Return(nil) + proposalsDB.EXPECT().Delete(proposalID3[:]).Return(nil) + + proposalIDsByEndtimeDB := database.NewMockDatabase(c) + proposalIDsByEndtimeDB.EXPECT().Put(proposalToKey(proposalID1[:], proposalWrapper1), nil).Return(nil) + proposalIDsByEndtimeDB.EXPECT().Delete(proposalToKey(proposalID3[:], proposalWrapper3)).Return(nil) + + return &caminoState{ + proposalIDsByEndtimeDB: proposalIDsByEndtimeDB, + proposalsDB: proposalsDB, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID1: {Proposal: proposalWrapper1.ProposalState, added: true}, + proposalID2: {Proposal: proposalWrapper2.ProposalState}, + proposalID3: {Proposal: proposalWrapper3.ProposalState, removed: true}, + }, + }, + proposalsNextExpirationTime: &proposalEndtime, + proposalsNextToExpireIDs: []ids.ID{proposalID2, proposalID3}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsDB: actualCaminoState.proposalsDB, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }, + proposalsNextExpirationTime: &proposalEndtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + } + }, + }, + "OK: nextExpiration full removal, can't add new, peek into db": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsDB := database.NewMockDatabase(c) + proposalsDB.EXPECT().Put(proposalID1[:], proposal1Bytes).Return(nil) + proposalsDB.EXPECT().Delete(proposalID2[:]).Return(nil) + + proposalsIterator := database.NewMockIterator(c) + proposalsIterator.EXPECT().Next().Return(true) + proposalsIterator.EXPECT().Key().Return(proposalToKey(proposalID1[:], proposalWrapper1)) + proposalsIterator.EXPECT().Next().Return(false) + proposalsIterator.EXPECT().Error().Return(nil) + proposalsIterator.EXPECT().Release() + + proposalIDsByEndtimeDB := database.NewMockDatabase(c) + proposalIDsByEndtimeDB.EXPECT().Put(proposalToKey(proposalID1[:], proposalWrapper1), nil).Return(nil) + proposalIDsByEndtimeDB.EXPECT().Delete(proposalToKey(proposalID2[:], proposalWrapper2)).Return(nil) + proposalIDsByEndtimeDB.EXPECT().NewIterator().Return(proposalsIterator) + + return &caminoState{ + proposalIDsByEndtimeDB: proposalIDsByEndtimeDB, + proposalsDB: proposalsDB, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{ + proposalID1: {Proposal: proposalWrapper1.ProposalState, added: true}, + proposalID2: {Proposal: proposalWrapper2.ProposalState, removed: true}, + }, + }, + proposalsNextExpirationTime: &proposalEndtime, + proposalsNextToExpireIDs: []ids.ID{proposalID2}, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalsDB: actualCaminoState.proposalsDB, + caminoDiff: &caminoDiff{ + modifiedProposals: map[ids.ID]*proposalDiff{}, + }, + proposalsNextExpirationTime: &proposalEndtime, + proposalsNextToExpireIDs: []ids.ID{proposalID1}, + } + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualCaminoState := tt.caminoState(ctrl) + require.ErrorIs(t, actualCaminoState.writeProposals(), tt.expectedErr) + require.Equal(t, tt.expectedCaminoState(actualCaminoState), actualCaminoState) + }) + } +} + +func TestLoadProposals(t *testing.T) { + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + proposalID4 := ids.ID{4} + + proposal1 := &dac.BaseFeeProposalState{End: 10} + proposal2 := &dac.BaseFeeProposalState{End: 10} + proposal3 := &dac.BaseFeeProposalState{End: 11} + + tests := map[string]struct { + caminoState func(*gomock.Controller) *caminoState + expectedCaminoState func(*caminoState) *caminoState + expectedErr error + }{ + "OK": { + caminoState: func(c *gomock.Controller) *caminoState { + expiredProposalsIterator := database.NewMockIterator(c) + expiredProposalsIterator.EXPECT().Next().Return(true).Times(3) + expiredProposalsIterator.EXPECT().Key().Return(proposalToKey(proposalID1[:], proposal1)) + expiredProposalsIterator.EXPECT().Key().Return(proposalToKey(proposalID2[:], proposal2)) + expiredProposalsIterator.EXPECT().Key().Return(proposalToKey(proposalID3[:], proposal3)) + expiredProposalsIterator.EXPECT().Error().Return(nil) + expiredProposalsIterator.EXPECT().Release() + + proposalIDsByEndtimeDB := database.NewMockDatabase(c) + proposalIDsByEndtimeDB.EXPECT().NewIterator().Return(expiredProposalsIterator) + + proposalsToFinishIterator := database.NewMockIterator(c) + proposalsToFinishIterator.EXPECT().Next().Return(true).Times(2) + proposalsToFinishIterator.EXPECT().Key().Return(proposalID2[:]) + proposalsToFinishIterator.EXPECT().Key().Return(proposalID4[:]) + proposalsToFinishIterator.EXPECT().Next().Return(false) + proposalsToFinishIterator.EXPECT().Error().Return(nil) + proposalsToFinishIterator.EXPECT().Release() + + proposalIDsToFinishDB := database.NewMockDatabase(c) + proposalIDsToFinishDB.EXPECT().NewIterator().Return(proposalsToFinishIterator) + + return &caminoState{ + proposalIDsByEndtimeDB: proposalIDsByEndtimeDB, + proposalIDsToFinishDB: proposalIDsToFinishDB, + } + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + nextTime := proposal1.EndTime() + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + proposalIDsToFinishDB: actualCaminoState.proposalIDsToFinishDB, + proposalsNextExpirationTime: &nextTime, + proposalsNextToExpireIDs: []ids.ID{proposalID1, proposalID2}, + proposalIDsToFinish: []ids.ID{proposalID2, proposalID4}, + } + }, + }, + "OK: no proposals": { + caminoState: func(c *gomock.Controller) *caminoState { + proposalsIterator := database.NewMockIterator(c) + proposalsIterator.EXPECT().Next().Return(false) + proposalsIterator.EXPECT().Error().Return(nil) + proposalsIterator.EXPECT().Release() + proposalIDsByEndtimeDB := database.NewMockDatabase(c) + proposalIDsByEndtimeDB.EXPECT().NewIterator().Return(proposalsIterator) + return &caminoState{proposalIDsByEndtimeDB: proposalIDsByEndtimeDB} + }, + expectedCaminoState: func(actualCaminoState *caminoState) *caminoState { + return &caminoState{ + proposalIDsByEndtimeDB: actualCaminoState.proposalIDsByEndtimeDB, + } + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + actualCaminoState := tt.caminoState(ctrl) + require.ErrorIs(t, actualCaminoState.loadProposals(), tt.expectedErr) + require.Equal(t, tt.expectedCaminoState(actualCaminoState), actualCaminoState) + }) + } +} diff --git a/vms/platformvm/state/camino_state.go b/vms/platformvm/state/camino_state.go index 7e60b8605557..41eac7751945 100644 --- a/vms/platformvm/state/camino_state.go +++ b/vms/platformvm/state/camino_state.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -140,3 +141,51 @@ func (s *state) DeleteDeferredValidator(staker *Staker) { func (s *state) GetDeferredStakerIterator() (StakerIterator, error) { return s.caminoState.GetDeferredStakerIterator() } + +func (s *state) AddProposal(proposalID ids.ID, proposal dac.ProposalState) { + s.caminoState.AddProposal(proposalID, proposal) +} + +func (s *state) ModifyProposal(proposalID ids.ID, proposal dac.ProposalState) { + s.caminoState.ModifyProposal(proposalID, proposal) +} + +func (s *state) RemoveProposal(proposalID ids.ID, proposal dac.ProposalState) { + s.caminoState.RemoveProposal(proposalID, proposal) +} + +func (s *state) GetProposal(proposalID ids.ID) (dac.ProposalState, error) { + return s.caminoState.GetProposal(proposalID) +} + +func (s *state) AddProposalIDToFinish(proposalID ids.ID) { + s.caminoState.AddProposalIDToFinish(proposalID) +} + +func (s *state) GetProposalIDsToFinish() ([]ids.ID, error) { + return s.caminoState.GetProposalIDsToFinish() +} + +func (s *state) RemoveProposalIDToFinish(proposalID ids.ID) { + s.caminoState.RemoveProposalIDToFinish(proposalID) +} + +func (s *state) GetNextToExpireProposalIDsAndTime(removedProposalIDs set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + return s.caminoState.GetNextToExpireProposalIDsAndTime(removedProposalIDs) +} + +func (s *state) GetNextProposalExpirationTime(removedProposalIDs set.Set[ids.ID]) (time.Time, error) { + return s.caminoState.GetNextProposalExpirationTime(removedProposalIDs) +} + +func (s *state) GetProposalIterator() (ProposalsIterator, error) { + return s.caminoState.GetProposalIterator() +} + +func (s *state) GetBaseFee() (uint64, error) { + return s.caminoState.GetBaseFee() +} + +func (s *state) SetBaseFee(baseFee uint64) { + s.caminoState.SetBaseFee(baseFee) +} diff --git a/vms/platformvm/state/camino_test.go b/vms/platformvm/state/camino_test.go index e5993ab99658..ef58dd0e2112 100644 --- a/vms/platformvm/state/camino_test.go +++ b/vms/platformvm/state/camino_test.go @@ -450,3 +450,46 @@ func defaultGenesisState(addresses []pvm_genesis.AddressState, deposits []*txs.T }, } } + +func TestGetBaseFee(t *testing.T) { + tests := map[string]struct { + caminoState *caminoState + expectedCaminoState *caminoState + expectedBaseFee uint64 + expectedErr error + }{ + "OK": { + caminoState: &caminoState{baseFee: 123}, + expectedCaminoState: &caminoState{baseFee: 123}, + expectedBaseFee: 123, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseFee, err := tt.caminoState.GetBaseFee() + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedBaseFee, baseFee) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} + +func TestSetBaseFee(t *testing.T) { + tests := map[string]struct { + baseFee uint64 + caminoState *caminoState + expectedCaminoState *caminoState + }{ + "OK": { + baseFee: 123, + caminoState: &caminoState{baseFee: 111}, + expectedCaminoState: &caminoState{baseFee: 123}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.caminoState.SetBaseFee(tt.baseFee) + require.Equal(t, tt.expectedCaminoState, tt.caminoState) + }) + } +} diff --git a/vms/platformvm/state/mock_chain.go b/vms/platformvm/state/mock_chain.go index a2ccb283231e..f65de5b52f32 100644 --- a/vms/platformvm/state/mock_chain.go +++ b/vms/platformvm/state/mock_chain.go @@ -16,6 +16,7 @@ import ( avax "github.com/ava-labs/avalanchego/vms/components/avax" multisig "github.com/ava-labs/avalanchego/vms/components/multisig" config "github.com/ava-labs/avalanchego/vms/platformvm/config" + dac "github.com/ava-labs/avalanchego/vms/platformvm/dac" deposit "github.com/ava-labs/avalanchego/vms/platformvm/deposit" locked "github.com/ava-labs/avalanchego/vms/platformvm/locked" status "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -280,6 +281,36 @@ func (mr *MockChainMockRecorder) GetClaimable(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaimable", reflect.TypeOf((*MockChain)(nil).GetClaimable), arg0) } +// GetProposal mocks base method. +func (m *MockChain) GetProposal(arg0 ids.ID) (dac.ProposalState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposal", arg0) + ret0, _ := ret[0].(dac.ProposalState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProposal indicates an expected call of GetProposal. +func (mr *MockChainMockRecorder) GetProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposal", reflect.TypeOf((*MockChain)(nil).GetProposal), arg0) +} + +// GetBaseFee mocks base method. +func (m *MockChain) GetBaseFee() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBaseFee") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBaseFee indicates an expected call of GetBaseFee. +func (mr *MockChainMockRecorder) GetBaseFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBaseFee", reflect.TypeOf((*MockChain)(nil).GetBaseFee)) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockChain) GetCurrentDelegatorIterator(arg0 ids.ID, arg1 ids.NodeID) (StakerIterator, error) { m.ctrl.T.Helper() @@ -386,6 +417,67 @@ func (mr *MockChainMockRecorder) GetNextToUnlockDepositIDsAndTime(arg0 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToUnlockDepositIDsAndTime", reflect.TypeOf((*MockChain)(nil).GetNextToUnlockDepositIDsAndTime), arg0) } +// GetNextProposalExpirationTime mocks base method. +func (m *MockChain) GetNextProposalExpirationTime(arg0 set.Set[ids.ID]) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextProposalExpirationTime", arg0) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNextProposalExpirationTime indicates an expected call of GetNextProposalExpirationTime. +func (mr *MockChainMockRecorder) GetNextProposalExpirationTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextProposalExpirationTime", reflect.TypeOf((*MockChain)(nil).GetNextProposalExpirationTime), arg0) +} + +// GetNextToExpireProposalIDsAndTime mocks base method. +func (m *MockChain) GetNextToExpireProposalIDsAndTime(arg0 set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextToExpireProposalIDsAndTime", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNextToExpireProposalIDsAndTime indicates an expected call of GetNextToExpireProposalIDsAndTime. +func (mr *MockChainMockRecorder) GetNextToExpireProposalIDsAndTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToExpireProposalIDsAndTime", reflect.TypeOf((*MockChain)(nil).GetNextToExpireProposalIDsAndTime), arg0) +} + +// GetProposalIDsToFinish mocks base method. +func (m *MockChain) GetProposalIDsToFinish() ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIDsToFinish") + ret0, _ := ret[0].([]ids.ID) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIDsToFinish indicates an expected call of GetProposalIDsToFinish. +func (mr *MockChainMockRecorder) GetProposalIDsToFinish() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIDsToFinish", reflect.TypeOf((*MockChain)(nil).GetProposalIDsToFinish)) +} + +// GetProposalIterator mocks base method. +func (m *MockChain) GetProposalIterator() (ProposalsIterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIterator") + ret0, _ := ret[0].(ProposalsIterator) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIterator indicates an expected call of GetProposalIterator. +func (mr *MockChainMockRecorder) GetProposalIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIterator", reflect.TypeOf((*MockChain)(nil).GetProposalIterator)) +} + // GetDepositOffer mocks base method. func (m *MockChain) GetDepositOffer(arg0 ids.ID) (*deposit.Offer, error) { m.ctrl.T.Helper() @@ -722,6 +814,78 @@ func (mr *MockChainMockRecorder) SetClaimable(arg0, arg1 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClaimable", reflect.TypeOf((*MockChain)(nil).SetClaimable), arg0, arg1) } +// AddProposal mocks base method. +func (m *MockChain) AddProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposal", arg0, arg1) +} + +// AddProposal indicates an expected call of AddProposal. +func (mr *MockChainMockRecorder) AddProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposal", reflect.TypeOf((*MockChain)(nil).AddProposal), arg0, arg1) +} + +// ModifyProposal mocks base method. +func (m *MockChain) ModifyProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ModifyProposal", arg0, arg1) +} + +// ModifyProposal indicates an expected call of ModifyProposal. +func (mr *MockChainMockRecorder) ModifyProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyProposal", reflect.TypeOf((*MockChain)(nil).ModifyProposal), arg0, arg1) +} + +// RemoveProposal mocks base method. +func (m *MockChain) RemoveProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposal", arg0, arg1) +} + +// RemoveProposal indicates an expected call of RemoveProposal. +func (mr *MockChainMockRecorder) RemoveProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposal", reflect.TypeOf((*MockChain)(nil).RemoveProposal), arg0, arg1) +} + +// AddProposalIDToFinish mocks base method. +func (m *MockChain) AddProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposalIDToFinish", arg0) +} + +// AddProposalIDToFinish indicates an expected call of AddProposalIDToFinish. +func (mr *MockChainMockRecorder) AddProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposalIDToFinish", reflect.TypeOf((*MockChain)(nil).AddProposalIDToFinish), arg0) +} + +// RemoveProposalIDToFinish mocks base method. +func (m *MockChain) RemoveProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposalIDToFinish", arg0) +} + +// RemoveProposalIDToFinish indicates an expected call of RemoveProposalIDToFinish. +func (mr *MockChainMockRecorder) RemoveProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposalIDToFinish", reflect.TypeOf((*MockChain)(nil).RemoveProposalIDToFinish), arg0) +} + +// SetBaseFee mocks base method. +func (m *MockChain) SetBaseFee(arg0 uint64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetBaseFee", arg0) +} + +// SetBaseFee indicates an expected call of SetBaseFee. +func (mr *MockChainMockRecorder) SetBaseFee(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBaseFee", reflect.TypeOf((*MockChain)(nil).SetBaseFee), arg0) +} + // SetCurrentSupply mocks base method. func (m *MockChain) SetCurrentSupply(arg0 ids.ID, arg1 uint64) { m.ctrl.T.Helper() diff --git a/vms/platformvm/state/mock_diff.go b/vms/platformvm/state/mock_diff.go index d90f29fbebf3..6e2bb2e9ceaf 100644 --- a/vms/platformvm/state/mock_diff.go +++ b/vms/platformvm/state/mock_diff.go @@ -16,6 +16,7 @@ import ( avax "github.com/ava-labs/avalanchego/vms/components/avax" multisig "github.com/ava-labs/avalanchego/vms/components/multisig" config "github.com/ava-labs/avalanchego/vms/platformvm/config" + dac "github.com/ava-labs/avalanchego/vms/platformvm/dac" deposit "github.com/ava-labs/avalanchego/vms/platformvm/deposit" locked "github.com/ava-labs/avalanchego/vms/platformvm/locked" status "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -304,6 +305,36 @@ func (mr *MockDiffMockRecorder) GetClaimable(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaimable", reflect.TypeOf((*MockDiff)(nil).GetClaimable), arg0) } +// GetProposal mocks base method. +func (m *MockDiff) GetProposal(arg0 ids.ID) (dac.ProposalState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposal", arg0) + ret0, _ := ret[0].(dac.ProposalState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProposal indicates an expected call of GetProposal. +func (mr *MockDiffMockRecorder) GetProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposal", reflect.TypeOf((*MockDiff)(nil).GetProposal), arg0) +} + +// GetBaseFee mocks base method. +func (m *MockDiff) GetBaseFee() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBaseFee") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBaseFee indicates an expected call of GetBaseFee. +func (mr *MockDiffMockRecorder) GetBaseFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBaseFee", reflect.TypeOf((*MockDiff)(nil).GetBaseFee)) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockDiff) GetCurrentDelegatorIterator(arg0 ids.ID, arg1 ids.NodeID) (StakerIterator, error) { m.ctrl.T.Helper() @@ -410,6 +441,67 @@ func (mr *MockDiffMockRecorder) GetNextToUnlockDepositIDsAndTime(arg0 interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToUnlockDepositIDsAndTime", reflect.TypeOf((*MockDiff)(nil).GetNextToUnlockDepositIDsAndTime), arg0) } +// GetNextProposalExpirationTime mocks base method. +func (m *MockDiff) GetNextProposalExpirationTime(arg0 set.Set[ids.ID]) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextProposalExpirationTime", arg0) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNextProposalExpirationTime indicates an expected call of GetNextProposalExpirationTime. +func (mr *MockDiffMockRecorder) GetNextProposalExpirationTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextProposalExpirationTime", reflect.TypeOf((*MockDiff)(nil).GetNextProposalExpirationTime), arg0) +} + +// GetNextToExpireProposalIDsAndTime mocks base method. +func (m *MockDiff) GetNextToExpireProposalIDsAndTime(arg0 set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextToExpireProposalIDsAndTime", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNextToExpireProposalIDsAndTime indicates an expected call of GetNextToExpireProposalIDsAndTime. +func (mr *MockDiffMockRecorder) GetNextToExpireProposalIDsAndTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToExpireProposalIDsAndTime", reflect.TypeOf((*MockDiff)(nil).GetNextToExpireProposalIDsAndTime), arg0) +} + +// GetProposalIDsToFinish mocks base method. +func (m *MockDiff) GetProposalIDsToFinish() ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIDsToFinish") + ret0, _ := ret[0].([]ids.ID) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIDsToFinish indicates an expected call of GetProposalIDsToFinish. +func (mr *MockDiffMockRecorder) GetProposalIDsToFinish() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIDsToFinish", reflect.TypeOf((*MockDiff)(nil).GetProposalIDsToFinish)) +} + +// GetProposalIterator mocks base method. +func (m *MockDiff) GetProposalIterator() (ProposalsIterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIterator") + ret0, _ := ret[0].(ProposalsIterator) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIterator indicates an expected call of GetProposalIterator. +func (mr *MockDiffMockRecorder) GetProposalIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIterator", reflect.TypeOf((*MockDiff)(nil).GetProposalIterator)) +} + // GetDepositOffer mocks base method. func (m *MockDiff) GetDepositOffer(arg0 ids.ID) (*deposit.Offer, error) { m.ctrl.T.Helper() @@ -746,6 +838,78 @@ func (mr *MockDiffMockRecorder) SetClaimable(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClaimable", reflect.TypeOf((*MockDiff)(nil).SetClaimable), arg0, arg1) } +// AddProposal mocks base method. +func (m *MockDiff) AddProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposal", arg0, arg1) +} + +// AddProposal indicates an expected call of AddProposal. +func (mr *MockDiffMockRecorder) AddProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposal", reflect.TypeOf((*MockDiff)(nil).AddProposal), arg0, arg1) +} + +// ModifyProposal mocks base method. +func (m *MockDiff) ModifyProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ModifyProposal", arg0, arg1) +} + +// ModifyProposal indicates an expected call of ModifyProposal. +func (mr *MockDiffMockRecorder) ModifyProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyProposal", reflect.TypeOf((*MockDiff)(nil).ModifyProposal), arg0, arg1) +} + +// RemoveProposal mocks base method. +func (m *MockDiff) RemoveProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposal", arg0, arg1) +} + +// RemoveProposal indicates an expected call of RemoveProposal. +func (mr *MockDiffMockRecorder) RemoveProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposal", reflect.TypeOf((*MockDiff)(nil).RemoveProposal), arg0, arg1) +} + +// AddProposalIDToFinish mocks base method. +func (m *MockDiff) AddProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposalIDToFinish", arg0) +} + +// AddProposalIDToFinish indicates an expected call of AddProposalIDToFinish. +func (mr *MockDiffMockRecorder) AddProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposalIDToFinish", reflect.TypeOf((*MockDiff)(nil).AddProposalIDToFinish), arg0) +} + +// RemoveProposalIDToFinish mocks base method. +func (m *MockDiff) RemoveProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposalIDToFinish", arg0) +} + +// RemoveProposalIDToFinish indicates an expected call of RemoveProposalIDToFinish. +func (mr *MockDiffMockRecorder) RemoveProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposalIDToFinish", reflect.TypeOf((*MockDiff)(nil).RemoveProposalIDToFinish), arg0) +} + +// SetBaseFee mocks base method. +func (m *MockDiff) SetBaseFee(arg0 uint64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetBaseFee", arg0) +} + +// SetBaseFee indicates an expected call of SetBaseFee. +func (mr *MockDiffMockRecorder) SetBaseFee(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBaseFee", reflect.TypeOf((*MockDiff)(nil).SetBaseFee), arg0) +} + // SetCurrentSupply mocks base method. func (m *MockDiff) SetCurrentSupply(arg0 ids.ID, arg1 uint64) { m.ctrl.T.Helper() diff --git a/vms/platformvm/state/mock_proposals_iterator.go b/vms/platformvm/state/mock_proposals_iterator.go new file mode 100644 index 000000000000..6450779bb881 --- /dev/null +++ b/vms/platformvm/state/mock_proposals_iterator.go @@ -0,0 +1,109 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/vms/platformvm/state (interfaces: ProposalsIterator) + +// Package state is a generated GoMock package. +package state + +import ( + reflect "reflect" + + ids "github.com/ava-labs/avalanchego/ids" + dac "github.com/ava-labs/avalanchego/vms/platformvm/dac" + gomock "github.com/golang/mock/gomock" +) + +// MockProposalsIterator is a mock of ProposalsIterator interface. +type MockProposalsIterator struct { + ctrl *gomock.Controller + recorder *MockProposalsIteratorMockRecorder +} + +// MockProposalsIteratorMockRecorder is the mock recorder for MockProposalsIterator. +type MockProposalsIteratorMockRecorder struct { + mock *MockProposalsIterator +} + +// NewMockProposalsIterator creates a new mock instance. +func NewMockProposalsIterator(ctrl *gomock.Controller) *MockProposalsIterator { + mock := &MockProposalsIterator{ctrl: ctrl} + mock.recorder = &MockProposalsIteratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProposalsIterator) EXPECT() *MockProposalsIteratorMockRecorder { + return m.recorder +} + +// Value mocks base method. +func (m *MockProposalsIterator) Value() (dac.ProposalState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Value") + ret0, _ := ret[0].(dac.ProposalState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Value indicates an expected call of Value. +func (mr *MockProposalsIteratorMockRecorder) Value() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockProposalsIterator)(nil).Value)) +} + +// Next mocks base method. +func (m *MockProposalsIterator) Next() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Next") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Next indicates an expected call of Next. +func (mr *MockProposalsIteratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockProposalsIterator)(nil).Next)) +} + +// Error mocks base method. +func (m *MockProposalsIterator) Error() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Error") + ret0, _ := ret[0].(error) + return ret0 +} + +// Error indicates an expected call of Error. +func (mr *MockProposalsIteratorMockRecorder) Error() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockProposalsIterator)(nil).Error)) +} + +// Release mocks base method. +func (m *MockProposalsIterator) Release() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Release") +} + +// Release indicates an expected call of Release. +func (mr *MockProposalsIteratorMockRecorder) Release() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockProposalsIterator)(nil).Release)) +} + +// key mocks base method. +func (m *MockProposalsIterator) key() (ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "key") + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// key indicates an expected call of key. +func (mr *MockProposalsIteratorMockRecorder) key() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "key", reflect.TypeOf((*MockProposalsIterator)(nil).key)) +} diff --git a/vms/platformvm/state/mock_state.go b/vms/platformvm/state/mock_state.go index 802cced3c50b..33a71d2fa8b8 100644 --- a/vms/platformvm/state/mock_state.go +++ b/vms/platformvm/state/mock_state.go @@ -21,6 +21,7 @@ import ( multisig "github.com/ava-labs/avalanchego/vms/components/multisig" blocks "github.com/ava-labs/avalanchego/vms/platformvm/blocks" config "github.com/ava-labs/avalanchego/vms/platformvm/config" + dac "github.com/ava-labs/avalanchego/vms/platformvm/dac" deposit "github.com/ava-labs/avalanchego/vms/platformvm/deposit" locked "github.com/ava-labs/avalanchego/vms/platformvm/locked" status "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -352,6 +353,36 @@ func (mr *MockStateMockRecorder) GetClaimable(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaimable", reflect.TypeOf((*MockState)(nil).GetClaimable), arg0) } +// GetProposal mocks base method. +func (m *MockState) GetProposal(arg0 ids.ID) (dac.ProposalState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposal", arg0) + ret0, _ := ret[0].(dac.ProposalState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProposal indicates an expected call of GetProposal. +func (mr *MockStateMockRecorder) GetProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposal", reflect.TypeOf((*MockState)(nil).GetProposal), arg0) +} + +// GetBaseFee mocks base method. +func (m *MockState) GetBaseFee() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBaseFee") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBaseFee indicates an expected call of GetBaseFee. +func (mr *MockStateMockRecorder) GetBaseFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBaseFee", reflect.TypeOf((*MockState)(nil).GetBaseFee)) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockState) GetCurrentDelegatorIterator(arg0 ids.ID, arg1 ids.NodeID) (StakerIterator, error) { m.ctrl.T.Helper() @@ -458,6 +489,67 @@ func (mr *MockStateMockRecorder) GetNextToUnlockDepositIDsAndTime(arg0 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToUnlockDepositIDsAndTime", reflect.TypeOf((*MockState)(nil).GetNextToUnlockDepositIDsAndTime), arg0) } +// GetNextProposalExpirationTime mocks base method. +func (m *MockState) GetNextProposalExpirationTime(arg0 set.Set[ids.ID]) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextProposalExpirationTime", arg0) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNextProposalExpirationTime indicates an expected call of GetNextProposalExpirationTime. +func (mr *MockStateMockRecorder) GetNextProposalExpirationTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextProposalExpirationTime", reflect.TypeOf((*MockState)(nil).GetNextProposalExpirationTime), arg0) +} + +// GetNextToExpireProposalIDsAndTime mocks base method. +func (m *MockState) GetNextToExpireProposalIDsAndTime(arg0 set.Set[ids.ID]) ([]ids.ID, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextToExpireProposalIDsAndTime", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNextToExpireProposalIDsAndTime indicates an expected call of GetNextToExpireProposalIDsAndTime. +func (mr *MockStateMockRecorder) GetNextToExpireProposalIDsAndTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextToExpireProposalIDsAndTime", reflect.TypeOf((*MockState)(nil).GetNextToExpireProposalIDsAndTime), arg0) +} + +// GetProposalIDsToFinish mocks base method. +func (m *MockState) GetProposalIDsToFinish() ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIDsToFinish") + ret0, _ := ret[0].([]ids.ID) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIDsToFinish indicates an expected call of GetProposalIDsToFinish. +func (mr *MockStateMockRecorder) GetProposalIDsToFinish() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIDsToFinish", reflect.TypeOf((*MockState)(nil).GetProposalIDsToFinish)) +} + +// GetProposalIterator mocks base method. +func (m *MockState) GetProposalIterator() (ProposalsIterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProposalIterator") + ret0, _ := ret[0].(ProposalsIterator) + ret2, _ := ret[1].(error) + return ret0, ret2 +} + +// GetProposalIterator indicates an expected call of GetProposalIterator. +func (mr *MockStateMockRecorder) GetProposalIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposalIterator", reflect.TypeOf((*MockState)(nil).GetProposalIterator)) +} + // GetDepositOffer mocks base method. func (m *MockState) GetDepositOffer(arg0 ids.ID) (*deposit.Offer, error) { m.ctrl.T.Helper() @@ -887,6 +979,78 @@ func (mr *MockStateMockRecorder) SetClaimable(arg0, arg1 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClaimable", reflect.TypeOf((*MockState)(nil).SetClaimable), arg0, arg1) } +// AddProposal mocks base method. +func (m *MockState) AddProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposal", arg0, arg1) +} + +// AddProposal indicates an expected call of AddProposal. +func (mr *MockStateMockRecorder) AddProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposal", reflect.TypeOf((*MockState)(nil).AddProposal), arg0, arg1) +} + +// ModifyProposal mocks base method. +func (m *MockState) ModifyProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ModifyProposal", arg0, arg1) +} + +// ModifyProposal indicates an expected call of ModifyProposal. +func (mr *MockStateMockRecorder) ModifyProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyProposal", reflect.TypeOf((*MockState)(nil).ModifyProposal), arg0, arg1) +} + +// RemoveProposal mocks base method. +func (m *MockState) RemoveProposal(arg0 ids.ID, arg1 dac.ProposalState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposal", arg0, arg1) +} + +// RemoveProposal indicates an expected call of RemoveProposal. +func (mr *MockStateMockRecorder) RemoveProposal(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposal", reflect.TypeOf((*MockState)(nil).RemoveProposal), arg0, arg1) +} + +// AddProposalIDToFinish mocks base method. +func (m *MockState) AddProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddProposalIDToFinish", arg0) +} + +// AddProposalIDToFinish indicates an expected call of AddProposalIDToFinish. +func (mr *MockStateMockRecorder) AddProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProposalIDToFinish", reflect.TypeOf((*MockState)(nil).AddProposalIDToFinish), arg0) +} + +// RemoveProposalIDToFinish mocks base method. +func (m *MockState) RemoveProposalIDToFinish(arg0 ids.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveProposalIDToFinish", arg0) +} + +// RemoveProposalIDToFinish indicates an expected call of RemoveProposalIDToFinish. +func (mr *MockStateMockRecorder) RemoveProposalIDToFinish(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProposalIDToFinish", reflect.TypeOf((*MockState)(nil).RemoveProposalIDToFinish), arg0) +} + +// SetBaseFee mocks base method. +func (m *MockState) SetBaseFee(arg0 uint64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetBaseFee", arg0) +} + +// SetBaseFee indicates an expected call of SetBaseFee. +func (mr *MockStateMockRecorder) SetBaseFee(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBaseFee", reflect.TypeOf((*MockState)(nil).SetBaseFee), arg0) +} + // SetCurrentSupply mocks base method. func (m *MockState) SetCurrentSupply(arg0 ids.ID, arg1 uint64) { m.ctrl.T.Helper() diff --git a/vms/platformvm/txs/builder/camino_builder.go b/vms/platformvm/txs/builder/camino_builder.go index 51ee76aefce0..af5f146a8e41 100644 --- a/vms/platformvm/txs/builder/camino_builder.go +++ b/vms/platformvm/txs/builder/camino_builder.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -106,6 +107,12 @@ type CaminoTxBuilder interface { NewSystemUnlockDepositTx( depositTxIDs []ids.ID, ) (*txs.Tx, error) + + FinishProposalsTx( + state state.Chain, + earlyFinishedProposalIDs []ids.ID, + expiredProposalIDs []ids.ID, + ) (*txs.Tx, error) } func NewCamino( @@ -683,6 +690,62 @@ func (b *caminoBuilder) NewSystemUnlockDepositTx( return tx, tx.SyntacticVerify(b.ctx) } +func (b *caminoBuilder) FinishProposalsTx( + state state.Chain, + earlyFinishedProposalIDs []ids.ID, + expiredProposalIDs []ids.ID, +) (*txs.Tx, error) { + ins, outs, err := b.Unlock( + state, + append(earlyFinishedProposalIDs, expiredProposalIDs...), + locked.StateBonded, + ) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx := &txs.FinishProposalsTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.ctx.NetworkID, + BlockchainID: b.ctx.ChainID, + Ins: ins, + Outs: outs, + }}} + + for _, proposalID := range earlyFinishedProposalIDs { + proposal, err := state.GetProposal(proposalID) + if err != nil { + return nil, fmt.Errorf("couldn't get proposal from state: %w", err) + } + if proposal.IsSuccessful() { + utx.EarlyFinishedSuccessfulProposalIDs = append(utx.EarlyFinishedSuccessfulProposalIDs, proposalID) + } else { + utx.EarlyFinishedFailedProposalIDs = append(utx.EarlyFinishedFailedProposalIDs, proposalID) + } + } + for _, proposalID := range expiredProposalIDs { + proposal, err := state.GetProposal(proposalID) + if err != nil { + return nil, fmt.Errorf("couldn't get proposal from state: %w", err) + } + if proposal.IsSuccessful() { + utx.ExpiredSuccessfulProposalIDs = append(utx.ExpiredSuccessfulProposalIDs, proposalID) + } else { + utx.ExpiredFailedProposalIDs = append(utx.ExpiredFailedProposalIDs, proposalID) + } + } + + utils.Sort(utx.EarlyFinishedSuccessfulProposalIDs) + utils.Sort(utx.EarlyFinishedFailedProposalIDs) + utils.Sort(utx.ExpiredSuccessfulProposalIDs) + utils.Sort(utx.ExpiredFailedProposalIDs) + + tx, err := txs.NewSigned(utx, txs.Codec, nil) + if err != nil { + return nil, err + } + return tx, tx.SyntacticVerify(b.ctx) +} + func getSigner( keys []*secp256k1.PrivateKey, address ids.ShortID, diff --git a/vms/platformvm/txs/builder/camino_helpers_test.go b/vms/platformvm/txs/builder/camino_helpers_test.go index 1b793ee3d32e..17174907fe2b 100644 --- a/vms/platformvm/txs/builder/camino_helpers_test.go +++ b/vms/platformvm/txs/builder/camino_helpers_test.go @@ -395,7 +395,7 @@ func defaultCaminoConfig(postBanff bool) config.Config { ApricotPhase5Time: defaultValidateEndTime, BanffTime: banffTime, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, } } diff --git a/vms/platformvm/txs/camino_add_deposit_offer_tx_test.go b/vms/platformvm/txs/camino_add_deposit_offer_tx_test.go index d186f75dc40f..5502d7a905b8 100644 --- a/vms/platformvm/txs/camino_add_deposit_offer_tx_test.go +++ b/vms/platformvm/txs/camino_add_deposit_offer_tx_test.go @@ -8,7 +8,6 @@ import ( "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" @@ -17,8 +16,7 @@ import ( ) func TestAddDepositOfferTxSyntacticVerify(t *testing.T) { - ctx := snow.DefaultContextTest() - ctx.AVAXAssetID = ids.GenerateTestID() + ctx := defaultContext() owner1 := secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{{0, 0, 1}}} depositTxID := ids.ID{0, 1} creatorAddress := ids.ShortID{1} @@ -139,7 +137,7 @@ func TestAddDepositOfferTxSyntacticVerify(t *testing.T) { }, expectedErr: locked.ErrWrongOutType, }, - "OK: v1": { + "OK: offer v1": { tx: &AddDepositOfferTx{ BaseTx: baseTx, DepositOfferCreatorAddress: creatorAddress, diff --git a/vms/platformvm/txs/camino_add_proposal_tx.go b/vms/platformvm/txs/camino_add_proposal_tx.go new file mode 100644 index 000000000000..00e8cb02add1 --- /dev/null +++ b/vms/platformvm/txs/camino_add_proposal_tx.go @@ -0,0 +1,118 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" +) + +var ( + _ UnsignedTx = (*AddProposalTx)(nil) + + errBadProposal = errors.New("bad proposal") + errBadProposerAuth = errors.New("bad proposer auth") + errTooBigBond = errors.New("to big bond") +) + +// AddProposalTx is an unsigned addProposalTx +type AddProposalTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Proposal bytes + ProposalPayload []byte `serialize:"true" json:"proposalPayload"` + // Address that can create proposals of this type + ProposerAddress ids.ShortID `serialize:"true" json:"proposerAddress"` + // Auth that will be used to verify credential for proposal + ProposerAuth verify.Verifiable `serialize:"true" json:"proposerAuth"` + + bondAmount *uint64 + proposal dac.Proposal +} + +// SyntacticVerify returns nil if [tx] is valid +func (tx *AddProposalTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: // already passed syntactic verification + return nil + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return fmt.Errorf("failed to verify BaseTx: %w", err) + } + + proposal, err := tx.Proposal() + if err != nil { + return err + } + + if err := proposal.Verify(); err != nil { + return fmt.Errorf("%w: %s", errBadProposal, err) + } + + if err := tx.ProposerAuth.Verify(); err != nil { + return fmt.Errorf("%w: %s", errBadProposerAuth, err) + } + + if err := locked.VerifyLockMode(tx.Ins, tx.Outs, true); err != nil { + return err + } + + bondAmount := uint64(0) + for _, out := range tx.Outs { + if lockedOut, ok := out.Out.(*locked.Out); ok && lockedOut.IsNewlyLockedWith(locked.StateBonded) { + newBondAmount, err := math.Add64(bondAmount, lockedOut.Amount()) + if err != nil { + return fmt.Errorf("%w: %s", errTooBigBond, err) + } + bondAmount = newBondAmount + } + } + tx.bondAmount = &bondAmount + + // cache that this is valid + tx.SyntacticallyVerified = true + return nil +} + +type ProposalWrapper struct { + dac.Proposal `serialize:"true"` +} + +func (tx *AddProposalTx) Proposal() (dac.Proposal, error) { + if tx.proposal == nil { + proposal := &ProposalWrapper{} + if _, err := Codec.Unmarshal(tx.ProposalPayload, proposal); err != nil { + return nil, fmt.Errorf("%w: %s", errBadProposal, err) + } + tx.proposal = proposal.Proposal + } + return tx.proposal, nil +} + +func (tx *AddProposalTx) BondAmount() uint64 { + if tx.bondAmount == nil { + bondAmount := uint64(0) + for _, out := range tx.Outs { + if lockedOut, ok := out.Out.(*locked.Out); ok && lockedOut.IsNewlyLockedWith(locked.StateBonded) { + bondAmount += lockedOut.Amount() + } + } + tx.bondAmount = &bondAmount + } + return *tx.bondAmount +} + +func (tx *AddProposalTx) Visit(visitor Visitor) error { + return visitor.AddProposalTx(tx) +} diff --git a/vms/platformvm/txs/camino_add_proposal_tx_test.go b/vms/platformvm/txs/camino_add_proposal_tx_test.go new file mode 100644 index 000000000000..7b76cb99655e --- /dev/null +++ b/vms/platformvm/txs/camino_add_proposal_tx_test.go @@ -0,0 +1,123 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/stretchr/testify/require" +) + +func TestAddProposalTxSyntacticVerify(t *testing.T) { + ctx := defaultContext() + owner1 := secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{{0, 0, 1}}} + + badProposal := &ProposalWrapper{Proposal: &dac.BaseFeeProposal{Options: []uint64{}}} + badProposalBytes, err := Codec.Marshal(Version, badProposal) + require.NoError(t, err) + + proposal := &ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + End: 1, + Options: []uint64{1}, + }} + proposalBytes, err := Codec.Marshal(Version, proposal) + require.NoError(t, err) + + baseTx := BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }} + + tests := map[string]struct { + tx *AddProposalTx + expectedErr error + }{ + "Nil tx": { + expectedErr: ErrNilTx, + }, + "Fail to unmarshal proposal": { + tx: &AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: []byte{}, + }, + expectedErr: errBadProposal, + }, + "Bad proposal": { + tx: &AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: badProposalBytes, + }, + expectedErr: errBadProposal, + }, + "Bad proposer auth": { + tx: &AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: proposalBytes, + ProposerAuth: (*secp256k1fx.Input)(nil), + }, + expectedErr: errBadProposerAuth, + }, + "Stakable base tx input": { + tx: &AddProposalTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestStakeableIn(ctx.AVAXAssetID, 1, 1, []uint32{0}), + }, + }}, + ProposalPayload: proposalBytes, + ProposerAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongInType, + }, + "Stakable base tx output": { + tx: &AddProposalTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Outs: []*avax.TransferableOutput{ + generateTestStakeableOut(ctx.AVAXAssetID, 1, 1, owner1), + }, + }}, + ProposalPayload: proposalBytes, + ProposerAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongOutType, + }, + "OK": { + tx: &AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: proposalBytes, + ProposerAuth: &secp256k1fx.Input{}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.tx.SyntacticVerify(ctx), tt.expectedErr) + }) + } +} + +func TestAddProposalTxProposal(t *testing.T) { + expectedProposal := &ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + Start: 11, End: 12, + Options: []uint64{555, 123, 7}, + }} + proposalBytes, err := Codec.Marshal(Version, expectedProposal) + require.NoError(t, err) + + tx := &AddProposalTx{ + ProposalPayload: proposalBytes, + } + txProposal, err := tx.Proposal() + require.NoError(t, err) + require.Equal(t, expectedProposal.Proposal, txProposal) +} diff --git a/vms/platformvm/txs/camino_add_vote_tx.go b/vms/platformvm/txs/camino_add_vote_tx.go new file mode 100644 index 000000000000..d3403e174c38 --- /dev/null +++ b/vms/platformvm/txs/camino_add_vote_tx.go @@ -0,0 +1,92 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" +) + +var ( + _ UnsignedTx = (*AddVoteTx)(nil) + + errBadVote = errors.New("bad vote") + errBadVoterAuth = errors.New("bad voter auth") +) + +// AddVoteTx is an unsigned AddVoteTx +type AddVoteTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Proposal id + ProposalID ids.ID `serialize:"true" json:"proposalID"` + // Vote bytes + VotePayload []byte `serialize:"true" json:"votePayload"` + // Address that is voting + VoterAddress ids.ShortID `serialize:"true" json:"proposerAddress"` + // Auth that will be used to verify credential for vote + VoterAuth verify.Verifiable `serialize:"true" json:"VoterAuth"` + + vote dac.Vote +} + +// SyntacticVerify returns nil if [tx] is valid +func (tx *AddVoteTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: // already passed syntactic verification + return nil + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return fmt.Errorf("failed to verify BaseTx: %w", err) + } + + vote, err := tx.Vote() + if err != nil { + return err + } + + if err := vote.Verify(); err != nil { + return fmt.Errorf("%w: %s", errBadVote, err) + } + + if err := tx.VoterAuth.Verify(); err != nil { + return fmt.Errorf("%w: %s", errBadVoterAuth, err) + } + + if err := locked.VerifyNoLocks(tx.Ins, tx.Outs); err != nil { + return err + } + + // cache that this is valid + tx.SyntacticallyVerified = true + return nil +} + +type VoteWrapper struct { + dac.Vote `serialize:"true"` +} + +func (tx *AddVoteTx) Vote() (dac.Vote, error) { + if tx.vote == nil { + vote := &VoteWrapper{} + if _, err := Codec.Unmarshal(tx.VotePayload, vote); err != nil { + return nil, fmt.Errorf("%w: %s", errBadVote, err) + } + tx.vote = vote.Vote + } + return tx.vote, nil +} + +func (tx *AddVoteTx) Visit(visitor Visitor) error { + return visitor.AddVoteTx(tx) +} diff --git a/vms/platformvm/txs/camino_add_vote_tx_test.go b/vms/platformvm/txs/camino_add_vote_tx_test.go new file mode 100644 index 000000000000..b9cf9c0c4083 --- /dev/null +++ b/vms/platformvm/txs/camino_add_vote_tx_test.go @@ -0,0 +1,143 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/stretchr/testify/require" +) + +func TestAddVoteTxSyntacticVerify(t *testing.T) { + ctx := defaultContext() + owner1 := secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{{0, 0, 1}}} + + badVote := &VoteWrapper{Vote: &dac.DummyVote{ErrorStr: "test errr"}} + badVoteBytes, err := Codec.Marshal(Version, badVote) + require.NoError(t, err) + + vote := &VoteWrapper{Vote: &dac.DummyVote{}} + voteBytes, err := Codec.Marshal(Version, vote) + require.NoError(t, err) + + baseTx := BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }} + + tests := map[string]struct { + tx *AddVoteTx + expectedErr error + }{ + "Nil tx": { + expectedErr: ErrNilTx, + }, + "Fail to unmarshal vote": { + tx: &AddVoteTx{ + BaseTx: baseTx, + VotePayload: []byte{}, + }, + expectedErr: errBadVote, + }, + "Bad vote": { + tx: &AddVoteTx{ + BaseTx: baseTx, + VotePayload: badVoteBytes, + }, + expectedErr: errBadVote, + }, + "Bad voter auth": { + tx: &AddVoteTx{ + BaseTx: baseTx, + VotePayload: voteBytes, + VoterAuth: (*secp256k1fx.Input)(nil), + }, + expectedErr: errBadVoterAuth, + }, + "Locked base tx input": { + tx: &AddVoteTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestIn(ctx.AVAXAssetID, 1, ids.ID{1}, ids.Empty, []uint32{0}), + }, + }}, + VotePayload: voteBytes, + VoterAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongInType, + }, + "Locked base tx output": { + tx: &AddVoteTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Outs: []*avax.TransferableOutput{ + generateTestOut(ctx.AVAXAssetID, 1, owner1, ids.ID{1}, ids.Empty), + }, + }}, + VotePayload: voteBytes, + VoterAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongOutType, + }, + "Stakable base tx input": { + tx: &AddVoteTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestStakeableIn(ctx.AVAXAssetID, 1, 1, []uint32{0}), + }, + }}, + VotePayload: voteBytes, + VoterAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongInType, + }, + "Stakable base tx output": { + tx: &AddVoteTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Outs: []*avax.TransferableOutput{ + generateTestStakeableOut(ctx.AVAXAssetID, 1, 1, owner1), + }, + }}, + VotePayload: voteBytes, + VoterAuth: &secp256k1fx.Input{}, + }, + expectedErr: locked.ErrWrongOutType, + }, + "OK": { + tx: &AddVoteTx{ + BaseTx: baseTx, + VotePayload: voteBytes, + VoterAuth: &secp256k1fx.Input{}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.tx.SyntacticVerify(ctx), tt.expectedErr) + }) + } +} + +func TestAddVoteTxVote(t *testing.T) { + expectedVote := &VoteWrapper{Vote: &dac.DummyVote{ErrorStr: "some data"}} + voteBytes, err := Codec.Marshal(Version, expectedVote) + require.NoError(t, err) + + tx := &AddVoteTx{VotePayload: voteBytes} + txVote, err := tx.Vote() + require.NoError(t, err) + require.Equal(t, expectedVote.Vote, txVote) +} diff --git a/vms/platformvm/txs/camino_address_state_tx.go b/vms/platformvm/txs/camino_address_state_tx.go index 065cbe204f34..355b18f86e1f 100644 --- a/vms/platformvm/txs/camino_address_state_tx.go +++ b/vms/platformvm/txs/camino_address_state_tx.go @@ -26,12 +26,13 @@ const ( AddressStateBitRoleKYC AddressStateBit = 1 AddressStateBitRoleOffersAdmin AddressStateBit = 2 - AddressStateBitKYCVerified AddressStateBit = 32 - AddressStateBitKYCExpired AddressStateBit = 33 - AddressStateBitConsortium AddressStateBit = 38 - AddressStateBitNodeDeferred AddressStateBit = 39 - AddressStateBitOffersCreator AddressStateBit = 50 - AddressStateBitMax AddressStateBit = 63 + AddressStateBitKYCVerified AddressStateBit = 32 + AddressStateBitKYCExpired AddressStateBit = 33 + AddressStateBitConsortium AddressStateBit = 38 + AddressStateBitNodeDeferred AddressStateBit = 39 + AddressStateBitOffersCreator AddressStateBit = 50 + AddressStateBitCaminoProposer AddressStateBit = 51 + AddressStateBitMax AddressStateBit = 63 // States @@ -50,13 +51,25 @@ const ( AddressStateNodeDeferred AddressState = AddressState(1) << AddressStateBitNodeDeferred // 0b1000000000000000000000000000000000000000 AddressStateVotableBits AddressState = AddressStateConsortiumMember | AddressStateNodeDeferred // 0b1100000000000000000000000000000000000000 - AddressStateOffersCreator AddressState = AddressState(1) << AddressStateBitOffersCreator // 0b100000000000000000000000000000000000000000000000000 - - AddressStateValidBits = AddressStateRoleAll | AddressStateKYCAll | AddressStateVotableBits | AddressStateOffersCreator // 0b100000000001100001100000000000000000000000000000111 + AddressStateOffersCreator AddressState = AddressState(1) << AddressStateBitOffersCreator // 0b0100000000000000000000000000000000000000000000000000 + AddressStateCaminoProposer AddressState = AddressState(1) << AddressStateBitCaminoProposer // 0b1000000000000000000000000000000000000000000000000000 AddressStateAthensPhaseBits = AddressStateRoleOffersAdmin | AddressStateOffersCreator + AddressStateBerlinPhaseBits = AddressStateCaminoProposer + + AddressStateValidBits = AddressStateRoleAll | AddressStateKYCAll | AddressStateVotableBits | + AddressStateAthensPhaseBits | + AddressStateBerlinPhaseBits // 0b1100000000001100001100000000000000000000000000000111 ) +func (as AddressState) Is(state AddressState) bool { + return as&state == state +} + +func (as AddressState) IsNot(state AddressState) bool { + return as&state != state +} + var ( _ UnsignedTx = (*AddressStateTx)(nil) diff --git a/vms/platformvm/txs/camino_finish_proposals_tx.go b/vms/platformvm/txs/camino_finish_proposals_tx.go new file mode 100644 index 000000000000..3b9ef4e3d1e6 --- /dev/null +++ b/vms/platformvm/txs/camino_finish_proposals_tx.go @@ -0,0 +1,105 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" +) + +var ( + _ UnsignedTx = (*FinishProposalsTx)(nil) + + errNoFinishedProposals = errors.New("no expired or successful proposals") + errNotUniqueProposalID = errors.New("not unique proposal id") + errNotSortedOrUniqueProposalIDs = errors.New("not sorted or not unique proposal ids") +) + +// FinishProposalsTx is an unsigned removeExpiredProposalsTx +type FinishProposalsTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Proposals that were finished early. + EarlyFinishedSuccessfulProposalIDs []ids.ID `serialize:"true" json:"earlyFinishedSuccessfulProposalIDs"` + // Proposals that were finished early. + EarlyFinishedFailedProposalIDs []ids.ID `serialize:"true" json:"earlyFinishedFailedProposalIDs"` + // Proposals that were expired. + ExpiredSuccessfulProposalIDs []ids.ID `serialize:"true" json:"expiredSuccessfulProposalIDs"` + // Proposals that were expired. + ExpiredFailedProposalIDs []ids.ID `serialize:"true" json:"expiredFailedProposalIDs"` +} + +// SyntacticVerify returns nil if [tx] is valid +func (tx *FinishProposalsTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: // already passed syntactic verification + return nil + case len(tx.EarlyFinishedSuccessfulProposalIDs) == 0 && + len(tx.EarlyFinishedFailedProposalIDs) == 0 && + len(tx.ExpiredSuccessfulProposalIDs) == 0 && + len(tx.ExpiredFailedProposalIDs) == 0: + return errNoFinishedProposals + case !utils.IsSortedAndUniqueSortable(tx.EarlyFinishedSuccessfulProposalIDs) || + !utils.IsSortedAndUniqueSortable(tx.EarlyFinishedFailedProposalIDs) || + !utils.IsSortedAndUniqueSortable(tx.ExpiredSuccessfulProposalIDs) || + !utils.IsSortedAndUniqueSortable(tx.ExpiredFailedProposalIDs): + return errNotSortedOrUniqueProposalIDs + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return fmt.Errorf("failed to verify BaseTx: %w", err) + } + + totalProposalsCount := len(tx.EarlyFinishedSuccessfulProposalIDs) + len(tx.EarlyFinishedFailedProposalIDs) + + len(tx.ExpiredSuccessfulProposalIDs) + len(tx.ExpiredFailedProposalIDs) + uniqueProposals := set.NewSet[ids.ID](totalProposalsCount) + for _, proposalID := range tx.EarlyFinishedSuccessfulProposalIDs { + uniqueProposals.Add(proposalID) + } + for _, proposalID := range tx.EarlyFinishedFailedProposalIDs { + if uniqueProposals.Contains(proposalID) { + return errNotUniqueProposalID + } + uniqueProposals.Add(proposalID) + } + for _, proposalID := range tx.ExpiredSuccessfulProposalIDs { + if uniqueProposals.Contains(proposalID) { + return errNotUniqueProposalID + } + uniqueProposals.Add(proposalID) + } + for _, proposalID := range tx.ExpiredFailedProposalIDs { + if uniqueProposals.Contains(proposalID) { + return errNotUniqueProposalID + } + uniqueProposals.Add(proposalID) + } + + if err := locked.VerifyLockMode(tx.Ins, tx.Outs, true); err != nil { + return err + } + + // cache that this is valid + tx.SyntacticallyVerified = true + return nil +} + +func (tx *FinishProposalsTx) Visit(visitor Visitor) error { + return visitor.FinishProposalsTx(tx) +} + +func (tx *FinishProposalsTx) ProposalIDs() []ids.ID { + lockTxIDs := tx.EarlyFinishedSuccessfulProposalIDs + lockTxIDs = append(lockTxIDs, tx.EarlyFinishedFailedProposalIDs...) + lockTxIDs = append(lockTxIDs, tx.ExpiredSuccessfulProposalIDs...) + return append(lockTxIDs, tx.ExpiredFailedProposalIDs...) +} diff --git a/vms/platformvm/txs/camino_finish_proposals_tx_test.go b/vms/platformvm/txs/camino_finish_proposals_tx_test.go new file mode 100644 index 000000000000..513509dd9ac1 --- /dev/null +++ b/vms/platformvm/txs/camino_finish_proposals_tx_test.go @@ -0,0 +1,192 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/stretchr/testify/require" +) + +func TestFinishProposalsTxSyntacticVerify(t *testing.T) { + ctx := defaultContext() + owner1 := secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{{0, 0, 1}}} + + proposalID1 := ids.ID{1} + proposalID2 := ids.ID{2} + proposalID3 := ids.ID{3} + proposalID4 := ids.ID{4} + proposalID5 := ids.ID{5} + proposalID6 := ids.ID{6} + proposalID7 := ids.ID{7} + proposalID8 := ids.ID{8} + + baseTx := BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }} + + tests := map[string]struct { + tx *FinishProposalsTx + expectedErr error + }{ + "Nil tx": { + expectedErr: ErrNilTx, + }, + "No proposals": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + }, + expectedErr: errNoFinishedProposals, + }, + "Not sorted proposals in EarlyFinishedSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID2, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not sorted proposals in EarlyFinishedFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID2, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not sorted proposals in ExpiredSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID2, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not sorted proposals in ExpiredFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + ExpiredFailedProposalIDs: []ids.ID{proposalID2, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not unique proposals in EarlyFinishedSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not unique proposals in EarlyFinishedFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID1, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not unique proposals in ExpiredSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID1, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not unique proposals in ExpiredFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + ExpiredFailedProposalIDs: []ids.ID{proposalID1, proposalID1}, + }, + expectedErr: errNotSortedOrUniqueProposalIDs, + }, + "Not unique proposals in EarlyFinishedSuccessfulProposalIDs and EarlyFinishedFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1}, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Not unique proposals in EarlyFinishedSuccessfulProposalIDs and ExpiredSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1}, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Not unique proposals in EarlyFinishedSuccessfulProposalIDs and ExpiredFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1}, + ExpiredFailedProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Not unique proposals in EarlyFinishedFailedProposalIDs and ExpiredSuccessfulProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID1}, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Not unique proposals in EarlyFinishedFailedProposalIDs and ExpiredFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID1}, + ExpiredFailedProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Not unique proposals in ExpiredSuccessfulProposalIDs and ExpiredFailedProposalIDs": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID1}, + ExpiredFailedProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: errNotUniqueProposalID, + }, + "Stakable base tx input": { + tx: &FinishProposalsTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestStakeableIn(ctx.AVAXAssetID, 1, 1, []uint32{0}), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: locked.ErrWrongInType, + }, + "Stakable base tx output": { + tx: &FinishProposalsTx{ + BaseTx: BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Outs: []*avax.TransferableOutput{ + generateTestStakeableOut(ctx.AVAXAssetID, 1, 1, owner1), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1}, + }, + expectedErr: locked.ErrWrongOutType, + }, + "OK": { + tx: &FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{proposalID1, proposalID2}, + EarlyFinishedFailedProposalIDs: []ids.ID{proposalID3, proposalID4}, + ExpiredSuccessfulProposalIDs: []ids.ID{proposalID5, proposalID6}, + ExpiredFailedProposalIDs: []ids.ID{proposalID7, proposalID8}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.tx.SyntacticVerify(ctx), tt.expectedErr) + }) + } +} diff --git a/vms/platformvm/txs/camino_multisig_alias_tx_test.go b/vms/platformvm/txs/camino_multisig_alias_tx_test.go index d6c0d89d8e2e..9061d486ce1b 100644 --- a/vms/platformvm/txs/camino_multisig_alias_tx_test.go +++ b/vms/platformvm/txs/camino_multisig_alias_tx_test.go @@ -11,15 +11,13 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) func TestMultisigAliasTxSyntacticVerify(t *testing.T) { - ctx := snow.DefaultContextTest() - ctx.AVAXAssetID = ids.GenerateTestID() + ctx := defaultContext() memo := []byte("memo") bigMemo := make([]byte, 257) diff --git a/vms/platformvm/txs/camino_visitor.go b/vms/platformvm/txs/camino_visitor.go index 496b90332bde..bdab80c1cbd9 100644 --- a/vms/platformvm/txs/camino_visitor.go +++ b/vms/platformvm/txs/camino_visitor.go @@ -13,4 +13,7 @@ type CaminoVisitor interface { BaseTx(*BaseTx) error MultisigAliasTx(*MultisigAliasTx) error AddDepositOfferTx(*AddDepositOfferTx) error + AddProposalTx(*AddProposalTx) error + AddVoteTx(*AddVoteTx) error + FinishProposalsTx(*FinishProposalsTx) error } diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index b589e43ef663..498b2839559e 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -20,6 +20,7 @@ import ( "github.com/ava-labs/avalanchego/codec/linearcodec" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/multisig" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" @@ -122,6 +123,13 @@ func RegisterUnsignedTxsTypes(targetCodec codec.CaminoRegistry) error { targetCodec.RegisterCustomType(&multisig.AliasWithNonce{}), targetCodec.RegisterCustomType(&secp256k1fx.CrossTransferOutput{}), targetCodec.RegisterCustomType(&AddDepositOfferTx{}), + targetCodec.RegisterCustomType(&AddProposalTx{}), + targetCodec.RegisterCustomType(&AddVoteTx{}), + targetCodec.RegisterCustomType(&FinishProposalsTx{}), + targetCodec.RegisterCustomType(&dac.BaseFeeProposal{}), + targetCodec.RegisterCustomType(&dac.BaseFeeProposalState{}), + targetCodec.RegisterCustomType(&dac.DummyVote{}), + targetCodec.RegisterCustomType(&dac.SimpleVote{}), ) return errs.Err } diff --git a/vms/platformvm/txs/executor/camino_chain_event.go b/vms/platformvm/txs/executor/camino_chain_event.go index 92265271521c..15e1488ee683 100644 --- a/vms/platformvm/txs/executor/camino_chain_event.go +++ b/vms/platformvm/txs/executor/camino_chain_event.go @@ -11,14 +11,13 @@ import ( ) // GetNextChainEventTime returns the next chain event time -// For example: stakers set changed, deposit expired +// For example: stakers set changed, deposit expired, proposal expired func GetNextChainEventTime(state state.Chain, stakerChangeTime time.Time) (time.Time, error) { earliestTime := stakerChangeTime nextDeferredStakerEndTime, err := getNextDeferredStakerEndTime(state) if err != nil && err != database.ErrNotFound { return time.Time{}, err } - if err != database.ErrNotFound && nextDeferredStakerEndTime.Before(earliestTime) { earliestTime = nextDeferredStakerEndTime } @@ -27,11 +26,29 @@ func GetNextChainEventTime(state state.Chain, stakerChangeTime time.Time) (time. if err != nil && err != database.ErrNotFound { return time.Time{}, err } - if err != database.ErrNotFound && depositUnlockTime.Before(earliestTime) { earliestTime = depositUnlockTime } + proposalExpirationTime, err := state.GetNextProposalExpirationTime(nil) + if err != nil && err != database.ErrNotFound { + return time.Time{}, err + } + if err != database.ErrNotFound && proposalExpirationTime.Before(earliestTime) { + earliestTime = proposalExpirationTime + } + + finishedProposalIDs, err := state.GetProposalIDsToFinish() + if err != nil { + return time.Time{}, err + } + if len(finishedProposalIDs) > 0 { + currentChainTime := state.GetTimestamp() + if currentChainTime.Before(earliestTime) { + earliestTime = currentChainTime + } + } + return earliestTime, nil } diff --git a/vms/platformvm/txs/executor/camino_dac.go b/vms/platformvm/txs/executor/camino_dac.go new file mode 100644 index 000000000000..5207fcc69c8e --- /dev/null +++ b/vms/platformvm/txs/executor/camino_dac.go @@ -0,0 +1,94 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package executor + +import ( + "errors" + + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + _ dac.VerifierVisitor = (*proposalVerifier)(nil) + _ dac.ExecutorVisitor = (*proposalExecutor)(nil) + + errNotPermittedToCreateProposal = errors.New("don't have permission to create proposal of this type") + errAlreadyActiveProposal = errors.New("there is already active proposal of this type") +) + +type proposalVerifier struct { + state state.Chain + fx fx.Fx + signedAddProposalTx *txs.Tx + addProposalTx *txs.AddProposalTx +} + +// Executor calls should never error. +// We should always mind possible proposals conflict, when implementing proposal execution logic. +// Because when proposal is semantically verified, state doesn't know about changes +// that already existing proposals will bring into state on their execution. +// And proposal execution is a system tx, so it should always succeed. +type proposalExecutor struct { + state state.Chain + fx fx.Fx +} + +func (e *CaminoStandardTxExecutor) proposalVerifier(tx *txs.AddProposalTx) *proposalVerifier { + return &proposalVerifier{ + state: e.State, + fx: e.Fx, + signedAddProposalTx: e.Tx, + addProposalTx: tx, + } +} + +func (e *CaminoStandardTxExecutor) proposalExecutor() *proposalExecutor { + return &proposalExecutor{state: e.State, fx: e.Fx} +} + +// BaseFeeProposal + +func (e *proposalVerifier) BaseFeeProposal(*dac.BaseFeeProposal) error { + // verify address state (role) + proposerAddressState, err := e.state.GetAddressStates(e.addProposalTx.ProposerAddress) + if err != nil { + return err + } + + if proposerAddressState.IsNot(txs.AddressStateCaminoProposer) { + return errNotPermittedToCreateProposal + } + + // verify that there is no existing base fee proposal + proposalsIterator, err := e.state.GetProposalIterator() + if err != nil { + return err + } + defer proposalsIterator.Release() + for proposalsIterator.Next() { + proposal, err := proposalsIterator.Value() + if err != nil { + return err + } + if _, ok := proposal.(*dac.BaseFeeProposalState); ok { + return errAlreadyActiveProposal + } + } + + if err := proposalsIterator.Error(); err != nil { + return err + } + + return nil +} + +// should never error +func (e *proposalExecutor) BaseFeeProposal(proposal *dac.BaseFeeProposalState) error { + _, mostVotedOptionIndex, _ := proposal.GetMostVoted() + e.state.SetBaseFee(proposal.Options[mostVotedOptionIndex].Value) + return nil +} diff --git a/vms/platformvm/txs/executor/camino_dac_test.go b/vms/platformvm/txs/executor/camino_dac_test.go new file mode 100644 index 000000000000..609e7f3897b9 --- /dev/null +++ b/vms/platformvm/txs/executor/camino_dac_test.go @@ -0,0 +1,197 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package executor + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/api" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +func TestProposalVerifierBaseFeeProposal(t *testing.T) { + ctx, _ := defaultCtx(nil) + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + } + + feeOwnerKey, _, feeOwner := generateKeyAndOwner(t) + bondOwnerKey, _, bondOwner := generateKeyAndOwner(t) + proposerKey, proposerAddr, _ := generateKeyAndOwner(t) + + proposalBondAmt := uint64(100) + feeUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 5}, ctx.AVAXAssetID, defaultTxFee, feeOwner, ids.Empty, ids.Empty) + bondUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 6}, ctx.AVAXAssetID, proposalBondAmt, bondOwner, ids.Empty, ids.Empty) + + proposal := &txs.ProposalWrapper{Proposal: &dac.BaseFeeProposal{End: 1, Options: []uint64{1}}} + proposalBytes, err := txs.Codec.Marshal(txs.Version, proposal) + require.NoError(t, err) + + baseTx := txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestInFromUTXO(feeUTXO, []uint32{0}), + generateTestInFromUTXO(bondUTXO, []uint32{0}), + }, + Outs: []*avax.TransferableOutput{ + generateTestOut(ctx.AVAXAssetID, proposalBondAmt, bondOwner, ids.Empty, locked.ThisTxID), + }, + }} + + tests := map[string]struct { + state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff + utx func() *txs.AddProposalTx + signers [][]*secp256k1.PrivateKey + expectedErr error + }{ + "Proposer isn't caminoProposer": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(txs.AddressStateEmpty, nil) // not AddressStateCaminoProposer + return s + }, + utx: func() *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errNotPermittedToCreateProposal, + }, + "Already active BaseFeeProposal": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + proposalsIterator := state.NewMockProposalsIterator(c) + proposalsIterator.EXPECT().Next().Return(true) + proposalsIterator.EXPECT().Value().Return(&dac.BaseFeeProposalState{}, nil) + proposalsIterator.EXPECT().Release() + + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(txs.AddressStateCaminoProposer, nil) + s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) + return s + }, + utx: func() *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errAlreadyActiveProposal, + }, + "OK": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + proposalsIterator := state.NewMockProposalsIterator(c) + proposalsIterator.EXPECT().Next().Return(false) + proposalsIterator.EXPECT().Release() + proposalsIterator.EXPECT().Error().Return(nil) + + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(txs.AddressStateCaminoProposer, nil) + s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) + return s + }, + utx: func() *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: baseTx, + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) + defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() + + utx := tt.utx() + avax.SortTransferableInputsWithSigners(utx.Ins, tt.signers) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) + tx, err := txs.NewSigned(utx, txs.Codec, tt.signers) + require.NoError(t, err) + + txExecutor := CaminoStandardTxExecutor{StandardTxExecutor{ + Backend: &env.backend, + State: tt.state(ctrl, utx), + Tx: tx, + }} + + proposal, err := utx.Proposal() + require.NoError(t, err) + err = proposal.Visit(txExecutor.proposalVerifier(utx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalExecutorBaseFeeProposal(t *testing.T) { + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + } + + tests := map[string]struct { + state func(*gomock.Controller) *state.MockDiff + proposal dac.ProposalState + expectedErr error + }{ + "OK": { + state: func(c *gomock.Controller) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().SetBaseFee(uint64(123)) + return s + }, + proposal: &dac.BaseFeeProposalState{ + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 0}, + {Value: 123, Weight: 2}, + {Value: 7, Weight: 1}, + }}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) + defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() + + txExecutor := CaminoStandardTxExecutor{StandardTxExecutor{ + Backend: &env.backend, + State: tt.state(ctrl), + }} + + err := tt.proposal.Visit(txExecutor.proposalExecutor()) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} diff --git a/vms/platformvm/txs/executor/camino_helpers_test.go b/vms/platformvm/txs/executor/camino_helpers_test.go index 78ccfb85446e..056e65d4c693 100644 --- a/vms/platformvm/txs/executor/camino_helpers_test.go +++ b/vms/platformvm/txs/executor/camino_helpers_test.go @@ -31,6 +31,7 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/nodeid" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -269,7 +270,7 @@ func defaultCaminoConfig(postBanff bool) config.Config { ApricotPhase5Time: defaultValidateEndTime, BanffTime: banffTime, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, } } @@ -348,7 +349,7 @@ func generateTestUTXO(txID ids.ID, assetID ids.ID, amount uint64, outputOwners s return generateTestUTXOWithIndex(txID, 0, assetID, amount, outputOwners, depositTxID, bondTxID, true) } -func generateTestUTXOWithIndex(txID ids.ID, outIndex uint32, assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID, init bool) *avax.UTXO { +func generateTestUTXOWithIndex(txID ids.ID, outIndex uint32, assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID, init bool) *avax.UTXO { //nolint:unparam var out avax.TransferableOut = &secp256k1fx.TransferOutput{ Amt: amount, OutputOwners: outputOwners, @@ -376,6 +377,34 @@ func generateTestUTXOWithIndex(txID ids.ID, outIndex uint32, assetID ids.ID, amo return testUTXO } +func generateTestOutFromUTXO(utxo *avax.UTXO, depositTxID, bondTxID ids.ID) *avax.TransferableOutput { + out := utxo.Out + if lockedOut, ok := out.(*locked.Out); ok { + out = lockedOut.TransferableOut + } + secpOut, ok := out.(*secp256k1fx.TransferOutput) + if !ok { + panic("not secp out") + } + var innerOut avax.TransferableOut = &secp256k1fx.TransferOutput{ + Amt: secpOut.Amt, + OutputOwners: secpOut.OutputOwners, + } + if depositTxID != ids.Empty || bondTxID != ids.Empty { + innerOut = &locked.Out{ + IDs: locked.IDs{ + DepositTxID: depositTxID, + BondTxID: bondTxID, + }, + TransferableOut: innerOut, + } + } + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: utxo.AssetID()}, + Out: innerOut, + } +} + func generateTestOut(assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID) *avax.TransferableOutput { var out avax.TransferableOut = &secp256k1fx.TransferOutput{ Amt: amount, @@ -490,9 +519,13 @@ func generateTestInFromUTXO(utxo *avax.UTXO, sigIndices []uint32) *avax.Transfer } func generateInsFromUTXOs(utxos []*avax.UTXO) []*avax.TransferableInput { + return generateInsFromUTXOsWithSigIndices(utxos, []uint32{0}) +} + +func generateInsFromUTXOsWithSigIndices(utxos []*avax.UTXO, sigIndices []uint32) []*avax.TransferableInput { ins := make([]*avax.TransferableInput, len(utxos)) for i := range utxos { - ins[i] = generateTestInFromUTXO(utxos[i], []uint32{0}) + ins[i] = generateTestInFromUTXO(utxos[i], sigIndices) } return ins } @@ -657,11 +690,13 @@ func newCaminoEnvironmentWithMocks( } } -func expectVerifyMultisigPermission(s *state.MockDiff, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce) { - expectGetMultisigAliases(s, addrs, aliases) +func expectVerifyMultisigPermission(t *testing.T, s *state.MockDiff, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce) { + t.Helper() + expectGetMultisigAliases(t, s, addrs, aliases) } -func expectGetMultisigAliases(s *state.MockDiff, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce) { +func expectGetMultisigAliases(t *testing.T, s *state.MockDiff, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce) { + t.Helper() for i := range addrs { var alias *multisig.AliasWithNonce if i < len(aliases) { @@ -676,28 +711,63 @@ func expectGetMultisigAliases(s *state.MockDiff, addrs []ids.ShortID, aliases [] } func expectVerifyLock( + t *testing.T, s *state.MockDiff, ins []*avax.TransferableInput, utxos []*avax.UTXO, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce, ) { - expectGetUTXOsFromInputs(s, ins, utxos) - expectGetMultisigAliases(s, addrs, aliases) + t.Helper() + expectGetUTXOsFromInputs(t, s, ins, utxos) + expectGetMultisigAliases(t, s, addrs, aliases) } func expectVerifyUnlockDeposit( + t *testing.T, s *state.MockDiff, ins []*avax.TransferableInput, utxos []*avax.UTXO, addrs []ids.ShortID, aliases []*multisig.AliasWithNonce, //nolint:unparam ) { - expectGetUTXOsFromInputs(s, ins, utxos) - expectGetMultisigAliases(s, addrs, aliases) + t.Helper() + expectGetUTXOsFromInputs(t, s, ins, utxos) + expectGetMultisigAliases(t, s, addrs, aliases) +} + +func expectUnlock( + t *testing.T, + s *state.MockDiff, + lockTxIDs []ids.ID, + addrs []ids.ShortID, + utxos []*avax.UTXO, + removedLockState locked.State, //nolint:unparam +) { + t.Helper() + lockTxIDsSet := set.NewSet[ids.ID](len(lockTxIDs)) + addrsSet := set.NewSet[ids.ShortID](len(addrs)) + lockTxIDsSet.Add(lockTxIDs...) + addrsSet.Add(addrs...) + for _, txID := range lockTxIDs { + s.EXPECT().GetTx(txID).Return(&txs.Tx{ + Unsigned: &txs.BaseTx{BaseTx: avax.BaseTx{ + Outs: []*avax.TransferableOutput{{ + Out: &locked.Out{ + IDs: locked.IDsEmpty.Lock(removedLockState), + TransferableOut: &secp256k1fx.TransferOutput{ + OutputOwners: secp256k1fx.OutputOwners{Addrs: addrs}, + }, + }, + }}, + }}, + }, status.Committed, nil) + } + s.EXPECT().LockedUTXOs(lockTxIDsSet, addrsSet, removedLockState).Return(utxos, nil) } -func expectGetUTXOsFromInputs(s *state.MockDiff, ins []*avax.TransferableInput, utxos []*avax.UTXO) { +func expectGetUTXOsFromInputs(t *testing.T, s *state.MockDiff, ins []*avax.TransferableInput, utxos []*avax.UTXO) { + t.Helper() for i := range ins { if utxos[i] == nil { s.EXPECT().GetUTXO(ins[i].InputID()).Return(nil, database.ErrNotFound) @@ -707,13 +777,15 @@ func expectGetUTXOsFromInputs(s *state.MockDiff, ins []*avax.TransferableInput, } } -func expectConsumeUTXOs(s *state.MockDiff, ins []*avax.TransferableInput) { +func expectConsumeUTXOs(t *testing.T, s *state.MockDiff, ins []*avax.TransferableInput) { + t.Helper() for _, in := range ins { s.EXPECT().DeleteUTXO(in.InputID()) } } -func expectProduceUTXOs(s *state.MockDiff, outs []*avax.TransferableOutput, txID ids.ID, baseOutIndex int) { //nolint:unparam +func expectProduceUTXOs(t *testing.T, s *state.MockDiff, outs []*avax.TransferableOutput, txID ids.ID, baseOutIndex int) { //nolint:unparam + t.Helper() for i := range outs { s.EXPECT().AddUTXO(&avax.UTXO{ UTXOID: avax.UTXOID{ @@ -726,7 +798,8 @@ func expectProduceUTXOs(s *state.MockDiff, outs []*avax.TransferableOutput, txID } } -func expectProduceNewlyLockedUTXOs(s *state.MockDiff, outs []*avax.TransferableOutput, txID ids.ID, baseOutIndex int, lockState locked.State) { //nolint:unparam +func expectProduceNewlyLockedUTXOs(t *testing.T, s *state.MockDiff, outs []*avax.TransferableOutput, txID ids.ID, baseOutIndex int, lockState locked.State) { //nolint:unparam + t.Helper() for i := range outs { out := outs[i].Out if lockedOut, ok := out.(*locked.Out); ok { diff --git a/vms/platformvm/txs/executor/camino_tx_executor.go b/vms/platformvm/txs/executor/camino_tx_executor.go index 20b17b0b9ef3..a4b091718daa 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor.go +++ b/vms/platformvm/txs/executor/camino_tx_executor.go @@ -4,6 +4,7 @@ package executor import ( + "bytes" "errors" "fmt" "reflect" @@ -23,6 +24,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "golang.org/x/exp/slices" deposits "github.com/ava-labs/avalanchego/vms/platformvm/deposit" ) @@ -72,6 +74,7 @@ var ( errBurnedDepositUnlock = errors.New("burned undeposited tokens") errAdminCannotBeDeleted = errors.New("admin cannot be deleted") errNotAthensPhase = errors.New("not allowed before AthensPhase") + errNotBerlinPhase = errors.New("not allowed before BerlinPhase") errOfferCreatorCredentialMismatch = errors.New("offer creator credential isn't matching") errNotOfferCreator = errors.New("address isn't allowed to create deposit offers") errDepositCreatorCredentialMismatch = errors.New("deposit creator credential isn't matching") @@ -79,6 +82,21 @@ var ( errEmptyDepositCreatorAddress = errors.New("empty deposit creator address, while offer owner isn't empty") errWrongTxUpgradeVersion = errors.New("wrong tx upgrade version") errNestedMsigAlias = errors.New("nested msig aliases are not allowed") + errProposalStartToEarly = errors.New("proposal start time is to early") + errProposalToFarInFuture = fmt.Errorf("proposal start time is more than %s ahead of the current chain time", MaxFutureStartTime) + errProposalInactive = errors.New("proposal is inactive") + errProposerCredentialMismatch = errors.New("proposer credential isn't matching") + errWrongProposalBondAmount = errors.New("wrong proposal bond amount") + errVoterCredentialMismatch = errors.New("voter credential isn't matching") + errNotSuccessfulProposal = errors.New("proposal is not successful") + errSuccessfulProposal = errors.New("proposal is successful") + errNotEarlyFinishedProposal = errors.New("proposal is not early finished") + errEarlyFinishedProposal = errors.New("proposal is early finished") + errNotExpiredProposal = errors.New("proposal is not expired") + errExpiredProposal = errors.New("proposal is expired") + errProposalsAreNotExpiredYet = errors.New("proposals are not expired yet") + errEarlyFinishedProposalsMismatch = errors.New("early proposals mismatch") + errExpiredProposalsMismatch = errors.New("expired proposals mismatch") ) type CaminoStandardTxExecutor struct { @@ -146,7 +164,7 @@ func (e *CaminoStandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error // verify camino tx - if err := e.Tx.SyntacticVerify(e.Backend.Ctx); err != nil { + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { return err } @@ -234,8 +252,8 @@ func (e *CaminoStandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error tx.Outs, e.Tx.Creds[:len(e.Tx.Creds)-1], 0, - e.Backend.Config.AddPrimaryNetworkValidatorFee, - e.Backend.Ctx.AVAXAssetID, + e.Backend.Config.AddPrimaryNetworkValidatorFee, // TODO@ use baseFee? + e.Ctx.AVAXAssetID, locked.StateBonded, ); err != nil { return fmt.Errorf("%w: %s", errFlowCheckFailed, err) @@ -280,7 +298,7 @@ func (e *CaminoStandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidat defer addCreds(e.Tx, creds) } - return e.StandardTxExecutor.AddSubnetValidatorTx(tx) + return e.StandardTxExecutor.AddSubnetValidatorTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { @@ -297,7 +315,7 @@ func (e *CaminoStandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error return err } - return e.StandardTxExecutor.AddDelegatorTx(tx) + return e.StandardTxExecutor.AddDelegatorTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { @@ -314,7 +332,7 @@ func (e *CaminoStandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermi return err } - return e.StandardTxExecutor.AddPermissionlessValidatorTx(tx) + return e.StandardTxExecutor.AddPermissionlessValidatorTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { @@ -331,7 +349,7 @@ func (e *CaminoStandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermi return err } - return e.StandardTxExecutor.AddPermissionlessDelegatorTx(tx) + return e.StandardTxExecutor.AddPermissionlessDelegatorTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { @@ -339,7 +357,7 @@ func (e *CaminoStandardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { return err } - return e.StandardTxExecutor.CreateChainTx(tx) + return e.StandardTxExecutor.CreateChainTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { @@ -347,7 +365,7 @@ func (e *CaminoStandardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error return err } - return e.StandardTxExecutor.CreateSubnetTx(tx) + return e.StandardTxExecutor.CreateSubnetTx(tx) // TODO@ will use avax tx fee } func (e *CaminoStandardTxExecutor) ExportTx(tx *txs.ExportTx) error { @@ -451,7 +469,7 @@ func (e *CaminoStandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) return err } - return e.StandardTxExecutor.TransformSubnetTx(tx) + return e.StandardTxExecutor.TransformSubnetTx(tx) // TODO@ will use avax tx fee } func (e *CaminoProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error { @@ -619,7 +637,7 @@ func (e *CaminoStandardTxExecutor) DepositTx(tx *txs.DepositTx) error { return err } - if err := e.Tx.SyntacticVerify(e.Backend.Ctx); err != nil { + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { return err } @@ -720,6 +738,11 @@ func (e *CaminoStandardTxExecutor) DepositTx(tx *txs.DepositTx) error { return err } + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + if err := e.FlowChecker.VerifyLock( tx, e.State, @@ -727,7 +750,7 @@ func (e *CaminoStandardTxExecutor) DepositTx(tx *txs.DepositTx) error { tx.Outs, baseTxCreds, 0, - e.Config.TxFee, + baseFee, e.Ctx.AVAXAssetID, locked.StateDeposited, ); err != nil { @@ -779,7 +802,7 @@ func (e *CaminoStandardTxExecutor) UnlockDepositTx(tx *txs.UnlockDepositTx) erro return errWrongLockMode } - if err := e.Tx.SyntacticVerify(e.Backend.Ctx); err != nil { + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { return err } @@ -844,9 +867,13 @@ func (e *CaminoStandardTxExecutor) UnlockDepositTx(tx *txs.UnlockDepositTx) erro return errBurnedDepositUnlock } - amountToBurn := e.Config.TxFee - if hasExpiredDeposits { - amountToBurn = 0 + amountToBurn := uint64(0) + if !hasExpiredDeposits { + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + amountToBurn = baseFee } if err := e.FlowChecker.VerifyUnlockDeposit( @@ -942,10 +969,8 @@ func (e *CaminoStandardTxExecutor) UnlockDepositTx(tx *txs.UnlockDepositTx) erro } } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.State, e.Tx.ID(), tx.Outs) return nil } @@ -962,7 +987,7 @@ func (e *CaminoStandardTxExecutor) ClaimTx(tx *txs.ClaimTx) error { return errWrongLockMode } - if err := e.Tx.SyntacticVerify(e.Backend.Ctx); err != nil { + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { return err } @@ -1093,6 +1118,10 @@ func (e *CaminoStandardTxExecutor) ClaimTx(tx *txs.ClaimTx) error { } // BaseTx check (fee, reward outs) + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } if err := e.FlowChecker.VerifyLock( tx, @@ -1101,7 +1130,7 @@ func (e *CaminoStandardTxExecutor) ClaimTx(tx *txs.ClaimTx) error { tx.Outs, e.Tx.Creds[:len(e.Tx.Creds)-len(tx.Claimables)], claimedAmount, - e.Config.TxFee, + baseFee, e.Ctx.AVAXAssetID, locked.StateUnlocked, ); err != nil { @@ -1126,7 +1155,7 @@ func (e *CaminoStandardTxExecutor) RegisterNodeTx(tx *txs.RegisterNodeTx) error return err } - if consortiumMemberAddressState&txs.AddressStateConsortiumMember == 0 { + if consortiumMemberAddressState.IsNot(txs.AddressStateConsortiumMember) { return errNotConsortiumMember } @@ -1196,6 +1225,10 @@ func (e *CaminoStandardTxExecutor) RegisterNodeTx(tx *txs.RegisterNodeTx) error } // verify the flowcheck + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } if err := e.FlowChecker.VerifyLock( tx, @@ -1204,7 +1237,7 @@ func (e *CaminoStandardTxExecutor) RegisterNodeTx(tx *txs.RegisterNodeTx) error tx.Outs, e.Tx.Creds[:len(e.Tx.Creds)-2], // base tx creds 0, - e.Config.TxFee, + baseFee, e.Ctx.AVAXAssetID, locked.StateUnlocked, ); err != nil { @@ -1442,15 +1475,20 @@ func (e *CaminoStandardTxExecutor) BaseTx(tx *txs.BaseTx) error { } if e.Bootstrapped.Get() { - if err := e.Backend.FlowChecker.VerifyLock( + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + + if err := e.FlowChecker.VerifyLock( tx, e.State, tx.Ins, tx.Outs, e.Tx.Creds, 0, - e.Backend.Config.TxFee, - e.Backend.Ctx.AVAXAssetID, + baseFee, + e.Ctx.AVAXAssetID, locked.StateUnlocked, ); err != nil { return fmt.Errorf("%w: %s", errFlowCheckFailed, err) @@ -1516,6 +1554,10 @@ func (e *CaminoStandardTxExecutor) MultisigAliasTx(tx *txs.MultisigAliasTx) erro } // verify the flowcheck + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } if err := e.FlowChecker.VerifyLock( tx, @@ -1524,7 +1566,7 @@ func (e *CaminoStandardTxExecutor) MultisigAliasTx(tx *txs.MultisigAliasTx) erro tx.Outs, baseCreds, 0, - e.Config.TxFee, + baseFee, e.Ctx.AVAXAssetID, locked.StateUnlocked, ); err != nil { @@ -1566,6 +1608,10 @@ func (e *CaminoStandardTxExecutor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) } // verify the flowcheck + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } if err := e.FlowChecker.VerifyLock( tx, @@ -1574,7 +1620,7 @@ func (e *CaminoStandardTxExecutor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) tx.Outs, e.Tx.Creds[:len(e.Tx.Creds)-1], // base tx credentials 0, - e.Config.TxFee, + baseFee, e.Ctx.AVAXAssetID, locked.StateUnlocked, ); err != nil { @@ -1588,7 +1634,7 @@ func (e *CaminoStandardTxExecutor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) return err } - if depositOfferCreatorAddressState&txs.AddressStateOffersCreator == 0 { + if depositOfferCreatorAddressState.IsNot(txs.AddressStateOffersCreator) { return errNotOfferCreator } @@ -1647,6 +1693,413 @@ func (e *CaminoStandardTxExecutor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) return nil } +func (e *CaminoStandardTxExecutor) AddProposalTx(tx *txs.AddProposalTx) error { + caminoConfig, err := e.State.CaminoConfig() + if err != nil { + return err + } + + if !caminoConfig.LockModeBondDeposit { + return errWrongLockMode + } + + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + return err + } + + chainTime := e.State.GetTimestamp() + + if !e.Config.IsBerlinPhaseActivated(chainTime) { + return errNotBerlinPhase + } + + txProposal, err := tx.Proposal() + if err != nil { + return err + } + + // verify proposal and proposer credential + + switch { + case tx.BondAmount() != e.Config.CaminoConfig.DACProposalBondAmount: + return errWrongProposalBondAmount + case txProposal.StartTime().Before(chainTime): + return errProposalStartToEarly + case txProposal.StartTime().After(chainTime.Add(MaxFutureStartTime)): + return errProposalToFarInFuture + case len(e.Tx.Creds) < 2: + return errWrongCredentialsNumber + } + + if err := e.Fx.VerifyMultisigPermission( + tx, + tx.ProposerAuth, + e.Tx.Creds[len(e.Tx.Creds)-1], // proposer credential + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{tx.ProposerAddress}, + }, + e.State, + ); err != nil { + return fmt.Errorf("%w: %s", errProposerCredentialMismatch, err) + } + + if err := txProposal.Visit(e.proposalVerifier(tx)); err != nil { + return err + } + + // verify the flowcheck + + lockState := locked.StateBonded + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + + if err := e.FlowChecker.VerifyLock( + tx, + e.State, + tx.Ins, + tx.Outs, + e.Tx.Creds[:len(e.Tx.Creds)-1], // base tx creds + 0, + baseFee, + e.Ctx.AVAXAssetID, + lockState, + ); err != nil { + return fmt.Errorf("%w: %s", errFlowCheckFailed, err) + } + + // Getting active validators + // Only validators who were active when the proposal was created can vote + + currentStakerIterator, err := e.State.GetCurrentStakerIterator() + if err != nil { + return err + } + defer currentStakerIterator.Release() + + allowedVoters := []ids.ShortID{} + for currentStakerIterator.Next() { + staker := currentStakerIterator.Value() + if staker.SubnetID != constants.PrimaryNetworkID { + continue + } + + consortiumMemberAddress, err := e.State.GetShortIDLink(ids.ShortID(staker.NodeID), state.ShortLinkKeyRegisterNode) + if err != nil { + return err + } + + desiredPos, _ := slices.BinarySearchFunc(allowedVoters, consortiumMemberAddress, func(id, other ids.ShortID) int { + return bytes.Compare(id[:], other[:]) + }) + allowedVoters = append(allowedVoters, consortiumMemberAddress) + if desiredPos < len(allowedVoters)-1 { + copy(allowedVoters[desiredPos+1:], allowedVoters[desiredPos:]) + allowedVoters[desiredPos] = consortiumMemberAddress + } + } + + // update state + + txID := e.Tx.ID() + e.State.AddProposal(txID, txProposal.CreateProposalState(allowedVoters)) + avax.Consume(e.State, tx.Ins) + return utxo.ProduceLocked(e.State, txID, tx.Outs, locked.StateBonded) +} + +func (e *CaminoStandardTxExecutor) AddVoteTx(tx *txs.AddVoteTx) error { + caminoConfig, err := e.State.CaminoConfig() + if err != nil { + return err + } + + if !caminoConfig.LockModeBondDeposit { + return errWrongLockMode + } + + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + return err + } + + chainTime := e.State.GetTimestamp() + + if !e.Config.IsBerlinPhaseActivated(chainTime) { + return errNotBerlinPhase + } + + // verify vote with proposal + + proposal, err := e.State.GetProposal(tx.ProposalID) + if err != nil { + return err + } + + if !proposal.IsActiveAt(chainTime) { + return errProposalInactive // should never happen, cause inactive proposals are removed from state + } + + // verify voter credential and address state (role) + + voterAddressState, err := e.State.GetAddressStates(tx.VoterAddress) + if err != nil { + return err + } + + if voterAddressState.IsNot(txs.AddressStateConsortiumMember) { + return errNotConsortiumMember + } + + if len(e.Tx.Creds) < 2 { + return errWrongCredentialsNumber + } + + if err := e.Backend.Fx.VerifyMultisigPermission( + e.Tx.Unsigned, + tx.VoterAuth, + e.Tx.Creds[len(e.Tx.Creds)-1], // voter credential + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{tx.VoterAddress}, + }, + e.State, + ); err != nil { + return fmt.Errorf("%w: %s", errVoterCredentialMismatch, err) + } + + // verify the flowcheck + + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + + if err := e.FlowChecker.VerifyLock( + tx, + e.State, + tx.Ins, + tx.Outs, + e.Tx.Creds[:len(e.Tx.Creds)-1], // base tx creds + 0, + baseFee, + e.Ctx.AVAXAssetID, + locked.StateUnlocked, + ); err != nil { + return fmt.Errorf("%w: %s", errFlowCheckFailed, err) + } + + // update state + + vote, err := tx.Vote() + if err != nil { + return err + } + + updatedProposal, err := proposal.AddVote(tx.VoterAddress, vote) + if err != nil { + return err + } + e.State.ModifyProposal(tx.ProposalID, updatedProposal) + + // If proposal became finishable, it cannot be reverted by future votes, even if they'll be accepted. + if updatedProposal.CanBeFinished() { + e.State.AddProposalIDToFinish(tx.ProposalID) + } + + avax.Consume(e.State, tx.Ins) + avax.Produce(e.State, e.Tx.ID(), tx.Outs) + + return nil +} + +func (e *CaminoStandardTxExecutor) FinishProposalsTx(tx *txs.FinishProposalsTx) error { + caminoConfig, err := e.State.CaminoConfig() + if err != nil { + return err + } + + if !caminoConfig.LockModeBondDeposit { + return errWrongLockMode + } + + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + return err + } + + // basic checks + + chainTime := e.State.GetTimestamp() + nextToExpireProposalIDs, expirationTime, err := e.State.GetNextToExpireProposalIDsAndTime(nil) + if err != nil { + return err + } + proposalIDsToFinish, err := e.State.GetProposalIDsToFinish() + if err != nil { + return err + } + isExpirationTime := expirationTime.Equal(chainTime) + + // TODO@ if chainTime == expirationTime, then all expired proposals must be in tx + + switch { + case len(e.Tx.Creds) != 0: + return errWrongCredentialsNumber + case !e.Config.IsBerlinPhaseActivated(chainTime): + return errNotBerlinPhase + case !isExpirationTime && + len(tx.ExpiredSuccessfulProposalIDs)+len(tx.ExpiredFailedProposalIDs) != 0: + return errProposalsAreNotExpiredYet + case isExpirationTime && + len(tx.ExpiredSuccessfulProposalIDs)+len(tx.ExpiredFailedProposalIDs) != len(nextToExpireProposalIDs): + return errExpiredProposalsMismatch + case len(tx.EarlyFinishedSuccessfulProposalIDs)+len(tx.EarlyFinishedFailedProposalIDs) != len(proposalIDsToFinish): + return errEarlyFinishedProposalsMismatch + } + + // verify ins and outs + + expectedIns, expectedOuts, err := e.FlowChecker.Unlock(e.State, tx.ProposalIDs(), locked.StateBonded) + if err != nil { + return err + } + + // TODO @evlekht change rewardValidator tx in the same manner + + if !inputsAreEqual(tx.Ins, expectedIns) { + return fmt.Errorf("%w: invalid inputs", errInvalidSystemTxBody) + } + + if !outputsAreEqual(tx.Outs, expectedOuts) { + return fmt.Errorf("%w: invalid outputs", errInvalidSystemTxBody) + } + + // getting early finished and expired proposal IDs to check that they match tx proposal IDs + + proposalIDsToFinishSet := set.NewSet[ids.ID](len(proposalIDsToFinish)) + for _, proposalID := range proposalIDsToFinish { + proposalIDsToFinishSet.Add(proposalID) + } + + nextToExpireProposalIDsSet := set.NewSet[ids.ID](len(nextToExpireProposalIDs)) + if isExpirationTime { + for _, proposalID := range nextToExpireProposalIDs { + nextToExpireProposalIDsSet.Add(proposalID) + } + } + + // processing tx proposal IDs + + // TODO@ what if early finished proposal expires at the finishTx block? + // TODO@ meaning that its id will be both in expired and early finished + // TODO@ prevent failing + + for _, proposalID := range tx.EarlyFinishedSuccessfulProposalIDs { + proposal, err := e.State.GetProposal(proposalID) + if err != nil { + return err + } + + if !proposal.IsSuccessful() { + return errNotSuccessfulProposal + } + + if !proposalIDsToFinishSet.Contains(proposalID) { + return errNotEarlyFinishedProposal + } + + if nextToExpireProposalIDsSet.Contains(proposalID) { + return errExpiredProposal + } + + // try to execute proposal + if err := proposal.Visit(e.proposalExecutor()); err != nil { + return err + } + + e.State.RemoveProposal(proposalID, proposal) + e.State.RemoveProposalIDToFinish(proposalID) + proposalIDsToFinishSet.Remove(proposalID) + } + + for _, proposalID := range tx.EarlyFinishedFailedProposalIDs { + proposal, err := e.State.GetProposal(proposalID) + if err != nil { + return err + } + + if proposal.IsSuccessful() { + return errSuccessfulProposal + } + + if !proposalIDsToFinishSet.Contains(proposalID) { + return errNotEarlyFinishedProposal + } + + if nextToExpireProposalIDsSet.Contains(proposalID) { + return errExpiredProposal + } + + e.State.RemoveProposal(proposalID, proposal) + e.State.RemoveProposalIDToFinish(proposalID) + proposalIDsToFinishSet.Remove(proposalID) + } + + for _, proposalID := range tx.ExpiredSuccessfulProposalIDs { + proposal, err := e.State.GetProposal(proposalID) + if err != nil { + return err + } + + if !proposal.IsSuccessful() { + return errNotSuccessfulProposal + } + + if proposalIDsToFinishSet.Contains(proposalID) { + return errEarlyFinishedProposal + } + + if !nextToExpireProposalIDsSet.Contains(proposalID) { + return errNotExpiredProposal + } + + // try to execute proposal + if err := proposal.Visit(e.proposalExecutor()); err != nil { + return err + } + + e.State.RemoveProposal(proposalID, proposal) + nextToExpireProposalIDsSet.Remove(proposalID) + } + + for _, proposalID := range tx.ExpiredFailedProposalIDs { + proposal, err := e.State.GetProposal(proposalID) + if err != nil { + return err + } + + if proposal.IsSuccessful() { + return errSuccessfulProposal + } + + if proposalIDsToFinishSet.Contains(proposalID) { + return errEarlyFinishedProposal + } + + if !nextToExpireProposalIDsSet.Contains(proposalID) { + return errNotExpiredProposal + } + + e.State.RemoveProposal(proposalID, proposal) + nextToExpireProposalIDsSet.Remove(proposalID) + } + + avax.Consume(e.State, tx.Ins) + avax.Produce(e.State, e.Tx.ID(), tx.Outs) + + return nil +} + func removeCreds(tx *txs.Tx, num int) []verify.Verifiable { newCredsLen := len(tx.Creds) - num removedCreds := tx.Creds[newCredsLen:len(tx.Creds)] @@ -1742,6 +2195,11 @@ func (e *CaminoStandardTxExecutor) AddressStateTx(tx *txs.AddressStateTx) error } // Verify the flowcheck + baseFee, err := e.State.GetBaseFee() + if err != nil { + return err + } + if err := e.FlowChecker.VerifySpend( tx, e.State, @@ -1749,7 +2207,7 @@ func (e *CaminoStandardTxExecutor) AddressStateTx(tx *txs.AddressStateTx) error tx.Outs, creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: e.Config.TxFee, + e.Ctx.AVAXAssetID: baseFee, }, ); err != nil { return err @@ -1797,9 +2255,9 @@ func (e *CaminoStandardTxExecutor) AddressStateTx(tx *txs.AddressStateTx) error // [state] must have only one bit set func verifyAccess(roles, state txs.AddressState) bool { switch { - case roles&txs.AddressStateRoleAdmin != 0: // admin can do anything - case txs.AddressStateKYCAll&state != 0 && roles&txs.AddressStateRoleKYC != 0: // kyc role can change kyc status - case txs.AddressStateOffersCreator&state != 0 && roles&txs.AddressStateRoleOffersAdmin != 0: // offers admin can assign offers creator role + case roles.Is(txs.AddressStateRoleAdmin): // admin can do anything + case txs.AddressStateKYCAll&state != 0 && roles.Is(txs.AddressStateRoleKYC): // kyc role can change kyc status + case state == txs.AddressStateOffersCreator && roles.Is(txs.AddressStateRoleOffersAdmin): // offers admin can assign offers creator role default: return false } @@ -1820,3 +2278,21 @@ func validatorExists(state state.Chain, subnetID ids.ID, nodeID ids.NodeID) erro } return nil } + +// Inner ins must implement Equal(any) bool func. +func inputsAreEqual(ins1, ins2 []*avax.TransferableInput) bool { + return slices.EqualFunc(ins1, ins2, func(in1, in2 *avax.TransferableInput) bool { + inEq1, ok := in1.In.(interface{ Equal(any) bool }) + return ok && + in1.Asset == in2.Asset && in1.TxID == in2.TxID && in1.OutputIndex == in2.OutputIndex && + inEq1.Equal(in2.In) + }) +} + +// Inner outs must implement Equal(any) bool func. +func outputsAreEqual(outs1, outs2 []*avax.TransferableOutput) bool { + return slices.EqualFunc(outs1, outs2, func(out1, out2 *avax.TransferableOutput) bool { + outEq1, ok := out1.Out.(interface{ Equal(any) bool }) + return ok && out1.Asset == out2.Asset && outEq1.Equal(out2.Out) + }) +} diff --git a/vms/platformvm/txs/executor/camino_tx_executor_test.go b/vms/platformvm/txs/executor/camino_tx_executor_test.go index dbd2510dca85..1917454e76a1 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor_test.go +++ b/vms/platformvm/txs/executor/camino_tx_executor_test.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" @@ -26,6 +27,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/reward" @@ -270,8 +272,7 @@ func TestCaminoStandardTxExecutorAddValidatorTx(t *testing.T) { }, expectedErr: errSignatureMissing, }, - // TODO@ - // "Not enough sigs from msig node owner": { + // "Not enough sigs from msig node owner": {// TODO @evlekht can't be created with tx builder, needs manual creation // generateArgs: func() args { // return args{ // stakeAmount: env.config.MinValidatorStake, @@ -2009,7 +2010,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { tests := map[string]struct { caminoGenesisConf api.Camino - state func(*gomock.Controller, *txs.DepositTx, ids.ID, *config.Config, int) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.DepositTx, ids.ID, *config.Config, int) *state.MockDiff utx func() *txs.DepositTx chaintime time.Time signers [][]*secp256k1.PrivateKey @@ -2017,7 +2018,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr []error // expectedErr[i] is expected for phase[i] }{ "Wrong lockModeBondDeposit flag": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: false}, nil) return s @@ -2034,7 +2035,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errWrongLockMode, errWrongLockMode}, }, "Stakeable ins": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) return s @@ -2054,7 +2055,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{locked.ErrWrongInType, locked.ErrWrongInType}, }, "Stakeable outs": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) return s @@ -2074,7 +2075,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{locked.ErrWrongOutType, locked.ErrWrongOutType}, }, "Not existing deposit offer ID": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(nil, database.ErrNotFound) @@ -2093,7 +2094,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{database.ErrNotFound, database.ErrNotFound}, }, "Deposit offer is inactive by flag": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(&deposit.Offer{Flags: deposit.OfferFlagLocked}, nil) @@ -2114,7 +2115,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositOfferInactive, errDepositOfferInactive}, }, "Deposit offer is not active yet": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) @@ -2135,7 +2136,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositOfferInactive, errDepositOfferInactive}, }, "Deposit offer has expired": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) @@ -2156,7 +2157,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositOfferInactive, errDepositOfferInactive}, }, "Deposit duration is too small": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) @@ -2178,7 +2179,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositDurationTooSmall, errDepositDurationTooSmall}, }, "Deposit duration is too big": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) @@ -2200,7 +2201,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositDurationTooBig, errDepositDurationTooBig}, }, "Deposit amount is too small": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) @@ -2225,7 +2226,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositTooSmall, errDepositTooSmall}, }, "Deposit amount is too big (offer.TotalMaxAmount)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithMaxAmount, nil) @@ -2251,7 +2252,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errDepositTooBig, errDepositTooBig}, }, "Deposit amount is too big (offer.TotalMaxRewardAmount)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithMaxRewardAmount, nil) @@ -2277,12 +2278,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errDepositTooBig}, }, "UTXO not found": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{unlockedUTXO1, nil}, nil, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{unlockedUTXO1, nil}, nil, nil) return s }, utx: func() *txs.DepositTx { @@ -2307,12 +2309,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errFlowCheckFailed, errFlowCheckFailed}, }, "Inputs and credentials length mismatch": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{unlockedUTXO1}, nil, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{unlockedUTXO1}, nil, nil) return s }, utx: func() *txs.DepositTx { @@ -2337,13 +2340,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errFlowCheckFailed, errFlowCheckFailed}, }, "Owned offer, bad offer permission credential (wrong deposit creator addr)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) } return s }, @@ -2384,13 +2387,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errOfferPermissionCredentialMismatch}, }, "Owned offer, bad offer permission credential (wrong offer owner key)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) } return s }, @@ -2431,13 +2434,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errOfferPermissionCredentialMismatch}, }, "Owned offer, bad offer permission credential (wrong offer owner auth)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress}, nil) } return s }, @@ -2478,13 +2481,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errOfferPermissionCredentialMismatch}, }, "Owned offer, bad deposit creator credential (wrong key)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) } return s }, @@ -2525,13 +2528,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errDepositCreatorCredentialMismatch}, }, "Owned offer, bad deposit creator credential (wrong auth)": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) } return s }, @@ -2572,12 +2575,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase, errDepositCreatorCredentialMismatch}, }, "Supply overflow": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO1}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2617,12 +2621,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errSupplyOverflow, errSupplyOverflow}, }, "OK": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO1}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2639,8 +2644,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2665,12 +2670,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}}, }, "OK: fee change to new address": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{doubleFeeUTXO, unlockedUTXO1}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2687,8 +2693,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2714,12 +2720,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}}, }, "OK: deposit bonded": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, bondedUTXOWithMinAmount}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2736,8 +2743,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2762,12 +2769,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}}, }, "OK: deposit bonded and unlocked": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO1, bondedUTXOWithMinAmount}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, utxoOwnerAddr, // consumed @@ -2784,8 +2792,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2812,12 +2820,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}, {utxoOwnerKey}}, }, "OK: deposited for new owner": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offer, nil) s.EXPECT().GetTimestamp().Return(offer.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO1}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2834,8 +2843,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2860,12 +2869,13 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}}, }, "OK: deposit offer with max amount": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithMaxAmount, nil) s.EXPECT().GetTimestamp().Return(offerWithMaxAmount.StartTime()) - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO2}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2885,8 +2895,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { updatedOffer.DepositedAmount += utx.DepositAmount() s.EXPECT().SetDepositOffer(&updatedOffer) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) return s }, utx: func() *txs.DepositTx { @@ -2912,13 +2922,14 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{{feeOwnerKey}, {utxoOwnerKey}}, }, "OK: deposit offer with max reward amount": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithMaxRewardAmount, nil) s.EXPECT().GetTimestamp().Return(offerWithMaxRewardAmount.StartTime()) if phaseIndex > 0 { - expectVerifyLock(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO3}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2939,8 +2950,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().SetDepositOffer(&updatedOffer) s.EXPECT().SetCurrentSupply(constants.PrimaryNetworkID, cfg.RewardConfig.SupplyCap) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) } return s }, @@ -2968,14 +2979,15 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { expectedErr: []error{errNotAthensPhase}, }, "OK|Fail: deposit offer with owner": { - state: func(c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.DepositTx, txID ids.ID, cfg *config.Config, phaseIndex int) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetDepositOffer(utx.DepositOfferID).Return(offerWithOwner, nil) s.EXPECT().GetTimestamp().Return(offerWithOwner.StartTime()) if phaseIndex > 0 { // if Athens - expectVerifyMultisigPermission(s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) - expectVerifyLock(s, utx.Ins, + expectVerifyMultisigPermission(t, s, []ids.ShortID{offerWithOwner.OwnerAddress, utx.DepositCreatorAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO, unlockedUTXO1}, []ids.ShortID{ feeOwnerAddr, utxoOwnerAddr, // consumed @@ -2992,8 +3004,8 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-deposit1.TotalReward(offer), nil) s.EXPECT().AddDeposit(txID, deposit1) - expectConsumeUTXOs(s, utx.Ins) - expectProduceNewlyLockedUTXOs(s, utx.Outs, txID, 0, locked.StateDeposited) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateDeposited) } return s }, @@ -3061,7 +3073,7 @@ func TestCaminoStandardTxExecutorDepositTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, utx, tx.ID(), env.config, phaseIndex), + State: tt.state(t, ctrl, utx, tx.ID(), env.config, phaseIndex), Tx: tx, }, }) @@ -3143,13 +3155,13 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { unlockedUTXOWithLargerTxID := generateTestUTXO(ids.ID{7}, ctx.AVAXAssetID, 1, owner1, ids.Empty, ids.Empty) tests := map[string]struct { - state func(*gomock.Controller, *txs.UnlockDepositTx, ids.ID) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.UnlockDepositTx, ids.ID) *state.MockDiff utx *txs.UnlockDepositTx signers [][]*secp256k1.PrivateKey expectedErr error }{ "Wrong lockModeBondDeposit flag": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: false}, nil) return s @@ -3158,11 +3170,12 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errWrongLockMode, }, "Unlock before deposit's unlock period": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1StartUnlockTime.Add(-1 * time.Second)) - expectVerifyUnlockDeposit(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{feeUTXO, deposit1UTXO}, []ids.ShortID{ feeOwnerAddr, owner1Addr, // consumed (not expired deposit) @@ -3183,7 +3196,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errUnlockedMoreThanAvailable, }, "Unlock expired deposit, tx has unlocked input before deposited input": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) @@ -3197,7 +3210,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errMixedDeposits, }, "Unlock expired deposit, tx has unlocked input after deposited input": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) @@ -3211,7 +3224,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errMixedDeposits, }, "Unlock active and expired deposits": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) @@ -3226,11 +3239,11 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errMixedDeposits, }, "Unlock not full amount, deposit expired": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) - expectVerifyUnlockDeposit(s, utx.Ins, + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{deposit1UTXO}, []ids.ShortID{ owner1Addr, // produced unlocked @@ -3248,11 +3261,12 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errExpiredDepositNotFullyUnlocked, }, "Unlock more, than available": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1HalfUnlockTime) - expectVerifyUnlockDeposit(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{feeUTXO, deposit1UTXO}, []ids.ShortID{ feeOwnerAddr, owner1Addr, // consumed (not expired deposit) @@ -3273,7 +3287,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errUnlockedMoreThanAvailable, }, "Burned tokens, while unlocking expired deposits": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) @@ -3289,11 +3303,12 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errBurnedDepositUnlock, }, "Only burn fee, nothing unlocked": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1HalfUnlockTime) - expectVerifyUnlockDeposit(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{feeUTXO, lessFeeUTXO, deposit1UTXO}, []ids.ShortID{ feeOwnerAddr, feeOwnerAddr, owner1Addr, // consumed (not expired deposit) @@ -3313,12 +3328,12 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { expectedErr: errNoUnlock, }, "OK: unlock full amount, expired deposit with unclaimed reward": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // checks s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1Expired) - expectVerifyUnlockDeposit(s, utx.Ins, + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{deposit1WithRewardUTXO}, []ids.ShortID{ owner1Addr, // produced unlocked @@ -3334,8 +3349,8 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { }) s.EXPECT().RemoveDeposit(depositWithRewardTxID1, deposit1WithReward) // state update: ins/outs/utxos - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) return s }, utx: &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ @@ -3346,12 +3361,13 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { }}}, }, "OK: unlock available amount, deposit is still unlocking": { - state: func(c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.UnlockDepositTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // checks s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(deposit1HalfUnlockTime) - expectVerifyUnlockDeposit(s, utx.Ins, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyUnlockDeposit(t, s, utx.Ins, []*avax.UTXO{feeUTXO, deposit1UTXO, deposit2UTXO}, []ids.ShortID{ feeOwnerAddr, owner1Addr, owner1Addr, // consumed (not expired deposit) @@ -3382,8 +3398,8 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { RewardOwner: deposit2.RewardOwner, }) // state update: ins/outs/utxos - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) return s }, utx: &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ @@ -3413,7 +3429,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, tt.utx, tx.ID()), + State: tt.state(t, ctrl, tt.utx, tx.ID()), Tx: tx, }, }) @@ -3484,13 +3500,13 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { } tests := map[string]struct { - state func(*gomock.Controller, *txs.ClaimTx, ids.ID) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.ClaimTx, ids.ID) *state.MockDiff utx *txs.ClaimTx signers [][]*secp256k1.PrivateKey expectedErr error }{ "Deposit not found": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) @@ -3515,7 +3531,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { expectedErr: errDepositNotFound, }, "Bad deposit credential": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) @@ -3523,7 +3539,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // deposit s.EXPECT().GetDeposit(depositTxID1). Return(&deposit.Deposit{RewardOwner: &depositRewardOwner}, nil) - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) return s }, utx: &txs.ClaimTx{ @@ -3542,14 +3558,14 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { expectedErr: errClaimableCredentialMismatch, }, "Bad claimable credential": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) s.EXPECT().GetTimestamp().Return(timestamp) // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) return s }, utx: &txs.ClaimTx{ @@ -3569,7 +3585,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, // no test case for expired deposits - expected to be alike "Claimed more than available (validator rewards)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) @@ -3577,7 +3593,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable 1 s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimableValidatorReward1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, &state.Claimable{ Owner: claimableValidatorReward1.Owner, ValidatorReward: claimableValidatorReward1.ValidatorReward - utx.Claimables[0].Amount, @@ -3585,7 +3601,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable 2 s.EXPECT().GetClaimable(claimableOwnerID2).Return(claimableValidatorReward2, nil) - expectVerifyMultisigPermission(s, claimableOwner2.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner2.Addrs, nil) return s }, utx: &txs.ClaimTx{ @@ -3613,7 +3629,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { expectedErr: errWrongClaimedAmount, }, "Claimed more than available (all treasury)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) @@ -3621,7 +3637,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable 1 s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, &state.Claimable{ Owner: claimable1.Owner, ExpiredDepositReward: 1, @@ -3629,7 +3645,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable 2 s.EXPECT().GetClaimable(claimableOwnerID2).Return(claimable2, nil) - expectVerifyMultisigPermission(s, claimableOwner2.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner2.Addrs, nil) return s }, utx: &txs.ClaimTx{ @@ -3657,7 +3673,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { expectedErr: errWrongClaimedAmount, }, "Claimed more than available (active deposit)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) @@ -3672,7 +3688,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { RewardOwner: &depositRewardOwner, } s.EXPECT().GetDeposit(depositTxID1).Return(deposit1, nil) - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) s.EXPECT().GetDepositOffer(depositOfferID).Return(&deposit.Offer{ InterestRateNominator: 1_000_000, // 100% }, nil) @@ -3694,18 +3710,19 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { expectedErr: errWrongClaimedAmount, }, "OK, 2 deposits and claimable": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1, claimToOwnerAddr1, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // deposit1 - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) deposit1 := &deposit.Deposit{ DepositOfferID: depositOfferID, Start: uint64(timestamp.Unix()) - 365*24*60*60/2, // 0.5 year ago @@ -3728,7 +3745,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }) // deposit2 - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) deposit2 := &deposit.Deposit{ DepositOfferID: depositOfferID, Start: uint64(timestamp.Unix()) - 365*24*60*60/2, // 0.5 year ago @@ -3752,7 +3769,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, nil) return s }, @@ -3809,27 +3826,28 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, 2 claimable (splitted outs)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{ feeOwnerAddr, claimToOwnerAddr1, claimToOwnerAddr2, claimToOwnerAddr1, claimToOwnerAddr1, }, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // claimable1 s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimableValidatorReward1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, nil) // claimable2 s.EXPECT().GetClaimable(claimableOwnerID2).Return(claimableValidatorReward2, nil) - expectVerifyMultisigPermission(s, claimableOwner2.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner2.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID2, &state.Claimable{ Owner: claimableValidatorReward2.Owner, ValidatorReward: claimableValidatorReward2.ValidatorReward - utx.Claimables[1].Amount, @@ -3889,24 +3907,25 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, 2 claimable (compacted out)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // claimable1 s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimableValidatorReward1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, nil) // claimable2 s.EXPECT().GetClaimable(claimableOwnerID2).Return(claimableValidatorReward2, nil) - expectVerifyMultisigPermission(s, claimableOwner2.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner2.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID2, &state.Claimable{ Owner: claimableValidatorReward2.Owner, ValidatorReward: claimableValidatorReward2.ValidatorReward - utx.Claimables[1].Amount, @@ -3945,18 +3964,19 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, active deposit with non-zero already claimed reward and no rewards period": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // deposit - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) deposit1 := &deposit.Deposit{ DepositOfferID: depositOfferID, Start: uint64(timestamp.Unix()) - 365*24*60*60/12*6, // 6 month @@ -4002,18 +4022,19 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, partial claim": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // deposit1 - expectVerifyMultisigPermission(s, depositRewardOwner.Addrs, nil) + expectVerifyMultisigPermission(t, s, depositRewardOwner.Addrs, nil) deposit1 := &deposit.Deposit{ DepositOfferID: depositOfferID, Start: uint64(timestamp.Unix()) - 365*24*60*60/2, // 0.5 year ago @@ -4037,7 +4058,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, &state.Claimable{ Owner: claimable1.Owner, ExpiredDepositReward: claimable1.ExpiredDepositReward / 2, @@ -4083,19 +4104,20 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, claim (expired deposit rewards)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, &state.Claimable{ Owner: claimable1.Owner, ValidatorReward: claimable1.ValidatorReward, @@ -4123,19 +4145,20 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, claim (validator rewards)": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr, claimToOwnerAddr1}, nil) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimable1, nil) - expectVerifyMultisigPermission(s, claimableOwner1.Addrs, nil) + expectVerifyMultisigPermission(t, s, claimableOwner1.Addrs, nil) s.EXPECT().SetClaimable(claimableOwnerID1, &state.Claimable{ Owner: claimable1.Owner, ExpiredDepositReward: claimable1.ExpiredDepositReward, @@ -4163,11 +4186,12 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, }, "OK, msig fee, claimable and deposit": { - state: func(c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.ClaimTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) // common checks and fee+ s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{msigFeeUTXO}, + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{msigFeeUTXO}, []ids.ShortID{ feeMsigAlias.ID, feeMsigAliasOwner.Addrs[0], @@ -4176,11 +4200,11 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { }, []*multisig.AliasWithNonce{feeMsigAlias}) s.EXPECT().GetTimestamp().Return(timestamp) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) // deposit1 - expectVerifyMultisigPermission(s, []ids.ShortID{ + expectVerifyMultisigPermission(t, s, []ids.ShortID{ depositRewardMsigAlias.ID, depositRewardMsigAliasOwner.Addrs[0], depositRewardMsigAliasOwner.Addrs[1], @@ -4208,7 +4232,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { // claimable s.EXPECT().GetClaimable(claimableOwnerID1).Return(claimableMsigOwned, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{ + expectVerifyMultisigPermission(t, s, []ids.ShortID{ claimableMsigAlias.ID, claimableMsigAliasOwner.Addrs[0], claimableMsigAliasOwner.Addrs[1], @@ -4272,7 +4296,7 @@ func TestCaminoStandardTxExecutorClaimTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, tt.utx, tx.ID()), + State: tt.state(t, ctrl, tt.utx, tx.ID()), Tx: tx, }, }) @@ -4305,13 +4329,13 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { }} tests := map[string]struct { - state func(*gomock.Controller, *txs.RegisterNodeTx) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.RegisterNodeTx) *state.MockDiff utx func() *txs.RegisterNodeTx signers [][]*secp256k1.PrivateKey expectedErr error }{ "Not consortium member": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateEmpty, nil) return s @@ -4331,7 +4355,7 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { expectedErr: errNotConsortiumMember, }, "Consortium member has already registered node": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). @@ -4353,12 +4377,12 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { expectedErr: errConsortiumMemberHasNode, }, "Old node is in current validator's set": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(nodeAddr1, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.NodeOwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.NodeOwnerAddress}, nil) s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, utx.OldNodeID).Return(nil, nil) // no error return s }, @@ -4379,12 +4403,12 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { expectedErr: errValidatorExists, }, "Old node is in pending validator's set": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(nodeAddr1, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.NodeOwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.NodeOwnerAddress}, nil) s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, utx.OldNodeID). Return(nil, database.ErrNotFound) s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, utx.OldNodeID).Return(nil, nil) // no error @@ -4407,12 +4431,12 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { expectedErr: errValidatorExists, }, "Old node is in deferred validator's set": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(nodeAddr1, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.NodeOwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.NodeOwnerAddress}, nil) s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, utx.OldNodeID). Return(nil, database.ErrNotFound) s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, utx.OldNodeID). @@ -4437,19 +4461,20 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { expectedErr: errValidatorExists, }, "OK: change registered node": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(nodeAddr1, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.NodeOwnerAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.NodeOwnerAddress}, nil) s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, utx.OldNodeID). Return(nil, database.ErrNotFound) s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, utx.OldNodeID). Return(nil, database.ErrNotFound) s.EXPECT().GetDeferredValidator(constants.PrimaryNetworkID, utx.OldNodeID). Return(nil, database.ErrNotFound) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().SetShortIDLink(ids.ShortID(utx.OldNodeID), state.ShortLinkKeyRegisterNode, nil) s.EXPECT().SetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode, nil) s.EXPECT().SetShortIDLink( @@ -4463,7 +4488,7 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { state.ShortLinkKeyRegisterNode, &link, ) - expectConsumeUTXOs(s, utx.Ins) + expectConsumeUTXOs(t, s, utx.Ins) return s }, utx: func() *txs.RegisterNodeTx { @@ -4482,14 +4507,14 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { }, }, "OK: consortium member is msig alias": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(ids.ShortEmpty, database.ErrNotFound) s.EXPECT().GetShortIDLink(ids.ShortID(utx.NewNodeID), state.ShortLinkKeyRegisterNode). Return(ids.ShortEmpty, database.ErrNotFound) - expectVerifyMultisigPermission(s, + expectVerifyMultisigPermission(t, s, []ids.ShortID{ utx.NodeOwnerAddress, consortiumMemberMsigAliasOwner.Addrs[0], @@ -4497,7 +4522,8 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { consortiumMemberMsigAliasOwner.Addrs[2], }, []*multisig.AliasWithNonce{consortiumMemberMsigAlias}) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().SetShortIDLink( ids.ShortID(utx.NewNodeID), state.ShortLinkKeyRegisterNode, @@ -4509,7 +4535,7 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { state.ShortLinkKeyRegisterNode, &link, ) - expectConsumeUTXOs(s, utx.Ins) + expectConsumeUTXOs(t, s, utx.Ins) return s }, utx: func() *txs.RegisterNodeTx { @@ -4528,15 +4554,16 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { }, }, "OK": { - state: func(c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.RegisterNodeTx) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetAddressStates(utx.NodeOwnerAddress).Return(txs.AddressStateConsortiumMember, nil) s.EXPECT().GetShortIDLink(utx.NodeOwnerAddress, state.ShortLinkKeyRegisterNode). Return(ids.ShortEmpty, database.ErrNotFound) s.EXPECT().GetShortIDLink(ids.ShortID(utx.NewNodeID), state.ShortLinkKeyRegisterNode). Return(ids.ShortEmpty, database.ErrNotFound) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.NodeOwnerAddress}, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.NodeOwnerAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().SetShortIDLink( ids.ShortID(utx.NewNodeID), state.ShortLinkKeyRegisterNode, @@ -4548,7 +4575,7 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { state.ShortLinkKeyRegisterNode, &link, ) - expectConsumeUTXOs(s, utx.Ins) + expectConsumeUTXOs(t, s, utx.Ins) return s }, utx: func() *txs.RegisterNodeTx { @@ -4583,7 +4610,7 @@ func TestCaminoStandardTxExecutorRegisterNodeTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, utx), + State: tt.state(t, ctrl, utx), Tx: tx, }, }) @@ -5312,13 +5339,13 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { } tests := map[string]struct { - state func(*gomock.Controller, *txs.MultisigAliasTx, ids.ID) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.MultisigAliasTx, ids.ID) *state.MockDiff utx *txs.MultisigAliasTx signers [][]*secp256k1.PrivateKey expectedErr error }{ "Add nested alias": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetMultisigAlias(msigAliasOwners.Addrs[0]).Return(&multisig.AliasWithNonce{}, nil) return s @@ -5344,9 +5371,9 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { expectedErr: errNestedMsigAlias, }, "Updating alias which does not exist": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) - expectGetMultisigAliases(s, msigAliasOwners.Addrs, nil) + expectGetMultisigAliases(t, s, msigAliasOwners.Addrs, nil) s.EXPECT().GetMultisigAlias(msigAlias.ID).Return(nil, database.ErrNotFound) return s }, @@ -5368,11 +5395,11 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { expectedErr: errAliasNotFound, }, "Updating existing alias with less signatures than threshold": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) - expectGetMultisigAliases(s, msigAliasOwners.Addrs, nil) + expectGetMultisigAliases(t, s, msigAliasOwners.Addrs, nil) s.EXPECT().GetMultisigAlias(msigAlias.ID).Return(msigAlias, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{ + expectVerifyMultisigPermission(t, s, []ids.ShortID{ msigAliasOwners.Addrs[0], msigAliasOwners.Addrs[1], }, []*multisig.AliasWithNonce{}) @@ -5396,21 +5423,22 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { expectedErr: errAliasCredentialMismatch, }, "OK, update existing alias": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) - expectGetMultisigAliases(s, msigAliasOwners.Addrs, nil) + expectGetMultisigAliases(t, s, msigAliasOwners.Addrs, nil) s.EXPECT().GetMultisigAlias(msigAlias.ID).Return(msigAlias, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{ + expectVerifyMultisigPermission(t, s, []ids.ShortID{ msigAliasOwners.Addrs[0], msigAliasOwners.Addrs[1], }, []*multisig.AliasWithNonce{}) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{ownerUTXO}, []ids.ShortID{ownerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{ownerUTXO}, []ids.ShortID{ownerAddr}, nil) s.EXPECT().SetMultisigAlias(&multisig.AliasWithNonce{ Alias: msigAlias.Alias, Nonce: msigAlias.Nonce + 1, }) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) return s }, utx: &txs.MultisigAliasTx{ @@ -5430,10 +5458,11 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { }, }, "OK, add new alias": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) - expectGetMultisigAliases(s, msigAliasOwners.Addrs, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{ownerUTXO}, []ids.ShortID{ownerAddr}, nil) + expectGetMultisigAliases(t, s, msigAliasOwners.Addrs, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{ownerUTXO}, []ids.ShortID{ownerAddr}, nil) s.EXPECT().SetMultisigAlias(&multisig.AliasWithNonce{ Alias: multisig.Alias{ ID: multisig.ComputeAliasID(txID), @@ -5442,8 +5471,8 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { }, Nonce: 0, }) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) return s }, utx: &txs.MultisigAliasTx{ @@ -5466,10 +5495,11 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { }, }, "OK, add new alias with multisig sender": { - state: func(c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.MultisigAliasTx, txID ids.ID) *state.MockDiff { s := state.NewMockDiff(c) - expectGetMultisigAliases(s, msigAliasOwners.Addrs, nil) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{msigUTXO}, []ids.ShortID{ + expectGetMultisigAliases(t, s, msigAliasOwners.Addrs, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{msigUTXO}, []ids.ShortID{ msigAlias.ID, msigAliasOwners.Addrs[0], msigAliasOwners.Addrs[1], @@ -5482,8 +5512,8 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { }, Nonce: 0, }) - expectConsumeUTXOs(s, utx.Ins) - expectProduceUTXOs(s, utx.Outs, txID, 0) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) return s }, utx: &txs.MultisigAliasTx{ @@ -5523,7 +5553,7 @@ func TestCaminoStandardTxExecutorMultisigAliasTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, tt.utx, tx.ID()), + State: tt.state(t, ctrl, tt.utx, tx.ID()), Tx: tx, }, }) @@ -5562,13 +5592,13 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { }} tests := map[string]struct { - state func(*gomock.Controller, *txs.AddDepositOfferTx, ids.ID, *config.Config) *state.MockDiff + state func(*testing.T, *gomock.Controller, *txs.AddDepositOfferTx, ids.ID, *config.Config) *state.MockDiff utx func() *txs.AddDepositOfferTx signers [][]*secp256k1.PrivateKey expectedErr error }{ "Not AthensPhase": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(cfg.AthensPhaseTime.Add(-1 * time.Second)) return s @@ -5587,10 +5617,11 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { expectedErr: errNotAthensPhase, }, "Not offer creator": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(time.Unix(100, 0)) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().GetAddressStates(utx.DepositOfferCreatorAddress).Return(txs.AddressStateEmpty, nil) return s }, @@ -5608,12 +5639,13 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { expectedErr: errNotOfferCreator, }, "Bad offer creator signature": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(time.Unix(100, 0)) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().GetAddressStates(utx.DepositOfferCreatorAddress).Return(txs.AddressStateOffersCreator, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) return s }, utx: func() *txs.AddDepositOfferTx { @@ -5630,14 +5662,15 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { expectedErr: errOfferCreatorCredentialMismatch, }, "Supply overflow (v1, no existing offers)": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { chainTime := time.Unix(100, 0) s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(chainTime) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().GetAddressStates(utx.DepositOfferCreatorAddress).Return(txs.AddressStateOffersCreator, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-offer1.TotalMaxRewardAmount+1, nil) s.EXPECT().GetAllDepositOffers().Return(nil, nil) @@ -5658,7 +5691,7 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { expectedErr: errSupplyOverflow, }, "Supply overflow (v1, existing offers)": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { chainTime := time.Unix(100, 0) existingOffers := []*deposit.Offer{ { // [0], expired @@ -5705,9 +5738,10 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(chainTime) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().GetAddressStates(utx.DepositOfferCreatorAddress).Return(txs.AddressStateOffersCreator, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID).Return(currentSupply, nil) s.EXPECT().GetAllDepositOffers().Return(existingOffers, nil) return s @@ -5727,12 +5761,13 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { expectedErr: errSupplyOverflow, }, "OK: v1": { - state: func(c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddDepositOfferTx, txID ids.ID, cfg *config.Config) *state.MockDiff { s := state.NewMockDiff(c) s.EXPECT().GetTimestamp().Return(time.Time{}) - expectVerifyLock(s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) s.EXPECT().GetAddressStates(utx.DepositOfferCreatorAddress).Return(txs.AddressStateOffersCreator, nil) - expectVerifyMultisigPermission(s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.DepositOfferCreatorAddress}, nil) s.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID). Return(cfg.RewardConfig.SupplyCap-offer1.TotalMaxRewardAmount, nil) s.EXPECT().GetAllDepositOffers().Return(nil, nil) @@ -5741,7 +5776,7 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { offer.ID = txID s.EXPECT().SetDepositOffer(&offer) - expectConsumeUTXOs(s, utx.Ins) + expectConsumeUTXOs(t, s, utx.Ins) return s }, utx: func() *txs.AddDepositOfferTx { @@ -5774,7 +5809,1499 @@ func TestCaminoStandardTxExecutorAddDepositOfferTx(t *testing.T) { err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ StandardTxExecutor{ Backend: &env.backend, - State: tt.state(ctrl, utx, tx.ID(), env.config), + State: tt.state(t, ctrl, utx, tx.ID(), env.config), + Tx: tx, + }, + }) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestCaminoStandardTxExecutorAddProposalTx(t *testing.T) { + ctx, _ := defaultCtx(nil) + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + } + caminoStateConf := &state.CaminoConfig{ + VerifyNodeSignature: caminoGenesisConf.VerifyNodeSignature, + LockModeBondDeposit: caminoGenesisConf.LockModeBondDeposit, + } + + feeOwnerKey, feeOwnerAddr, feeOwner := generateKeyAndOwner(t) + bondOwnerKey, bondOwnerAddr, bondOwner := generateKeyAndOwner(t) + proposerKey, proposerAddr, _ := generateKeyAndOwner(t) + + proposalBondAmt := uint64(100) + feeUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 5}, ctx.AVAXAssetID, defaultTxFee, feeOwner, ids.Empty, ids.Empty) + bondUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 6}, ctx.AVAXAssetID, proposalBondAmt, bondOwner, ids.Empty, ids.Empty) + + proposalWrapper := &txs.ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + Start: 100, End: 101, Options: []uint64{1}, + }} + proposalBytes, err := txs.Codec.Marshal(txs.Version, proposalWrapper) + require.NoError(t, err) + + baseTxWithBondAmt := func(bondAmt uint64) *txs.BaseTx { + return &txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestInFromUTXO(feeUTXO, []uint32{0}), + generateTestInFromUTXO(bondUTXO, []uint32{0}), + }, + Outs: []*avax.TransferableOutput{ + generateTestOut(ctx.AVAXAssetID, bondAmt, bondOwner, ids.Empty, locked.ThisTxID), + }, + }} + } + + tests := map[string]struct { + state func(*testing.T, *gomock.Controller, *txs.AddProposalTx, ids.ID, *config.Config) *state.MockDiff + utx func(*config.Config) *txs.AddProposalTx + signers [][]*secp256k1.PrivateKey + expectedErr error + }{ + "Wrong lockModeBondDeposit flag": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: false}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errWrongLockMode, + }, + "Not BerlinPhase": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime.Add(-1 * time.Second)) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errNotBerlinPhase, + }, + "Too small bond": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount - 1), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errWrongProposalBondAmount, + }, + "Too big bond": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount + 1), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errWrongProposalBondAmount, + }, + "Proposal start before chaintime": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + proposalBytes, err := txs.Codec.Marshal(txs.Version, &txs.ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + Start: uint64(cfg.BerlinPhaseTime.Unix()) - 1, + End: uint64(cfg.BerlinPhaseTime.Unix()) + 1, + Options: []uint64{1}, + }}) + require.NoError(t, err) + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errProposalStartToEarly, + }, + "Proposal starts to far in the future": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + startTime := uint64(cfg.BerlinPhaseTime.Add(MaxFutureStartTime).Unix() + 1) + proposalBytes, err := txs.Codec.Marshal(txs.Version, &txs.ProposalWrapper{Proposal: &dac.BaseFeeProposal{ + Start: startTime, + End: startTime + 1, + Options: []uint64{1}, + }}) + require.NoError(t, err) + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errProposalToFarInFuture, + }, + "Wrong proposer credential": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.ProposerAddress}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {bondOwnerKey}, + }, + expectedErr: errProposerCredentialMismatch, + }, + // for more proposal specific test cases see camino_dac_test.go + "Semantically invalid proposal": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.ProposerAddress}, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(txs.AddressStateEmpty, nil) // not AddressStateCaminoProposer + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + expectedErr: errNotPermittedToCreateProposal, + }, + "OK": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + staker1 := &state.Staker{TxID: ids.ID{0, 1}, SubnetID: constants.PrimaryNetworkID} + staker2 := &state.Staker{TxID: ids.ID{0, 2}, SubnetID: ids.ID{0, 0, 1}} + staker3 := &state.Staker{TxID: ids.ID{0, 3}, SubnetID: constants.PrimaryNetworkID} + consortiumMemberAddr1 := ids.ShortID{0, 0, 0, 0, 1} + consortiumMemberAddr3 := ids.ShortID{0, 0, 0, 0, 3} + proposal, err := utx.Proposal() + require.NoError(t, err) + proposalState := proposal.CreateProposalState([]ids.ShortID{consortiumMemberAddr1, consortiumMemberAddr3}) + + currentStakerIterator := state.NewMockStakerIterator(c) + currentStakerIterator.EXPECT().Next().Return(true).Times(3) + currentStakerIterator.EXPECT().Value().Return(staker3) + currentStakerIterator.EXPECT().Value().Return(staker1) + currentStakerIterator.EXPECT().Value().Return(staker2) + currentStakerIterator.EXPECT().Next().Return(false) + currentStakerIterator.EXPECT().Release() + + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.ProposerAddress}, nil) + + // * proposal verifier + proposalsIterator := state.NewMockProposalsIterator(c) + proposalsIterator.EXPECT().Next().Return(false) + proposalsIterator.EXPECT().Release() + proposalsIterator.EXPECT().Error().Return(nil) + + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(txs.AddressStateCaminoProposer, nil) + s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) + // * + + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, + []*avax.UTXO{feeUTXO, bondUTXO}, + []ids.ShortID{ + feeOwnerAddr, bondOwnerAddr, // consumed + bondOwnerAddr, // produced + }, nil) + s.EXPECT().GetCurrentStakerIterator().Return(currentStakerIterator, nil) + s.EXPECT().GetShortIDLink(ids.ShortID(staker1.NodeID), state.ShortLinkKeyRegisterNode). + Return(consortiumMemberAddr1, nil) + s.EXPECT().GetShortIDLink(ids.ShortID(staker3.NodeID), state.ShortLinkKeyRegisterNode). + Return(consortiumMemberAddr3, nil) + s.EXPECT().AddProposal(txID, proposalState) + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceNewlyLockedUTXOs(t, s, utx.Outs, txID, 0, locked.StateBonded) + return s + }, + utx: func(cfg *config.Config) *txs.AddProposalTx { + return &txs.AddProposalTx{ + BaseTx: *baseTxWithBondAmt(cfg.CaminoConfig.DACProposalBondAmount), + ProposalPayload: proposalBytes, + ProposerAddress: proposerAddr, + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) + defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() + + env.config.CaminoConfig.DACProposalBondAmount = proposalBondAmt + env.config.BerlinPhaseTime = proposalWrapper.StartTime() + + utx := tt.utx(env.config) + avax.SortTransferableInputsWithSigners(utx.Ins, tt.signers) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) + tx, err := txs.NewSigned(utx, txs.Codec, tt.signers) + require.NoError(t, err) + + err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ + StandardTxExecutor{ + Backend: &env.backend, + State: tt.state(t, ctrl, utx, tx.ID(), env.config), + Tx: tx, + }, + }) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestCaminoStandardTxExecutorAddVoteTx(t *testing.T) { + ctx, _ := defaultCtx(nil) + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + } + caminoStateConf := &state.CaminoConfig{ + VerifyNodeSignature: caminoGenesisConf.VerifyNodeSignature, + LockModeBondDeposit: caminoGenesisConf.LockModeBondDeposit, + } + + feeOwnerKey, feeOwnerAddr, feeOwner := generateKeyAndOwner(t) + voterKey1, voterAddr1, _ := generateKeyAndOwner(t) + voterKey2, voterAddr2, _ := generateKeyAndOwner(t) + _, voterAddr3, _ := generateKeyAndOwner(t) + voterKey4, voterAddr4, _ := generateKeyAndOwner(t) + + feeUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 5}, ctx.AVAXAssetID, defaultTxFee, feeOwner, ids.Empty, ids.Empty) + + simpleVote := &txs.VoteWrapper{Vote: &dac.SimpleVote{OptionIndex: 0}} + voteBytes, err := txs.Codec.Marshal(txs.Version, simpleVote) + require.NoError(t, err) + + baseTx := txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestInFromUTXO(feeUTXO, []uint32{0}), + }, + }} + + proposalID := ids.ID{1, 1, 1, 1} + proposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr3}, + Start: 100, End: 102, + TotalAllowedVoters: 3, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555}, + {Value: 123, Weight: 1}, + {Value: 7}, + }}, + } + utils.Sort(proposal.AllowedVoters) + + tests := map[string]struct { + state func(*testing.T, *gomock.Controller, *txs.AddVoteTx, *config.Config) *state.MockDiff + utx func(*config.Config) *txs.AddVoteTx + signers [][]*secp256k1.PrivateKey + expectedErr error + }{ + "Wrong lockModeBondDeposit flag": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: false}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: errWrongLockMode, + }, + "Not BerlinPhase": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime.Add(-1 * time.Second)) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: errNotBerlinPhase, + }, + "Proposal not exist": { // should be in case of already inactive or non-existing proposal + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetProposal(utx.ProposalID).Return(nil, database.ErrNotFound) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: database.ErrNotFound, + }, + "Proposal is already inactive": { // shouldn't be possible, inactive proposals are removed + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.EndTime().Add(time.Second)) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: errProposalInactive, + }, + "Proposal is not active yet": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime().Add(-1 * time.Second)) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: errProposalInactive, + }, + "Voter isn't consortium member": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateEmpty, nil) // not AddressStateConsortiumMember + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: errNotConsortiumMember, + }, + "Wrong voter credential": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {feeOwnerKey}, + }, + expectedErr: errVoterCredentialMismatch, + }, + "Wrong vote for this proposal (bad vote type)": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + vote := &txs.VoteWrapper{Vote: &dac.DummyVote{}} // not SimpleVote + voteBytes, err := txs.Codec.Marshal(txs.Version, vote) + require.NoError(t, err) + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: dac.ErrWrongVote, + }, + "Wrong vote for this proposal (bad option index)": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + simpleVote := &txs.VoteWrapper{Vote: &dac.SimpleVote{OptionIndex: 5}} // just 3 options in proposal + voteBytes, err := txs.Codec.Marshal(txs.Version, simpleVote) + require.NoError(t, err) + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + expectedErr: dac.ErrWrongVote, + }, + "Already voted": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr2, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey2}, + }, + expectedErr: dac.ErrNotAllowedToVoteOnProposal, + }, + "Not allowed to vote for this proposal (wasn't active validator at proposal creation)": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr4, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey4}, + }, + expectedErr: dac.ErrNotAllowedToVoteOnProposal, + }, + "OK": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + voteIntf, err := utx.Vote() + require.NoError(t, err) + vote, ok := voteIntf.(*dac.SimpleVote) + require.True(t, ok) + updatedProposal, err := proposal.AddVote(utx.VoterAddress, vote) + require.NoError(t, err) + + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().ModifyProposal(utx.ProposalID, updatedProposal) + expectConsumeUTXOs(t, s, utx.Ins) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + }, + "OK: threshold is reached, proposal planned for execution": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.AddVoteTx, cfg *config.Config) *state.MockDiff { + voteIntf, err := utx.Vote() + require.NoError(t, err) + vote, ok := voteIntf.(*dac.SimpleVote) + require.True(t, ok) + updatedProposal, err := proposal.AddVote(utx.VoterAddress, vote) + require.NoError(t, err) + + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(proposal.StartTime()) + s.EXPECT().GetProposal(utx.ProposalID).Return(proposal, nil) + s.EXPECT().GetAddressStates(utx.VoterAddress).Return(txs.AddressStateConsortiumMember, nil) + expectVerifyMultisigPermission(t, s, []ids.ShortID{utx.VoterAddress}, nil) + s.EXPECT().GetBaseFee().Return(defaultTxFee, nil) + expectVerifyLock(t, s, utx.Ins, []*avax.UTXO{feeUTXO}, []ids.ShortID{feeOwnerAddr}, nil) + s.EXPECT().ModifyProposal(utx.ProposalID, updatedProposal) + s.EXPECT().AddProposalIDToFinish(utx.ProposalID) + expectConsumeUTXOs(t, s, utx.Ins) + return s + }, + utx: func(cfg *config.Config) *txs.AddVoteTx { + simpleVote := &txs.VoteWrapper{Vote: &dac.SimpleVote{OptionIndex: 1}} + voteBytes, err := txs.Codec.Marshal(txs.Version, simpleVote) + require.NoError(t, err) + return &txs.AddVoteTx{ + BaseTx: baseTx, + ProposalID: proposalID, + VotePayload: voteBytes, + VoterAddress: voterAddr1, + VoterAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + } + }, + signers: [][]*secp256k1.PrivateKey{ + {feeOwnerKey}, {voterKey1}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) + defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() + + env.config.BerlinPhaseTime = proposal.StartTime().Add(-1 * time.Second) + + utx := tt.utx(env.config) + avax.SortTransferableInputsWithSigners(utx.Ins, tt.signers) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) + tx, err := txs.NewSigned(utx, txs.Codec, tt.signers) + require.NoError(t, err) + + err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ + StandardTxExecutor{ + Backend: &env.backend, + State: tt.state(t, ctrl, utx, env.config), + Tx: tx, + }, + }) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { + ctx, _ := defaultCtx(nil) + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + } + caminoStateConf := &state.CaminoConfig{ + VerifyNodeSignature: caminoGenesisConf.VerifyNodeSignature, + LockModeBondDeposit: caminoGenesisConf.LockModeBondDeposit, + } + + bondOwnerAddr1 := ids.ShortID{1} + bondOwnerAddr2 := ids.ShortID{2} + bondOwnerAddr3 := ids.ShortID{3} + bondOwnerAddr4 := ids.ShortID{4} + bondOwnerAddr5 := ids.ShortID{5} + bondOwnerAddr6 := ids.ShortID{6} + voterAddr1 := ids.ShortID{7} + voterAddr2 := ids.ShortID{8} + bond1Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr1}, Threshold: 1} + bond2Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr2}, Threshold: 1} + bond3Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr3}, Threshold: 1} + bond4Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr4}, Threshold: 1} + bond5Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr5}, Threshold: 1} + bond6Owner := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr6}, Threshold: 1} + + successfulEarlyFinishedProposalID := ids.ID{1, 1} + failedEarlyFinishedProposalID := ids.ID{2, 2} + successfulExpiredProposalID := ids.ID{3, 3} + failedExpiredProposalID := ids.ID{4, 4} + successfulActiveProposalID := ids.ID{5, 5} + failedActiveProposalID := ids.ID{6, 6} + proposalBondAmt := uint64(100) + successfulEarlyFinishedProposalUTXO := generateTestUTXOWithIndex(ids.ID{1, 1, 1}, 0, ctx.AVAXAssetID, proposalBondAmt, bond1Owner, ids.Empty, locked.ThisTxID, true) + failedEarlyFinishedProposalUTXO := generateTestUTXOWithIndex(ids.ID{2, 2, 2}, 0, ctx.AVAXAssetID, proposalBondAmt, bond2Owner, ids.Empty, locked.ThisTxID, true) + successfulExpiredProposalUTXO := generateTestUTXOWithIndex(ids.ID{3, 3, 3}, 0, ctx.AVAXAssetID, proposalBondAmt, bond3Owner, ids.Empty, locked.ThisTxID, true) + failedExpiredProposalUTXO := generateTestUTXOWithIndex(ids.ID{4, 4, 4}, 0, ctx.AVAXAssetID, proposalBondAmt, bond4Owner, ids.Empty, locked.ThisTxID, true) + successfulActiveProposalUTXO := generateTestUTXOWithIndex(ids.ID{5, 5, 5}, 0, ctx.AVAXAssetID, proposalBondAmt, bond5Owner, ids.Empty, locked.ThisTxID, true) + failedActiveProposalUTXO := generateTestUTXOWithIndex(ids.ID{6, 6, 6}, 0, ctx.AVAXAssetID, proposalBondAmt, bond6Owner, ids.Empty, locked.ThisTxID, true) + + baseTx := txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, failedEarlyFinishedProposalUTXO, + successfulExpiredProposalUTXO, failedExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(failedEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(failedExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }} + + mostVotedIndex := uint32(1) + successfulEarlyFinishedProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555}, + {Value: 123, Weight: 2}, + {Value: 7}, + }}, + TotalAllowedVoters: 3, + } + + failedEarlyFinishedProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 1}, + {Value: 123, Weight: 1}, + {Value: 7, Weight: 1}, + }}, + TotalAllowedVoters: 3, + } + + successfulExpiredProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 1}, + {Value: 123, Weight: 2}, + {Value: 7}, + }}, + TotalAllowedVoters: 4, + } + + failedExpiredProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 1}, + {Value: 123, Weight: 1}, + {Value: 7}, + }}, + TotalAllowedVoters: 4, + } + + successfulActiveProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 1}, + {Value: 123, Weight: 2}, + {Value: 7}, + }}, + TotalAllowedVoters: 4, + } + + failedActiveProposal := &dac.BaseFeeProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ + {Value: 555, Weight: 1}, + {Value: 123, Weight: 1}, + {Value: 7}, + }}, + TotalAllowedVoters: 4, + } + + tests := map[string]struct { + state func(*testing.T, *gomock.Controller, *txs.FinishProposalsTx, ids.ID, *config.Config) *state.MockDiff + utx func(*config.Config) *txs.FinishProposalsTx + signers [][]*secp256k1.PrivateKey + expectedErr error + }{ + "Not BerlinPhase": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime.Add(-1 * time.Second)) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{failedEarlyFinishedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredFailedProposalIDs: []ids.ID{failedExpiredProposalID}, + } + }, + expectedErr: errNotBerlinPhase, + }, + "Not zero credentials": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{failedEarlyFinishedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredFailedProposalIDs: []ids.ID{failedExpiredProposalID}, + } + }, + signers: [][]*secp256k1.PrivateKey{{}}, + expectedErr: errWrongCredentialsNumber, + }, + "Not expiration time": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulActiveProposalID}, cfg.BerlinPhaseTime.Add(time.Second), nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulActiveProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + } + }, + expectedErr: errProposalsAreNotExpiredYet, + }, + "Not all expired proposals": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID, failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + } + }, + expectedErr: errExpiredProposalsMismatch, + }, + "Not all early finished proposals": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return([]ids.ID{successfulEarlyFinishedProposalID, failedEarlyFinishedProposalID}, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + } + }, + expectedErr: errEarlyFinishedProposalsMismatch, + }, + "Invalid inputs": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, utx.ExpiredSuccessfulProposalIDs...) //nolint:gocritic + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr1, bondOwnerAddr3, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + }, locked.StateBonded) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ // missing 2nd input + generateTestInFromUTXO(successfulEarlyFinishedProposalUTXO, []uint32{}), + }, + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + } + }, + expectedErr: errInvalidSystemTxBody, + }, + "Invalid outs": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, utx.ExpiredSuccessfulProposalIDs...) //nolint:gocritic + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr1, bondOwnerAddr3, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + }, locked.StateBonded) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOut(ctx.AVAXAssetID, proposalBondAmt, bond1Owner, ids.Empty, ids.Empty), // successfulExpiredProposalUTXO with different owner + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + } + }, + expectedErr: errInvalidSystemTxBody, + }, + "Proposal not exist": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return(utx.EarlyFinishedSuccessfulProposalIDs, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr1, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(successfulEarlyFinishedProposalID).Return(nil, database.ErrNotFound) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + } + }, + expectedErr: database.ErrNotFound, + }, + "Early-finish check: successful early finished swapped with successful expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr1, bondOwnerAddr3, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + } + }, + expectedErr: errNotEarlyFinishedProposal, + }, + "Early-finish check: failed early finished swapped with failed expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr1, bondOwnerAddr3, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + } + }, + expectedErr: errNotEarlyFinishedProposal, + }, + "Early-finish check: successful active in successful early finished": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr5, + }, []*avax.UTXO{ + successfulActiveProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(successfulActiveProposalID).Return(successfulActiveProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulActiveProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + } + }, + expectedErr: errNotEarlyFinishedProposal, + }, + "Early-finish check: failed active in failed early finished": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return([]ids.ID{failedEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr6, + }, []*avax.UTXO{ + failedActiveProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(failedActiveProposalID).Return(failedActiveProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + failedActiveProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(failedActiveProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedFailedProposalIDs: []ids.ID{failedActiveProposalID}, + } + }, + expectedErr: errNotEarlyFinishedProposal, + }, + "Expire check: successful active in successful expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr5, + }, []*avax.UTXO{ + successfulActiveProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(successfulActiveProposalID).Return(successfulActiveProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulActiveProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + } + }, + expectedErr: errNotExpiredProposal, + }, + "Expire check: failed active in failed expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr6, + }, []*avax.UTXO{ + failedActiveProposalUTXO, + }, locked.StateBonded) + s.EXPECT().GetProposal(failedActiveProposalID).Return(failedActiveProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + failedActiveProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(failedActiveProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredFailedProposalIDs: []ids.ID{failedActiveProposalID}, + } + }, + expectedErr: errNotExpiredProposal, + }, + "Success check: failed proposal in successful expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr4, + }, []*avax.UTXO{ + failedExpiredProposalUTXO, + }, locked.StateBonded) + + s.EXPECT().GetProposal(failedExpiredProposalID).Return(failedExpiredProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + failedExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(failedExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredSuccessfulProposalIDs: []ids.ID{failedExpiredProposalID}, + } + }, + expectedErr: errNotSuccessfulProposal, + }, + "Success check: failed proposal in successful early finished": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{failedEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr2, + }, []*avax.UTXO{ + failedEarlyFinishedProposalUTXO, + }, locked.StateBonded) + + s.EXPECT().GetProposal(failedEarlyFinishedProposalID).Return(failedEarlyFinishedProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + failedEarlyFinishedProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(failedEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{failedEarlyFinishedProposalID}, + } + }, + expectedErr: errNotSuccessfulProposal, + }, + "Success check: successful proposal in failed expired": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). + Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr3, + }, []*avax.UTXO{ + successfulExpiredProposalUTXO, + }, locked.StateBonded) + + s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulExpiredProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + ExpiredFailedProposalIDs: []ids.ID{successfulExpiredProposalID}, + } + }, + expectedErr: errSuccessfulProposal, + }, + "Success check: successful proposal in failed early finished": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr1, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + }, locked.StateBonded) + + s.EXPECT().GetProposal(successfulEarlyFinishedProposalID).Return(successfulEarlyFinishedProposal, nil) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, + }, []uint32{}), + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedFailedProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + } + }, + expectedErr: errSuccessfulProposal, + }, + "OK": { + state: func(t *testing.T, c *gomock.Controller, utx *txs.FinishProposalsTx, txID ids.ID, cfg *config.Config) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) + s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) + expiredProposalIDs := append(utx.ExpiredSuccessfulProposalIDs, utx.ExpiredFailedProposalIDs...) //nolint:gocritic + s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return(expiredProposalIDs, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return(append(utx.EarlyFinishedSuccessfulProposalIDs, utx.EarlyFinishedFailedProposalIDs...), nil) + expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ + bondOwnerAddr1, bondOwnerAddr2, bondOwnerAddr3, bondOwnerAddr4, + }, []*avax.UTXO{ + successfulEarlyFinishedProposalUTXO, failedEarlyFinishedProposalUTXO, + successfulExpiredProposalUTXO, failedExpiredProposalUTXO, + }, locked.StateBonded) + + s.EXPECT().GetProposal(successfulEarlyFinishedProposalID).Return(successfulEarlyFinishedProposal, nil) + s.EXPECT().SetBaseFee(successfulEarlyFinishedProposal.Options[mostVotedIndex].Value) // proposal executor + s.EXPECT().RemoveProposal(successfulEarlyFinishedProposalID, successfulEarlyFinishedProposal) + s.EXPECT().RemoveProposalIDToFinish(successfulEarlyFinishedProposalID) + + s.EXPECT().GetProposal(failedEarlyFinishedProposalID).Return(failedEarlyFinishedProposal, nil) + s.EXPECT().RemoveProposal(failedEarlyFinishedProposalID, failedEarlyFinishedProposal) + s.EXPECT().RemoveProposalIDToFinish(failedEarlyFinishedProposalID) + + s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + s.EXPECT().SetBaseFee(successfulExpiredProposal.Options[mostVotedIndex].Value) // proposal executor + s.EXPECT().RemoveProposal(successfulExpiredProposalID, successfulExpiredProposal) + + s.EXPECT().GetProposal(failedExpiredProposalID).Return(failedExpiredProposal, nil) + s.EXPECT().RemoveProposal(failedExpiredProposalID, failedExpiredProposal) + + expectConsumeUTXOs(t, s, utx.Ins) + expectProduceUTXOs(t, s, utx.Outs, txID, 0) + return s + }, + utx: func(cfg *config.Config) *txs.FinishProposalsTx { + return &txs.FinishProposalsTx{ + BaseTx: baseTx, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{failedEarlyFinishedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredFailedProposalIDs: []ids.ID{failedExpiredProposalID}, + } + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) + defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() + + env.config.BerlinPhaseTime = successfulEarlyFinishedProposal.StartTime().Add(-1 * time.Second) + + utx := tt.utx(env.config) + avax.SortTransferableInputsWithSigners(utx.Ins, tt.signers) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) + tx, err := txs.NewSigned(utx, txs.Codec, tt.signers) + require.NoError(t, err) + + err = tx.Unsigned.Visit(&CaminoStandardTxExecutor{ + StandardTxExecutor{ + Backend: &env.backend, + State: tt.state(t, ctrl, utx, tx.ID(), env.config), Tx: tx, }, }) diff --git a/vms/platformvm/txs/executor/camino_visitor.go b/vms/platformvm/txs/executor/camino_visitor.go index 20cd0cf28dca..2a712755bb98 100644 --- a/vms/platformvm/txs/executor/camino_visitor.go +++ b/vms/platformvm/txs/executor/camino_visitor.go @@ -45,6 +45,18 @@ func (*StandardTxExecutor) AddDepositOfferTx(*txs.AddDepositOfferTx) error { return errWrongTxType } +func (*StandardTxExecutor) AddProposalTx(*txs.AddProposalTx) error { + return errWrongTxType +} + +func (*StandardTxExecutor) AddVoteTx(*txs.AddVoteTx) error { + return errWrongTxType +} + +func (*StandardTxExecutor) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errWrongTxType +} + // Proposal func (*ProposalTxExecutor) AddressStateTx(*txs.AddressStateTx) error { @@ -83,6 +95,18 @@ func (*ProposalTxExecutor) AddDepositOfferTx(*txs.AddDepositOfferTx) error { return errWrongTxType } +func (*ProposalTxExecutor) AddProposalTx(*txs.AddProposalTx) error { + return errWrongTxType +} + +func (*ProposalTxExecutor) AddVoteTx(*txs.AddVoteTx) error { + return errWrongTxType +} + +func (*ProposalTxExecutor) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errWrongTxType +} + // Atomic func (*AtomicTxExecutor) AddressStateTx(*txs.AddressStateTx) error { @@ -121,6 +145,18 @@ func (*AtomicTxExecutor) AddDepositOfferTx(*txs.AddDepositOfferTx) error { return errWrongTxType } +func (*AtomicTxExecutor) AddProposalTx(*txs.AddProposalTx) error { + return errWrongTxType +} + +func (*AtomicTxExecutor) AddVoteTx(*txs.AddVoteTx) error { + return errWrongTxType +} + +func (*AtomicTxExecutor) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errWrongTxType +} + // MemPool func (v *MempoolTxVerifier) AddressStateTx(tx *txs.AddressStateTx) error { @@ -158,3 +194,15 @@ func (v *MempoolTxVerifier) MultisigAliasTx(tx *txs.MultisigAliasTx) error { func (v *MempoolTxVerifier) AddDepositOfferTx(tx *txs.AddDepositOfferTx) error { return v.standardTx(tx) } + +func (v *MempoolTxVerifier) AddProposalTx(tx *txs.AddProposalTx) error { + return v.standardTx(tx) +} + +func (v *MempoolTxVerifier) AddVoteTx(tx *txs.AddVoteTx) error { + return v.standardTx(tx) +} + +func (*MempoolTxVerifier) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errWrongTxType +} diff --git a/vms/platformvm/txs/executor/staker_tx_verification.go b/vms/platformvm/txs/executor/staker_tx_verification.go index 54edaca9c3b5..564f6a893ed7 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification.go +++ b/vms/platformvm/txs/executor/staker_tx_verification.go @@ -1,3 +1,13 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// +// This file is a derived work, based on ava-labs code whose +// original notices appear below. +// +// It is distributed under the same license conditions as the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********************************************************** // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -289,6 +299,11 @@ func removeSubnetValidatorValidation( return nil, false, err } + fee, err := chainState.GetBaseFee() + if err != nil { + return nil, false, err + } + // Verify the flowcheck if err := backend.FlowChecker.VerifySpend( tx, @@ -297,7 +312,7 @@ func removeSubnetValidatorValidation( tx.Outs, baseTxCreds, map[ids.ID]uint64{ - backend.Ctx.AVAXAssetID: backend.Config.TxFee, + backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return nil, false, fmt.Errorf("%w: %v", errFlowCheckFailed, err) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 3af71b9e18ef..02eec789c679 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -175,6 +175,11 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { copy(ins, tx.Ins) copy(ins[len(tx.Ins):], tx.ImportedInputs) + fee, err := e.State.GetBaseFee() + if err != nil { + return err + } + if err := e.FlowChecker.VerifySpendUTXOs( e.State, tx, @@ -183,7 +188,7 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { tx.Outs, e.Tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: e.Config.TxFee, + e.Ctx.AVAXAssetID: fee, }, ); err != nil { return err @@ -220,6 +225,11 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { } } + fee, err := e.State.GetBaseFee() + if err != nil { + return err + } + // Verify the flowcheck if err := e.FlowChecker.VerifySpend( tx, @@ -228,7 +238,7 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { outs, e.Tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: e.Config.TxFee, + e.Ctx.AVAXAssetID: fee, }, ); err != nil { return fmt.Errorf("failed verifySpend: %w", err) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 2e93597cf7b0..e68c806db180 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -1,3 +1,13 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// +// This file is a derived work, based on ava-labs code whose +// original notices appear below. +// +// It is distributed under the same license conditions as the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********************************************************** // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -1092,6 +1102,7 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { } env.state.EXPECT().GetTx(env.unsignedTx.Subnet).Return(subnetTx, status.Committed, nil).Times(1) env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) + env.state.EXPECT().GetBaseFee().Return(defaultTxFee, nil) env.flowChecker.EXPECT().VerifySpend( env.unsignedTx, env.state, env.unsignedTx.Ins, env.unsignedTx.Outs, env.tx.Creds[:len(env.tx.Creds)-1], gomock.Any(), ).Return(nil).Times(1) @@ -1296,6 +1307,7 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { } env.state.EXPECT().GetTx(env.unsignedTx.Subnet).Return(subnetTx, status.Committed, nil) env.fx.EXPECT().VerifyPermission(gomock.Any(), env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil) + env.state.EXPECT().GetBaseFee().Return(defaultTxFee, nil) env.flowChecker.EXPECT().VerifySpend( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), ).Return(errTest) diff --git a/vms/platformvm/txs/mempool/camino_visitor.go b/vms/platformvm/txs/mempool/camino_visitor.go index 1149845afb36..3037f7791360 100644 --- a/vms/platformvm/txs/mempool/camino_visitor.go +++ b/vms/platformvm/txs/mempool/camino_visitor.go @@ -4,9 +4,13 @@ package mempool import ( + "errors" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) +var errUnsupportedTxType = errors.New("unsupported tx type") + // Issuer func (i *issuer) AddressStateTx(*txs.AddressStateTx) error { @@ -54,6 +58,20 @@ func (i *issuer) AddDepositOfferTx(*txs.AddDepositOfferTx) error { return nil } +func (i *issuer) AddProposalTx(*txs.AddProposalTx) error { + i.m.addDecisionTx(i.tx) + return nil +} + +func (i *issuer) AddVoteTx(*txs.AddVoteTx) error { + i.m.addDecisionTx(i.tx) + return nil +} + +func (*issuer) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errUnsupportedTxType +} + // Remover func (r *remover) AddressStateTx(*txs.AddressStateTx) error { @@ -100,3 +118,18 @@ func (r *remover) AddDepositOfferTx(*txs.AddDepositOfferTx) error { r.m.removeDecisionTxs([]*txs.Tx{r.tx}) return nil } + +func (r *remover) AddProposalTx(*txs.AddProposalTx) error { + r.m.removeDecisionTxs([]*txs.Tx{r.tx}) + return nil +} + +func (r *remover) AddVoteTx(*txs.AddVoteTx) error { + r.m.removeDecisionTxs([]*txs.Tx{r.tx}) + return nil +} + +func (*remover) FinishProposalsTx(*txs.FinishProposalsTx) error { + // this tx is never in mempool + return nil +} diff --git a/vms/platformvm/utxo/camino_helpers_test.go b/vms/platformvm/utxo/camino_helpers_test.go index 9f6cb9c3ae14..0ad9b5507eac 100644 --- a/vms/platformvm/utxo/camino_helpers_test.go +++ b/vms/platformvm/utxo/camino_helpers_test.go @@ -80,7 +80,7 @@ func defaultConfig() *config.Config { ApricotPhase5Time: defaultValidateEndTime, BanffTime: mockable.MaxTime, CaminoConfig: caminoconfig.Config{ - DaoProposalBondAmount: 100 * units.Avax, + DACProposalBondAmount: 100 * units.Avax, }, } } diff --git a/vms/platformvm/utxo/camino_locked.go b/vms/platformvm/utxo/camino_locked.go index 60e8cb003184..aeb0e9d723a9 100644 --- a/vms/platformvm/utxo/camino_locked.go +++ b/vms/platformvm/utxo/camino_locked.go @@ -538,7 +538,7 @@ func (h *handler) Unlock( for i, output := range outputs { lockedOut, ok := output.Out.(*locked.Out) if !ok || !lockedOut.IsNewlyLockedWith(removedLockState) { - // we'r only intersed in outs locked by this tx + // we'r only interested in outs locked by this tx continue } innerOut, ok := lockedOut.TransferableOut.(*secp256k1fx.TransferOutput) diff --git a/vms/platformvm/utxo/camino_locked_test.go b/vms/platformvm/utxo/camino_locked_test.go index 61cb3cc9b343..16a7b1ca76a8 100644 --- a/vms/platformvm/utxo/camino_locked_test.go +++ b/vms/platformvm/utxo/camino_locked_test.go @@ -1051,6 +1051,7 @@ func TestGetDepositUnlockableAmounts(t *testing.T) { func TestUnlockDeposit(t *testing.T) { testHandler := defaultCaminoHandler(t) ctx := testHandler.ctx + testHandler.clk.Set(time.Now()) testID := ids.GenerateTestID() txID := ids.GenerateTestID() @@ -1060,7 +1061,7 @@ func TestUnlockDeposit(t *testing.T) { generateTestUTXO(txID, ctx.AVAXAssetID, depositedAmount, outputOwners, testID, ids.Empty), } - nowMinus10m := uint64(time.Now().Add(-10 * time.Minute).Unix()) + nowMinus10m := uint64(testHandler.clk.Time().Add(-10 * time.Minute).Unix()) type args struct { state func(*gomock.Controller) state.Chain diff --git a/vms/secp256k1fx/camino_transfer_input.go b/vms/secp256k1fx/camino_transfer_input.go new file mode 100644 index 000000000000..e2813560a85e --- /dev/null +++ b/vms/secp256k1fx/camino_transfer_input.go @@ -0,0 +1,12 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package secp256k1fx + +import "golang.org/x/exp/slices" + +// Used in vms/platformvm/txs/executor/camino_tx_executor.go func inputsAreEqual +func (in *TransferInput) Equal(to any) bool { + toIn, ok := to.(*TransferInput) + return ok && in.Amt == toIn.Amt && slices.Equal(in.SigIndices, toIn.SigIndices) +} diff --git a/vms/secp256k1fx/camino_transfer_output.go b/vms/secp256k1fx/camino_transfer_output.go index 655bfb0235f8..a820a5ede894 100644 --- a/vms/secp256k1fx/camino_transfer_output.go +++ b/vms/secp256k1fx/camino_transfer_output.go @@ -55,3 +55,9 @@ func (out *CrossTransferOutput) Verify() error { return nil } + +// Used in vms/platformvm/txs/executor/camino_tx_executor.go func outputsAreEqual +func (out *TransferOutput) Equal(to any) bool { + toOut, ok := to.(*TransferOutput) + return ok && out.Amt == toOut.Amt && out.OutputOwners.Equals(&toOut.OutputOwners) +} diff --git a/wallet/chain/p/camino_visitor.go b/wallet/chain/p/camino_visitor.go index 6a5f178b990e..f22d361e55c3 100644 --- a/wallet/chain/p/camino_visitor.go +++ b/wallet/chain/p/camino_visitor.go @@ -46,6 +46,18 @@ func (b *backendVisitor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) AddProposalTx(tx *txs.AddProposalTx) error { + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) AddVoteTx(tx *txs.AddVoteTx) error { + return b.baseTx(&tx.BaseTx) +} + +func (*backendVisitor) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errUnsupportedTxType +} + // signer func (s *signerVisitor) AddressStateTx(tx *txs.AddressStateTx) error { @@ -115,3 +127,23 @@ func (s *signerVisitor) AddDepositOfferTx(tx *txs.AddDepositOfferTx) error { } return sign(s.tx, false, txSigners) } + +func (s *signerVisitor) AddProposalTx(tx *txs.AddProposalTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, false, txSigners) +} + +func (s *signerVisitor) AddVoteTx(tx *txs.AddVoteTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, false, txSigners) +} + +func (*signerVisitor) FinishProposalsTx(*txs.FinishProposalsTx) error { + return errUnsupportedTxType +}