From ea1c8552617c2658f7aa091882806301e8182e72 Mon Sep 17 00:00:00 2001 From: evlekht <> Date: Tue, 14 Nov 2023 17:14:30 +0400 Subject: [PATCH] [PVM, DAC] ExcludeMemberProposal --- vms/platformvm/camino_helpers_test.go | 2 +- vms/platformvm/camino_vm_test.go | 444 +++++++++- .../dac/camino_add_member_proposal.go | 5 +- .../dac/camino_base_fee_proposal.go | 5 +- .../dac/camino_exclude_member_proposal.go | 210 +++++ .../camino_exclude_member_proposal_test.go | 397 +++++++++ vms/platformvm/dac/camino_mock.go | 212 +++++ vms/platformvm/dac/camino_proposal.go | 10 + vms/platformvm/txs/builder/camino_builder.go | 35 +- vms/platformvm/txs/camino_add_proposal_tx.go | 2 + vms/platformvm/txs/codec.go | 3 + vms/platformvm/txs/dac/camino_dac.go | 294 +++++++ vms/platformvm/txs/dac/camino_dac_test.go | 665 +++++++++++++++ vms/platformvm/txs/dac/camino_helpers_test.go | 221 +++++ vms/platformvm/txs/executor/camino_dac.go | 147 ---- .../txs/executor/camino_dac_test.go | 390 --------- .../txs/executor/camino_helpers_test.go | 1 + .../txs/executor/camino_tx_executor.go | 41 +- .../txs/executor/camino_tx_executor_test.go | 791 ++++++++++++++---- 19 files changed, 3116 insertions(+), 759 deletions(-) create mode 100644 vms/platformvm/dac/camino_exclude_member_proposal.go create mode 100644 vms/platformvm/dac/camino_exclude_member_proposal_test.go create mode 100644 vms/platformvm/dac/camino_mock.go create mode 100644 vms/platformvm/txs/dac/camino_dac.go create mode 100644 vms/platformvm/txs/dac/camino_dac_test.go create mode 100644 vms/platformvm/txs/dac/camino_helpers_test.go delete mode 100644 vms/platformvm/txs/executor/camino_dac.go delete mode 100644 vms/platformvm/txs/executor/camino_dac_test.go diff --git a/vms/platformvm/camino_helpers_test.go b/vms/platformvm/camino_helpers_test.go index 15531b22970b..98b403b12bd0 100644 --- a/vms/platformvm/camino_helpers_test.go +++ b/vms/platformvm/camino_helpers_test.go @@ -115,7 +115,7 @@ func newCaminoVM(genesisConfig api.Camino, genesisUTXOs []api.UTXO, startTime *t return vm } -func defaultCaminoConfig(postBanff bool) config.Config { +func defaultCaminoConfig(postBanff bool) config.Config { //nolint:unparam banffTime := mockable.MaxTime if postBanff { banffTime = defaultValidateEndTime.Add(-2 * time.Second) diff --git a/vms/platformvm/camino_vm_test.go b/vms/platformvm/camino_vm_test.go index 107a74909bcf..5c0eb0f61690 100644 --- a/vms/platformvm/camino_vm_test.go +++ b/vms/platformvm/camino_vm_test.go @@ -91,17 +91,8 @@ func TestRemoveDeferredValidator(t *testing.T) { ) require.NoError(err) _ = buildAndAcceptBlock(t, vm, tx) - proposalTx := buildAddMemberProposalTx( - t, - vm, - caminoPreFundedKeys[0], - vm.Config.CaminoConfig.DACProposalBondAmount, - defaultTxFee, - adminProposerKey, // AdminProposer - consortiumMemberKey.Address(), - vm.clock.Time(), - true, - ) + proposalTx := buildAddMemberProposalTx(t, vm, caminoPreFundedKeys[0], vm.Config.CaminoConfig.DACProposalBondAmount, defaultTxFee, + adminProposerKey, consortiumMemberKey.Address(), vm.clock.Time(), true) _, _, _, _ = makeProposalWithTx(t, vm, proposalTx) // add admin proposal _ = buildAndAcceptBlock(t, vm, nil) // execute admin proposal @@ -277,17 +268,8 @@ func TestRemoveReactivatedValidator(t *testing.T) { ) require.NoError(err) _ = buildAndAcceptBlock(t, vm, tx) - proposalTx := buildAddMemberProposalTx( - t, - vm, - caminoPreFundedKeys[0], - vm.Config.CaminoConfig.DACProposalBondAmount, - defaultTxFee, - adminProposerKey, // AdminProposer - consortiumMemberKey.Address(), - vm.clock.Time(), - true, - ) + proposalTx := buildAddMemberProposalTx(t, vm, caminoPreFundedKeys[0], vm.Config.CaminoConfig.DACProposalBondAmount, defaultTxFee, + adminProposerKey, consortiumMemberKey.Address(), vm.clock.Time(), true) _, _, _, _ = makeProposalWithTx(t, vm, proposalTx) // add admin proposal _ = buildAndAcceptBlock(t, vm, nil) // execute admin proposal @@ -792,6 +774,359 @@ func TestAdminProposals(t *testing.T) { require.True(applicantAddrState.Is(as.AddressStateConsortiumMember)) } +func TestExcludeMemberProposals(t *testing.T) { + caminoPreFundedKey0AddrStr, err := address.FormatBech32(constants.NetworkIDToHRP[testNetworkID], caminoPreFundedKeys[0].Address().Bytes()) + require.NoError(t, err) + memberKey, memberAddr, _ := generateKeyAndOwner(t) + memberAddrStr, err := address.FormatBech32(constants.NetworkIDToHRP[testNetworkID], memberAddr.Bytes()) + require.NoError(t, err) + memberNodeKey, memberNodeShortID, _ := generateKeyAndOwner(t) + memberNodeID := ids.NodeID(memberNodeShortID) + rootAdminKey := caminoPreFundedKeys[0] + adminProposerKey := caminoPreFundedKeys[0] + + defaultConfig := defaultCaminoConfig(true) + fee := defaultConfig.TxFee + addValidatorFee := defaultConfig.AddPrimaryNetworkValidatorFee + proposalBondAmount := defaultConfig.CaminoConfig.DACProposalBondAmount + validatorBondAmount := defaultConfig.MaxValidatorStake // is equal to min + + tests := map[string]struct { + moreExlcude bool // try to exclude member with additional proposal + registerNode bool // member has registered node + currentValidator bool // member has current validator + pendingValidator bool // member has pending validator + expire bool // means that proposal should expire, not early finish + success bool // doesn't mean that most voted option is "yes", just means that proposal was successfully voted with some option + excluded bool // means that most voted option is "yes", proposal is successful and member was excluded + }{ + "Failed: tried to exclude with another proposal": { + moreExlcude: true, + }, + "Excluded: no registered node": { + success: true, + excluded: true, + }, + "Excluded: no validators": { + registerNode: true, + success: true, + excluded: true, + }, + "Excluded: has pending validator": { + registerNode: true, + pendingValidator: true, + success: true, + excluded: true, + }, + "Excluded: has active validator": { + registerNode: true, + currentValidator: true, + success: true, + excluded: true, + }, + "Not excluded: no registered node": { + success: true, + excluded: false, + }, + "Not excluded: no validators": { + registerNode: true, + success: true, + excluded: false, + }, + "Not excluded: has pending validator": { + registerNode: true, + pendingValidator: true, + success: true, + excluded: false, + }, + "Not excluded: has active validator": { + registerNode: true, + currentValidator: true, + success: true, + excluded: false, + }, + "Not excluded: expire": { + registerNode: true, + expire: true, + excluded: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + numberOfValidatorsInNetwork := 5 + if tt.currentValidator { + numberOfValidatorsInNetwork++ + } + burnedAmt := uint64(0) + bondedAmt := uint64(0) + balance := proposalBondAmount + defaultTxFee*8 + validatorBondAmount*2 + addValidatorFee*2 + // Prepare vm + vm := newCaminoVM(api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + InitialAdmin: caminoPreFundedKeys[0].Address(), + }, []api.UTXO{ + { + Amount: json.Uint64(balance), + Address: memberAddrStr, + }, + { + Amount: json.Uint64(defaultTxFee*3 + proposalBondAmount*2), + Address: caminoPreFundedKey0AddrStr, + }, + }, &defaultConfig.BanffTime) + vm.ctx.Lock.Lock() + defer func() { require.NoError(vm.Shutdown(context.Background())) }() //nolint:lint + initialMemberBalance, _, _, _, _ := getBalance(t, vm.state, memberAddr) + checkBalance(t, vm.state, memberAddr, + initialMemberBalance, // total + 0, 0, 0, initialMemberBalance, // unlocked + ) + memberAddrState, err := vm.state.GetAddressStates(memberAddr) + require.NoError(err) + require.Equal(as.AddressStateEmpty, memberAddrState) + _, err = vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + require.ErrorIs(err, database.ErrNotFound) + + // Make member actual consortium member + tx, err := vm.txBuilder.NewAddressStateTx( + adminProposerKey.Address(), + false, + as.AddressStateBitRoleConsortiumAdminProposer, + []*secp256k1.PrivateKey{rootAdminKey}, + nil, + ) + require.NoError(err) + _ = buildAndAcceptBlock(t, vm, tx) + addMemberProposalTx := buildAddMemberProposalTx(t, vm, caminoPreFundedKeys[0], proposalBondAmount, defaultTxFee, + adminProposerKey, memberAddr, vm.clock.Time(), true) + _, _, _, _ = makeProposalWithTx(t, vm, addMemberProposalTx) // add admin proposal + _ = buildAndAcceptBlock(t, vm, nil) // execute admin proposal + memberAddrState, err = vm.state.GetAddressStates(memberAddr) + require.NoError(err) + require.Equal(as.AddressStateConsortiumMember, memberAddrState) + + // Register member's node + if tt.registerNode { + registerNodeTx, err := vm.txBuilder.NewRegisterNodeTx( + ids.EmptyNodeID, + memberNodeID, + memberAddr, + []*secp256k1.PrivateKey{memberKey, memberNodeKey}, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{memberAddr}, + }, + ) + require.NoError(err) + blk := buildAndAcceptBlock(t, vm, registerNodeTx) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), registerNodeTx.ID()) + registeredMemberNodeID, err := vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + require.NoError(err) + require.Equal(memberNodeShortID, registeredMemberNodeID) + burnedAmt += fee + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + 0, 0, 0, balance-burnedAmt, // unlocked + ) + } + + chainTime := vm.state.GetTimestamp() + pendingValidatorStartTime := chainTime.Add(120 * time.Second) + currentValidatorStartTime := chainTime.Add(240 * time.Second) + validatorsEndTime := currentValidatorStartTime.Add(defaultConfig.MinStakeDuration) + + // Add pending validator + if tt.pendingValidator { + addValidatorTx, err := vm.txBuilder.NewCaminoAddValidatorTx( + validatorBondAmount, + uint64(pendingValidatorStartTime.Unix()), + uint64(validatorsEndTime.Unix()), + memberNodeID, + memberAddr, + memberAddr, + 0, + []*secp256k1.PrivateKey{memberKey}, + memberAddr, + ) + require.NoError(err) + blk := buildAndAcceptBlock(t, vm, addValidatorTx) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), addValidatorTx.ID()) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + require.NoError(err) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + require.ErrorIs(err, database.ErrNotFound) + burnedAmt += addValidatorFee + bondedAmt += validatorBondAmount + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + } + + // Add current validator + if tt.currentValidator { + // Add current validator as pending + addValidatorTx, err := vm.txBuilder.NewCaminoAddValidatorTx( + validatorBondAmount, + uint64(currentValidatorStartTime.Unix()), + uint64(validatorsEndTime.Unix()), + memberNodeID, + memberAddr, + memberAddr, + 0, + []*secp256k1.PrivateKey{memberKey}, + memberAddr, + ) + require.NoError(err) + blk := buildAndAcceptBlock(t, vm, addValidatorTx) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), addValidatorTx.ID()) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + require.NoError(err) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + require.ErrorIs(err, database.ErrNotFound) + + // Advance time, so pending validator will become current + vm.clock.Set(currentValidatorStartTime) + blk = buildAndAcceptBlock(t, vm, nil) + require.Empty(blk.Txs()) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + require.ErrorIs(err, database.ErrNotFound) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + require.NoError(err) + burnedAmt += addValidatorFee + bondedAmt += validatorBondAmount + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + } + + chainTime = vm.state.GetTimestamp() + + // Add proposal (member proposes to exclude himself using his own funds) + proposalStartTime := chainTime.Add(100 * time.Second) + proposalEndTime := proposalStartTime.Add(time.Duration(dac.ExcludeMemberProposalMinDuration) * time.Second) + excludeMemberProposalTx := buildExcludeMemberProposalTx(t, vm, memberKey, proposalBondAmount, fee, + memberKey, memberAddr, proposalStartTime, proposalEndTime, false) + proposalState, _, _, _ := makeProposalWithTx(t, vm, excludeMemberProposalTx) + excludeMemberProposalState, ok := proposalState.(*dac.ExcludeMemberProposalState) + require.True(ok) + burnedAmt += fee + bondedAmt += proposalBondAmount + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + + vm.clock.Set(excludeMemberProposalState.StartTime()) + + if tt.moreExlcude { + excludeMemberProposalTx := buildExcludeMemberProposalTx(t, vm, caminoPreFundedKeys[0], proposalBondAmount, fee, + adminProposerKey, memberAddr, proposalStartTime, proposalStartTime.Add(time.Duration(dac.ExcludeMemberProposalMinDuration)*time.Second), true) + require.Error(vm.Builder.AddUnverifiedTx(excludeMemberProposalTx)) + return + } + + // If we want proposal to succeed, pick option 0, to fail - option 1 + optionIndex := uint32(1) + if tt.excluded { + optionIndex = 0 + } + optionWeights := make([]uint32, len(excludeMemberProposalState.Options)) + + // If proposal should be finished early, than we're voting for it with enough votes + numberOfVotesToSuccess := numberOfValidatorsInNetwork/2 + 1 + if tt.expire { + numberOfVotesToSuccess = 0 + } + + for i := 0; i < numberOfVotesToSuccess; i++ { + optionWeights[optionIndex]++ + voteTx := buildSimpleVoteTx(t, vm, memberKey, fee, excludeMemberProposalTx.ID(), caminoPreFundedKeys[i], optionIndex) + proposalState = voteWithTx(t, vm, voteTx, excludeMemberProposalTx.ID(), optionWeights) + burnedAmt += fee + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + } + require.Equal(tt.success, proposalState.IsSuccessful()) + + // If we need proposal to expire, advance time forward + if tt.expire { + vm.clock.Set(proposalState.EndTime()) + } + + // Build block with FinishProposalsTx + blk := buildAndAcceptBlock(t, vm, nil) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), blk.Txs()[0].ID()) + _, err = vm.state.GetProposal(excludeMemberProposalTx.ID()) + require.ErrorIs(err, database.ErrNotFound) + + bondedAmt -= proposalBondAmount + memberAddrState, err = vm.state.GetAddressStates(memberAddr) + require.NoError(err) + _, pendingValidatorErr := vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + _, currentValidatorErr := vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + _, suspendedValidatorErr := vm.state.GetDeferredValidator(constants.PrimaryNetworkID, memberNodeID) + registeredMemberNodeID, registeredNodeErr := vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + if tt.excluded { + require.Equal(as.AddressStateEmpty, memberAddrState) + require.ErrorIs(pendingValidatorErr, database.ErrNotFound) + require.ErrorIs(currentValidatorErr, database.ErrNotFound) + require.ErrorIs(registeredNodeErr, database.ErrNotFound) + if tt.currentValidator { + require.NoError(suspendedValidatorErr) + } else { + require.ErrorIs(suspendedValidatorErr, database.ErrNotFound) + } + if tt.pendingValidator { + bondedAmt -= validatorBondAmount + } + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + } else { + require.Equal(as.AddressStateConsortiumMember, memberAddrState) + if tt.pendingValidator { + require.NoError(pendingValidatorErr) + } else { + require.ErrorIs(pendingValidatorErr, database.ErrNotFound) + } + if tt.currentValidator { + require.NoError(currentValidatorErr) + } else { + require.ErrorIs(currentValidatorErr, database.ErrNotFound) + } + if tt.registerNode { + require.NoError(registeredNodeErr) + require.Equal(memberNodeShortID, registeredMemberNodeID) + } else { + require.ErrorIs(registeredNodeErr, database.ErrNotFound) + } + require.ErrorIs(suspendedValidatorErr, database.ErrNotFound) + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-burnedAmt-bondedAmt, // unlocked + ) + } + }) + } +} + func buildAndAcceptBlock(t *testing.T, vm *VM, tx *txs.Tx) blocks.Block { t.Helper() if tx != nil { @@ -852,11 +1187,11 @@ func checkBalance( ) { 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) + require.Equal(t, expectedTotal, total, "total balance") + require.Equal(t, expectedBonded, bonded, "bonded balance") + require.Equal(t, expectedDeposited, deposited, "deposited balance") + require.Equal(t, expectedDepositBonded, depositBonded, "depositBonded balance") + require.Equal(t, expectedUnlocked, unlocked, "unlocked balance") } func checkTx(t *testing.T, vm *VM, blkID, txID ids.ID) { @@ -923,7 +1258,7 @@ func buildAddMemberProposalTx( proposerKey *secp256k1.PrivateKey, applicantAddress ids.ShortID, startTime time.Time, - adminProposal bool, + adminProposal bool, //nolint:unparam ) *txs.Tx { t.Helper() ins, outs, signers, _, err := vm.txBuilder.Lock( @@ -961,6 +1296,54 @@ func buildAddMemberProposalTx( return proposalTx } +func buildExcludeMemberProposalTx( + t *testing.T, + vm *VM, + fundsKey *secp256k1.PrivateKey, + amountToBond uint64, + amountToBurn uint64, + proposerKey *secp256k1.PrivateKey, + memberAddress ids.ShortID, + startTime time.Time, + endTime time.Time, + adminProposal bool, +) *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) + var proposal dac.Proposal = &dac.ExcludeMemberProposal{ + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + MemberAddress: memberAddress, + } + if adminProposal { + proposal = &dac.AdminProposal{Proposal: proposal} + } + wrapper := &txs.ProposalWrapper{Proposal: proposal} + proposalBytes, err := txs.Codec.Marshal(txs.Version, wrapper) + 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, @@ -1040,6 +1423,11 @@ func voteWithTx( for i := range proposalState.Options { require.Equal(t, expectedVoteWeights[i], proposalState.Options[i].Weight) } + case *dac.ExcludeMemberProposalState: + require.Len(t, proposalState.Options, len(expectedVoteWeights)) + for i := range proposalState.Options { + require.Equal(t, expectedVoteWeights[i], proposalState.Options[i].Weight) + } default: require.Fail(t, "unexpected proposalState type") } diff --git a/vms/platformvm/dac/camino_add_member_proposal.go b/vms/platformvm/dac/camino_add_member_proposal.go index e18d44c4c48c..ebe9f06d13ed 100644 --- a/vms/platformvm/dac/camino_add_member_proposal.go +++ b/vms/platformvm/dac/camino_add_member_proposal.go @@ -125,7 +125,6 @@ func (p *AddMemberProposalState) Outcome() any { return mostVotedOptionIndex } -// Votes must be valid for this proposal, could panic otherwise. func (p *AddMemberProposalState) Result() (bool, uint32, bool) { mostVotedWeight, mostVotedOptionIndex, unambiguous := p.GetMostVoted() return p.Options[mostVotedOptionIndex].Value, mostVotedWeight, unambiguous @@ -196,3 +195,7 @@ func (p *AddMemberProposalState) ForceAddVote(voteIntf Vote) (ProposalState, err func (p *AddMemberProposalState) Visit(visitor ExecutorVisitor) error { return visitor.AddMemberProposal(p) } + +func (p *AddMemberProposalState) GetBondTxIDs(visitor BondTxIDsGetter) ([]ids.ID, error) { + return visitor.AddMemberProposal(p) +} diff --git a/vms/platformvm/dac/camino_base_fee_proposal.go b/vms/platformvm/dac/camino_base_fee_proposal.go index 11580e81fe1a..546ba7cbf4c3 100644 --- a/vms/platformvm/dac/camino_base_fee_proposal.go +++ b/vms/platformvm/dac/camino_base_fee_proposal.go @@ -136,7 +136,6 @@ func (p *BaseFeeProposalState) Outcome() any { 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 @@ -205,3 +204,7 @@ func (p *BaseFeeProposalState) ForceAddVote(voteIntf Vote) (ProposalState, error func (p *BaseFeeProposalState) Visit(visitor ExecutorVisitor) error { return visitor.BaseFeeProposal(p) } + +func (p *BaseFeeProposalState) GetBondTxIDs(visitor BondTxIDsGetter) ([]ids.ID, error) { + return visitor.BaseFeeProposal(p) +} diff --git a/vms/platformvm/dac/camino_exclude_member_proposal.go b/vms/platformvm/dac/camino_exclude_member_proposal.go new file mode 100644 index 000000000000..93b8af737fc2 --- /dev/null +++ b/vms/platformvm/dac/camino_exclude_member_proposal.go @@ -0,0 +1,210 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "bytes" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/ids" + as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" + "golang.org/x/exp/slices" +) + +const ( + ExcludeMemberProposalMinDuration = uint64(time.Hour * 24 * 7 / time.Second) // 7 days + ExcludeMemberProposalMaxDuration = uint64(time.Hour * 24 * 30 / time.Second) // 30 days +) + +var ( + _ Proposal = (*ExcludeMemberProposal)(nil) + _ ProposalState = (*ExcludeMemberProposalState)(nil) +) + +type ExcludeMemberProposal struct { + MemberAddress ids.ShortID `serialize:"true"` + Start uint64 `serialize:"true"` + End uint64 `serialize:"true"` +} + +func (p *ExcludeMemberProposal) StartTime() time.Time { + return time.Unix(int64(p.Start), 0) +} + +func (p *ExcludeMemberProposal) EndTime() time.Time { + return time.Unix(int64(p.End), 0) +} + +func (*ExcludeMemberProposal) GetOptions() any { + return []bool{true, false} +} + +func (p *ExcludeMemberProposal) GetData() any { + return p.MemberAddress +} + +func (*ExcludeMemberProposal) AdminProposer() as.AddressState { + return as.AddressStateRoleConsortiumAdminProposer +} + +func (p *ExcludeMemberProposal) Verify() error { + switch { + case p.Start >= p.End: + return errEndNotAfterStart + case p.End-p.Start < ExcludeMemberProposalMinDuration: + return fmt.Errorf("%w (expected: minimum duration %d, actual: %d)", errWrongDuration, ExcludeMemberProposalMinDuration, p.End-p.Start) + case p.End-p.Start > ExcludeMemberProposalMaxDuration: + return fmt.Errorf("%w (expected: maximum duration %d, actual: %d)", errWrongDuration, ExcludeMemberProposalMaxDuration, p.End-p.Start) + } + return nil +} + +func (p *ExcludeMemberProposal) CreateProposalState(allowedVoters []ids.ShortID) ProposalState { + stateProposal := &ExcludeMemberProposalState{ + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true}, + {Value: false}, + }, + }, + MemberAddress: p.MemberAddress, + Start: p.Start, + End: p.End, + AllowedVoters: allowedVoters, + TotalAllowedVoters: uint32(len(allowedVoters)), + } + return stateProposal +} + +func (p *ExcludeMemberProposal) CreateFinishedProposalState(optionIndex uint32) (ProposalState, error) { + if optionIndex >= 2 { + return nil, fmt.Errorf("%w (expected: less than 2, actual: %d)", errWrongOptionIndex, optionIndex) + } + proposalState := p.CreateProposalState([]ids.ShortID{}).(*ExcludeMemberProposalState) + proposalState.Options[optionIndex].Weight++ + return proposalState, nil +} + +func (p *ExcludeMemberProposal) Visit(visitor VerifierVisitor) error { + return visitor.ExcludeMemberProposal(p) +} + +type ExcludeMemberProposalState struct { + SimpleVoteOptions[bool] `serialize:"true"` + + MemberAddress ids.ShortID `serialize:"true"` + Start uint64 `serialize:"true"` + End uint64 `serialize:"true"` + AllowedVoters []ids.ShortID `serialize:"true"` + TotalAllowedVoters uint32 `serialize:"true"` +} + +func (p *ExcludeMemberProposalState) StartTime() time.Time { + return time.Unix(int64(p.Start), 0) +} + +func (p *ExcludeMemberProposalState) EndTime() time.Time { + return time.Unix(int64(p.End), 0) +} + +func (p *ExcludeMemberProposalState) IsActiveAt(time time.Time) bool { + timestamp := uint64(time.Unix()) + return p.Start <= timestamp && timestamp <= p.End +} + +func (p *ExcludeMemberProposalState) CanBeFinished() bool { + mostVotedWeight, _, unambiguous := p.GetMostVoted() + voted := p.Voted() + return voted == p.TotalAllowedVoters || unambiguous && mostVotedWeight > p.TotalAllowedVoters/2 +} + +func (p *ExcludeMemberProposalState) IsSuccessful() bool { + mostVotedWeight, _, unambiguous := p.GetMostVoted() + voted := p.Voted() + return unambiguous && voted > p.TotalAllowedVoters/2 && mostVotedWeight > voted/2 +} + +func (p *ExcludeMemberProposalState) Outcome() any { + _, mostVotedOptionIndex, unambiguous := p.GetMostVoted() + if !unambiguous { + return -1 + } + return mostVotedOptionIndex +} + +func (p *ExcludeMemberProposalState) Result() (bool, 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 *ExcludeMemberProposalState) 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 := &ExcludeMemberProposalState{ + MemberAddress: p.MemberAddress, + Start: p.Start, + End: p.End, + AllowedVoters: make([]ids.ShortID, len(p.AllowedVoters)-1), + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: make([]SimpleVoteOption[bool], 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 *ExcludeMemberProposalState) ForceAddVote(voteIntf Vote) (ProposalState, error) { + vote, ok := voteIntf.(*SimpleVote) + if !ok { + return nil, ErrWrongVote + } + if int(vote.OptionIndex) >= len(p.Options) { + return nil, ErrWrongVote + } + + updatedProposal := &ExcludeMemberProposalState{ + MemberAddress: p.MemberAddress, + Start: p.Start, + End: p.End, + AllowedVoters: p.AllowedVoters, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: make([]SimpleVoteOption[bool], 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 *ExcludeMemberProposalState) Visit(visitor ExecutorVisitor) error { + return visitor.ExcludeMemberProposal(p) +} + +func (p *ExcludeMemberProposalState) GetBondTxIDs(visitor BondTxIDsGetter) ([]ids.ID, error) { + return visitor.ExcludeMemberProposal(p) +} diff --git a/vms/platformvm/dac/camino_exclude_member_proposal_test.go b/vms/platformvm/dac/camino_exclude_member_proposal_test.go new file mode 100644 index 000000000000..5367eecc1850 --- /dev/null +++ b/vms/platformvm/dac/camino_exclude_member_proposal_test.go @@ -0,0 +1,397 @@ +// 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 TestExcludeMemberProposalCreateProposalState(t *testing.T) { + tests := map[string]struct { + proposal *ExcludeMemberProposal + allowedVoters []ids.ShortID + expectedProposalState ProposalState + expectedProposal *ExcludeMemberProposal + }{ + "OK: even number of allowed voters": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + }, + allowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}}, + expectedProposalState: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + AllowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true}, + {Value: false}, + }, + }, + TotalAllowedVoters: 4, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + }, + }, + "OK: odd number of allowed voters": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + }, + allowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}, {5}}, + expectedProposalState: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + AllowedVoters: []ids.ShortID{{1}, {2}, {3}, {4}, {5}}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true}, + {Value: false}, + }, + }, + TotalAllowedVoters: 5, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + }, + }, + } + 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 TestExcludeMemberProposalStateAddVote(t *testing.T) { + voterAddr1 := ids.ShortID{1} + voterAddr2 := ids.ShortID{1} + voterAddr3 := ids.ShortID{1} + + tests := map[string]struct { + proposal *ExcludeMemberProposalState + voterAddr ids.ShortID + vote Vote + expectedUpdatedProposal ProposalState + expectedOriginalProposal *ExcludeMemberProposalState + expectedErr error + }{ + "Wrong vote type": { + proposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &DummyVote{}, // not *SimpleVote + expectedOriginalProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + expectedErr: ErrWrongVote, + }, + "Wrong vote option index": { + proposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: ids.ShortID{3}, + vote: &SimpleVote{OptionIndex: 2}, + expectedOriginalProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + expectedErr: ErrWrongVote, + }, + "Not allowed to vote on this proposal": { + proposal: &ExcludeMemberProposalState{ + AllowedVoters: []ids.ShortID{{1}, {2}}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{{}}, + }, + }, + voterAddr: ids.ShortID{3}, + vote: &SimpleVote{OptionIndex: 0}, + expectedOriginalProposal: &ExcludeMemberProposalState{ + AllowedVoters: []ids.ShortID{{1}, {2}}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{{}}, + }, + }, + expectedErr: ErrNotAllowedToVoteOnProposal, + }, + "OK: adding vote to not voted option": { + proposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 0}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &SimpleVote{OptionIndex: 1}, + expectedUpdatedProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + }, + }, + expectedOriginalProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 0}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + }, + "OK: adding vote to already voted option": { + proposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + voterAddr: voterAddr1, + vote: &SimpleVote{OptionIndex: 1}, + expectedUpdatedProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 2}, // 1 + }, + }, + }, + expectedOriginalProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, // 0 + {Value: false, Weight: 1}, // 1 + }, + mostVotedWeight: 2, + mostVotedOptionIndex: 0, + unambiguous: true, + }, + }, + }, + "OK: voter addr in the middle of allowedVoters array": { + proposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{{}}, + }, + }, + voterAddr: voterAddr2, + vote: &SimpleVote{OptionIndex: 0}, + expectedUpdatedProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{{Weight: 1}}, + }, + }, + expectedOriginalProposal: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + TotalAllowedVoters: 555, + AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2, voterAddr3}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{{}}, + }, + }, + }, + } + 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) + }) + } +} + +func TestExcludeMemberProposalCreateFinishedProposalState(t *testing.T) { + memberAddress := ids.ShortID{1} + + tests := map[string]struct { + proposal *ExcludeMemberProposal + optionIndex uint32 + expectedProposalState ProposalState + expectedOriginalProposal *ExcludeMemberProposal + expectedErr error + }{ + "Fail: option 2 out of bonds": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + optionIndex: 2, + expectedOriginalProposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + expectedErr: errWrongOptionIndex, + }, + "OK: option 0": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + optionIndex: 0, + expectedProposalState: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true, Weight: 1}, + {Value: false}, + }, + }, + }, + expectedOriginalProposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + }, + "OK: option 1": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + optionIndex: 1, + expectedProposalState: &ExcludeMemberProposalState{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + AllowedVoters: []ids.ShortID{}, + SimpleVoteOptions: SimpleVoteOptions[bool]{ + Options: []SimpleVoteOption[bool]{ + {Value: true}, + {Value: false, Weight: 1}, + }, + }, + }, + expectedOriginalProposal: &ExcludeMemberProposal{ + Start: 100, + End: 101, + MemberAddress: memberAddress, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + proposalState, err := tt.proposal.CreateFinishedProposalState(tt.optionIndex) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedProposalState, proposalState) + require.Equal(t, tt.expectedOriginalProposal, tt.proposal) + if tt.expectedErr == nil { + require.True(t, proposalState.CanBeFinished()) + require.True(t, proposalState.IsSuccessful()) + } + }) + } +} diff --git a/vms/platformvm/dac/camino_mock.go b/vms/platformvm/dac/camino_mock.go new file mode 100644 index 000000000000..c78f3c8048fb --- /dev/null +++ b/vms/platformvm/dac/camino_mock.go @@ -0,0 +1,212 @@ +package dac + +import ( + "reflect" + + "github.com/ava-labs/avalanchego/ids" + "github.com/golang/mock/gomock" +) + +var ( + _ VerifierVisitor = (*MockVerifierVisitor)(nil) + _ ExecutorVisitor = (*MockExecutorVisitor)(nil) + _ BondTxIDsGetter = (*MockBondTxIDsGetter)(nil) +) + +// MockVerifierVisitor is a mock of VerifierVisitor interface. +type MockVerifierVisitor struct { + ctrl *gomock.Controller + recorder *MockVerifierVisitorMockRecorder +} + +// MockVerifierVisitorMockRecorder is the mock recorder for MockVerifierVisitor. +type MockVerifierVisitorMockRecorder struct { + mock *MockVerifierVisitor +} + +// NewMockVerifierVisitor creates a new mock instance. +func NewMockVerifierVisitor(ctrl *gomock.Controller) *MockVerifierVisitor { + mock := &MockVerifierVisitor{ctrl: ctrl} + mock.recorder = &MockVerifierVisitorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVerifierVisitor) EXPECT() *MockVerifierVisitorMockRecorder { + return m.recorder +} + +// BaseFeeProposal mocks base method. +func (m *MockVerifierVisitor) BaseFeeProposal(arg0 *BaseFeeProposal) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseFeeProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// BaseFeeProposal indicates an expected call of BaseFeeProposal. +func (mr *MockVerifierVisitorMockRecorder) BaseFeeProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseFeeProposal", reflect.TypeOf((*MockVerifierVisitor)(nil).BaseFeeProposal), arg0) +} + +// AddMemberProposal mocks base method. +func (m *MockVerifierVisitor) AddMemberProposal(arg0 *AddMemberProposal) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddMemberProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddMemberProposal indicates an expected call of AddMemberProposal. +func (mr *MockVerifierVisitorMockRecorder) AddMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMemberProposal", reflect.TypeOf((*MockVerifierVisitor)(nil).AddMemberProposal), arg0) +} + +// ExcludeMemberProposal mocks base method. +func (m *MockVerifierVisitor) ExcludeMemberProposal(arg0 *ExcludeMemberProposal) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExcludeMemberProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExcludeMemberProposal indicates an expected call of ExcludeMemberProposal. +func (mr *MockVerifierVisitorMockRecorder) ExcludeMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExcludeMemberProposal", reflect.TypeOf((*MockVerifierVisitor)(nil).ExcludeMemberProposal), arg0) +} + +// MockExecutorVisitor is a mock of ExecutorVisitor interface. +type MockExecutorVisitor struct { + ctrl *gomock.Controller + recorder *MockExecutorVisitorMockRecorder +} + +// MockExecutorVisitorMockRecorder is the mock recorder for MockExecutorVisitor. +type MockExecutorVisitorMockRecorder struct { + mock *MockExecutorVisitor +} + +// NewMockExecutorVisitor creates a new mock instance. +func NewMockExecutorVisitor(ctrl *gomock.Controller) *MockExecutorVisitor { + mock := &MockExecutorVisitor{ctrl: ctrl} + mock.recorder = &MockExecutorVisitorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExecutorVisitor) EXPECT() *MockExecutorVisitorMockRecorder { + return m.recorder +} + +// BaseFeeProposal mocks base method. +func (m *MockExecutorVisitor) BaseFeeProposal(arg0 *BaseFeeProposalState) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseFeeProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// BaseFeeProposal indicates an expected call of BaseFeeProposal. +func (mr *MockExecutorVisitorMockRecorder) BaseFeeProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseFeeProposal", reflect.TypeOf((*MockExecutorVisitor)(nil).BaseFeeProposal), arg0) +} + +// AddMemberProposal mocks base method. +func (m *MockExecutorVisitor) AddMemberProposal(arg0 *AddMemberProposalState) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddMemberProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddMemberProposal indicates an expected call of AddMemberProposal. +func (mr *MockExecutorVisitorMockRecorder) AddMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMemberProposal", reflect.TypeOf((*MockExecutorVisitor)(nil).AddMemberProposal), arg0) +} + +// ExcludeMemberProposal mocks base method. +func (m *MockExecutorVisitor) ExcludeMemberProposal(arg0 *ExcludeMemberProposalState) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExcludeMemberProposal", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExcludeMemberProposal indicates an expected call of ExcludeMemberProposal. +func (mr *MockExecutorVisitorMockRecorder) ExcludeMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExcludeMemberProposal", reflect.TypeOf((*MockExecutorVisitor)(nil).ExcludeMemberProposal), arg0) +} + +// MockBondTxIDsGetter is a mock of BondTxIDsGetter interface. +type MockBondTxIDsGetter struct { + ctrl *gomock.Controller + recorder *MockBondTxIDsGetterMockRecorder +} + +// MockBondTxIDsGetterMockRecorder is the mock recorder for MockBondTxIDsGetter. +type MockBondTxIDsGetterMockRecorder struct { + mock *MockBondTxIDsGetter +} + +// NewMockBondTxIDsGetter creates a new mock instance. +func NewMockBondTxIDsGetter(ctrl *gomock.Controller) *MockBondTxIDsGetter { + mock := &MockBondTxIDsGetter{ctrl: ctrl} + mock.recorder = &MockBondTxIDsGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBondTxIDsGetter) EXPECT() *MockBondTxIDsGetterMockRecorder { + return m.recorder +} + +// BaseFeeProposal mocks base method. +func (m *MockBondTxIDsGetter) BaseFeeProposal(arg0 *BaseFeeProposalState) ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseFeeProposal", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BaseFeeProposal indicates an expected call of BaseFeeProposal. +func (mr *MockBondTxIDsGetterMockRecorder) BaseFeeProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseFeeProposal", reflect.TypeOf((*MockBondTxIDsGetter)(nil).BaseFeeProposal), arg0) +} + +// AddMemberProposal mocks base method. +func (m *MockBondTxIDsGetter) AddMemberProposal(arg0 *AddMemberProposalState) ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddMemberProposal", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddMemberProposal indicates an expected call of AddMemberProposal. +func (mr *MockBondTxIDsGetterMockRecorder) AddMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMemberProposal", reflect.TypeOf((*MockBondTxIDsGetter)(nil).AddMemberProposal), arg0) +} + +// ExcludeMemberProposal mocks base method. +func (m *MockBondTxIDsGetter) ExcludeMemberProposal(arg0 *ExcludeMemberProposalState) ([]ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExcludeMemberProposal", arg0) + ret0, _ := ret[0].([]ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExcludeMemberProposal indicates an expected call of ExcludeMemberProposal. +func (mr *MockBondTxIDsGetterMockRecorder) ExcludeMemberProposal(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExcludeMemberProposal", reflect.TypeOf((*MockBondTxIDsGetter)(nil).ExcludeMemberProposal), arg0) +} diff --git a/vms/platformvm/dac/camino_proposal.go b/vms/platformvm/dac/camino_proposal.go index 47523f839cfc..136718fd3217 100644 --- a/vms/platformvm/dac/camino_proposal.go +++ b/vms/platformvm/dac/camino_proposal.go @@ -25,11 +25,19 @@ var ( type VerifierVisitor interface { BaseFeeProposal(*BaseFeeProposal) error AddMemberProposal(*AddMemberProposal) error + ExcludeMemberProposal(*ExcludeMemberProposal) error } type ExecutorVisitor interface { BaseFeeProposal(*BaseFeeProposalState) error AddMemberProposal(*AddMemberProposalState) error + ExcludeMemberProposal(*ExcludeMemberProposalState) error +} + +type BondTxIDsGetter interface { + BaseFeeProposal(*BaseFeeProposalState) ([]ids.ID, error) + AddMemberProposal(*AddMemberProposalState) ([]ids.ID, error) + ExcludeMemberProposal(*ExcludeMemberProposalState) ([]ids.ID, error) } type Proposal interface { @@ -62,6 +70,8 @@ type ProposalState interface { IsSuccessful() bool // should be called only for finished proposals Outcome() any // should be called only for finished successful proposals Visit(ExecutorVisitor) error + // Visits getter and returns additional lock tx ids, that should be unbonded when this proposal is successfully finished. + GetBondTxIDs(BondTxIDsGetter) ([]ids.ID, error) // Will return modified ProposalState with added vote, original ProposalState will not be modified! AddVote(voterAddress ids.ShortID, vote Vote) (ProposalState, error) diff --git a/vms/platformvm/txs/builder/camino_builder.go b/vms/platformvm/txs/builder/camino_builder.go index 8e765daf0f77..a6768d0a82f3 100644 --- a/vms/platformvm/txs/builder/camino_builder.go +++ b/vms/platformvm/txs/builder/camino_builder.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/treasury" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/dac" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -696,28 +697,24 @@ func (b *caminoBuilder) FinishProposalsTx( 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, }}} + lockTxIDs := append(earlyFinishedProposalIDs, expiredProposalIDs...) //nolint:gocritic + 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() { + bondTxIDs, err := proposal.GetBondTxIDs(dac.ProposalBondTxIDsGetter(state)) + if err != nil { + return nil, err + } + lockTxIDs = append(lockTxIDs, bondTxIDs...) utx.EarlyFinishedSuccessfulProposalIDs = append(utx.EarlyFinishedSuccessfulProposalIDs, proposalID) } else { utx.EarlyFinishedFailedProposalIDs = append(utx.EarlyFinishedFailedProposalIDs, proposalID) @@ -729,6 +726,11 @@ func (b *caminoBuilder) FinishProposalsTx( return nil, fmt.Errorf("couldn't get proposal from state: %w", err) } if proposal.IsSuccessful() { + bondTxIDs, err := proposal.GetBondTxIDs(dac.ProposalBondTxIDsGetter(state)) + if err != nil { + return nil, err + } + lockTxIDs = append(lockTxIDs, bondTxIDs...) utx.ExpiredSuccessfulProposalIDs = append(utx.ExpiredSuccessfulProposalIDs, proposalID) } else { utx.ExpiredFailedProposalIDs = append(utx.ExpiredFailedProposalIDs, proposalID) @@ -740,6 +742,17 @@ func (b *caminoBuilder) FinishProposalsTx( utils.Sort(utx.ExpiredSuccessfulProposalIDs) utils.Sort(utx.ExpiredFailedProposalIDs) + ins, outs, err := b.Unlock( + state, + lockTxIDs, + locked.StateBonded, + ) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + utx.Ins = ins + utx.Outs = outs + tx, err := txs.NewSigned(utx, txs.Codec, nil) if err != nil { return nil, err diff --git a/vms/platformvm/txs/camino_add_proposal_tx.go b/vms/platformvm/txs/camino_add_proposal_tx.go index 00e8cb02add1..384ec412d741 100644 --- a/vms/platformvm/txs/camino_add_proposal_tx.go +++ b/vms/platformvm/txs/camino_add_proposal_tx.go @@ -29,6 +29,8 @@ type AddProposalTx struct { BaseTx `serialize:"true"` // Proposal bytes ProposalPayload []byte `serialize:"true" json:"proposalPayload"` + // Flag that tells if proposal is adminProposal. Admin proposals are auto-executed without voting and can only be proposed by corresponding role. + IsAdminProposal bool `serialize:"true" json:"isAdminProposal"` // 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 diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 96e58ec925ee..1524fa74d63b 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -133,6 +133,9 @@ func RegisterUnsignedTxsTypes(targetCodec codec.CaminoRegistry) error { targetCodec.RegisterCustomType(&dac.AddMemberProposal{}), targetCodec.RegisterCustomType(&dac.AddMemberProposalState{}), targetCodec.RegisterCustomType(&dac.AdminProposal{}), + targetCodec.RegisterCustomType(&dac.ExcludeMemberProposal{}), + targetCodec.RegisterCustomType(&dac.ExcludeMemberProposalState{}), + // TODO@ maybe move proposal states to their own codec? not part of the txs! only used in state ) return errs.Err } diff --git a/vms/platformvm/txs/dac/camino_dac.go b/vms/platformvm/txs/dac/camino_dac.go new file mode 100644 index 000000000000..e98ced1dda3e --- /dev/null +++ b/vms/platformvm/txs/dac/camino_dac.go @@ -0,0 +1,294 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "errors" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" + "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) + + errNotConsortiumMember = errors.New("address isn't consortium member") + errConsortiumMember = errors.New("address is consortium member") + 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 +} + +// 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 proposalBondTxIDsGetter struct { + state state.Chain +} + +func ProposalVerifier(state state.Chain, fx fx.Fx, signedTx *txs.Tx, tx *txs.AddProposalTx) *proposalVerifier { + return &proposalVerifier{ + state: state, + fx: fx, + signedAddProposalTx: signedTx, + addProposalTx: tx, + } +} + +func ProposalExecutor(state state.Chain, fx fx.Fx) *proposalExecutor { + return &proposalExecutor{state: state, fx: fx} +} + +func ProposalBondTxIDsGetter(state state.Chain) *proposalBondTxIDsGetter { + return &proposalBondTxIDsGetter{state: state} +} + +// 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(as.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 +} + +func (e *proposalExecutor) BaseFeeProposal(proposal *dac.BaseFeeProposalState) error { + _, mostVotedOptionIndex, _ := proposal.GetMostVoted() + e.state.SetBaseFee(proposal.Options[mostVotedOptionIndex].Value) + return nil +} + +func (*proposalBondTxIDsGetter) BaseFeeProposal(*dac.BaseFeeProposalState) ([]ids.ID, error) { + return nil, nil +} + +// AddMemberProposal + +func (e *proposalVerifier) AddMemberProposal(proposal *dac.AddMemberProposal) error { + // verify that address isn't consortium member + applicantAddress, err := e.state.GetAddressStates(proposal.ApplicantAddress) + if err != nil { + return err + } + + if applicantAddress.Is(as.AddressStateConsortiumMember) { + return errConsortiumMember + } + + // verify that there is no existing add member proposal for this address + proposalsIterator, err := e.state.GetProposalIterator() + if err != nil { + return err + } + defer proposalsIterator.Release() + for proposalsIterator.Next() { + existingProposal, err := proposalsIterator.Value() + if err != nil { + return err + } + addMemberProposal, ok := existingProposal.(*dac.AddMemberProposalState) + if ok && addMemberProposal.ApplicantAddress == proposal.ApplicantAddress { + return errAlreadyActiveProposal + } + } + + if err := proposalsIterator.Error(); err != nil { + return err + } + + return nil +} + +func (e *proposalExecutor) AddMemberProposal(proposal *dac.AddMemberProposalState) error { + if accepted, _, _ := proposal.Result(); !accepted { + return nil + } + + addrState, err := e.state.GetAddressStates(proposal.ApplicantAddress) + if err != nil { + return err + } + newAddrState := addrState | as.AddressStateConsortiumMember + if newAddrState == addrState { + // c-member was somehow already added + // this should never happen, cause adding only done via addMemberProposal + // and only one addMemberProposal per applicant can exist at the same time + return nil + } + e.state.SetAddressStates(proposal.ApplicantAddress, newAddrState) + return nil +} + +func (*proposalBondTxIDsGetter) AddMemberProposal(*dac.AddMemberProposalState) ([]ids.ID, error) { + return nil, nil +} + +// ExcludeMemberProposal + +func (e *proposalVerifier) ExcludeMemberProposal(proposal *dac.ExcludeMemberProposal) error { + // verify that address is consortium member + memberAddressState, err := e.state.GetAddressStates(proposal.MemberAddress) + if err != nil { + return err + } + + if memberAddressState.IsNot(as.AddressStateConsortiumMember) { + return errNotConsortiumMember + } + + // verify that there is no existing exclude member proposal for this address + proposalsIterator, err := e.state.GetProposalIterator() + if err != nil { + return err + } + defer proposalsIterator.Release() + for proposalsIterator.Next() { + existingProposal, err := proposalsIterator.Value() + if err != nil { + return err + } + excludeMemberProposal, ok := existingProposal.(*dac.ExcludeMemberProposalState) + if ok && excludeMemberProposal.MemberAddress == proposal.MemberAddress { + return errAlreadyActiveProposal + } + } + + if err := proposalsIterator.Error(); err != nil { + return err + } + + return nil +} + +func (e *proposalExecutor) ExcludeMemberProposal(proposal *dac.ExcludeMemberProposalState) error { + if accepted, _, _ := proposal.Result(); !accepted { + return nil + } + + // set address state + addrState, err := e.state.GetAddressStates(proposal.MemberAddress) + if err != nil { + return err + } + newAddrState := addrState ^ as.AddressStateConsortiumMember + if newAddrState == addrState { + // c-member was somehow already excluded + // this should never happen, cause excluding only done via excludeMemberProposal + // and only one excludeMemberProposal per member can exist at the same time + return nil + } + e.state.SetAddressStates(proposal.MemberAddress, newAddrState) + + // get member nodeID + nodeShortID, err := e.state.GetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode) + switch { + case err == database.ErrNotFound: + // member doesn't have node, so we don't need to do anything with it + return nil + case err != nil: + return nil + } + nodeID := ids.NodeID(nodeShortID) + + // remove member node registration + e.state.SetShortIDLink(nodeShortID, state.ShortLinkKeyRegisterNode, nil) + e.state.SetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode, nil) + + // transfer validator to from current to deferred validator set + validator, err := e.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + switch { + case err == nil: + e.state.DeleteCurrentValidator(validator) + e.state.PutDeferredValidator(validator) + case err != database.ErrNotFound: + return err + } + + // remove pending validator + pendingValidator, err := e.state.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + switch { + case err == nil: + e.state.DeletePendingValidator(pendingValidator) + case err != database.ErrNotFound: + return err + } + + return nil +} + +func (e *proposalBondTxIDsGetter) ExcludeMemberProposal(proposal *dac.ExcludeMemberProposalState) ([]ids.ID, error) { + if accepted, _, _ := proposal.Result(); !accepted { + return nil, nil + } + + nodeShortID, err := e.state.GetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode) + switch { + case err == database.ErrNotFound: // member doesn't have node + return nil, nil + case err != nil: + return nil, err + } + nodeID := ids.NodeID(nodeShortID) + + pendingValidator, err := e.state.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + switch { + case err == database.ErrNotFound: // member doesn't have pending validator + return nil, nil + case err != nil: + return nil, err + } + + return []ids.ID{pendingValidator.TxID}, nil +} diff --git a/vms/platformvm/txs/dac/camino_dac_test.go b/vms/platformvm/txs/dac/camino_dac_test.go new file mode 100644 index 000000000000..3c79810e7e9c --- /dev/null +++ b/vms/platformvm/txs/dac/camino_dac_test.go @@ -0,0 +1,665 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "testing" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/components/avax" + as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" + "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) + + 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(as.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(as.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(as.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() + fx := defaultFx(true) + + 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) + + proposal, err := utx.Proposal() + require.NoError(t, err) + err = proposal.Visit(ProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalExecutorBaseFeeProposal(t *testing.T) { + 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() + fx := defaultFx(true) + err := tt.proposal.Visit(ProposalExecutor(tt.state(ctrl), fx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalVerifierAddMemberProposal(t *testing.T) { + ctx := defaultCtx(nil) + + feeOwnerKey, _, feeOwner := generateKeyAndOwner(t) + bondOwnerKey, _, bondOwner := generateKeyAndOwner(t) + proposerKey, proposerAddr, _ := generateKeyAndOwner(t) + applicantAddress := ids.ShortID{1} + + 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.AddMemberProposal{End: 1, ApplicantAddress: applicantAddress}} + 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 + }{ + "Applicant address is consortium member": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateConsortiumMember, 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: errConsortiumMember, + }, + "Already active AddMemberProposal for this applicant": { + 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.AddMemberProposalState{ApplicantAddress: applicantAddress}, nil) + proposalsIterator.EXPECT().Release() + + s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateEmpty, 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(applicantAddress).Return(as.AddressStateEmpty, 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() + fx := defaultFx(true) + + 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) + + proposal, err := utx.Proposal() + require.NoError(t, err) + err = proposal.Visit(ProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalExecutorAddMemberProposal(t *testing.T) { + applicantAddress := ids.ShortID{1} + applicantAddressState := as.AddressStateCaminoProposer // just not empty + + 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().GetAddressStates(applicantAddress).Return(applicantAddressState, nil) + s.EXPECT().SetAddressStates(applicantAddress, applicantAddressState|as.AddressStateConsortiumMember) + return s + }, + proposal: &dac.AddMemberProposalState{ + ApplicantAddress: applicantAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, + {Value: false, Weight: 1}, + }}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + fx := defaultFx(true) + err := tt.proposal.Visit(ProposalExecutor(tt.state(ctrl), fx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalVerifierExcludeMemberProposal(t *testing.T) { + ctx := defaultCtx(nil) + + feeOwnerKey, _, feeOwner := generateKeyAndOwner(t) + bondOwnerKey, _, bondOwner := generateKeyAndOwner(t) + proposerKey, proposerAddr, _ := generateKeyAndOwner(t) + memberAddress := ids.ShortID{1} + + 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.ExcludeMemberProposal{End: 1, MemberAddress: memberAddress}} + 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 + }{ + "Applicant address is not consortium member": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateEmpty, 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: errNotConsortiumMember, + }, + "Already active ExcludeMemberProposal for this member": { + 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.ExcludeMemberProposalState{MemberAddress: memberAddress}, nil) + proposalsIterator.EXPECT().Release() + + s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, 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(memberAddress).Return(as.AddressStateConsortiumMember, 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() + fx := defaultFx(true) + + 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) + + proposal, err := utx.Proposal() + require.NoError(t, err) + err = proposal.Visit(ProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalExecutorExcludeMemberProposal(t *testing.T) { + memberAddress := ids.ShortID{1} + memberAddressState := as.AddressStateCaminoProposer | as.AddressStateConsortiumMember // just not only c-member + memberNodeShortID := ids.ShortID{2} + memberNodeID := ids.NodeID(memberNodeShortID) + memberValidator := &state.Staker{TxID: ids.ID{3}} + + 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().GetAddressStates(memberAddress).Return(memberAddressState, nil) + s.EXPECT().SetAddressStates(memberAddress, memberAddressState^as.AddressStateConsortiumMember) + s.EXPECT().GetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) + s.EXPECT().DeleteCurrentValidator(memberValidator) + s.EXPECT().PutDeferredValidator(memberValidator) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) + s.EXPECT().DeletePendingValidator(memberValidator) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{ + Options: []dac.SimpleVoteOption[bool]{{Value: true, Weight: 1}, {Value: false}}, + }, + }, + }, + "OK: no pending validator": { + state: func(c *gomock.Controller) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(memberAddressState, nil) + s.EXPECT().SetAddressStates(memberAddress, memberAddressState^as.AddressStateConsortiumMember) + s.EXPECT().GetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) + s.EXPECT().DeleteCurrentValidator(memberValidator) + s.EXPECT().PutDeferredValidator(memberValidator) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, memberNodeID).Return(nil, database.ErrNotFound) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{ + Options: []dac.SimpleVoteOption[bool]{{Value: true, Weight: 1}, {Value: false}}, + }, + }, + }, + "OK: no current validator": { + state: func(c *gomock.Controller) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(memberAddressState, nil) + s.EXPECT().SetAddressStates(memberAddress, memberAddressState^as.AddressStateConsortiumMember) + s.EXPECT().GetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(nil, database.ErrNotFound) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) + s.EXPECT().DeletePendingValidator(memberValidator) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{ + Options: []dac.SimpleVoteOption[bool]{{Value: true, Weight: 1}, {Value: false}}, + }, + }, + }, + "OK: no validators": { + state: func(c *gomock.Controller) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(memberAddressState, nil) + s.EXPECT().SetAddressStates(memberAddress, memberAddressState^as.AddressStateConsortiumMember) + s.EXPECT().GetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(nil, database.ErrNotFound) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, memberNodeID).Return(nil, database.ErrNotFound) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{ + Options: []dac.SimpleVoteOption[bool]{{Value: true, Weight: 1}, {Value: false}}, + }, + }, + }, + "OK: no registered node": { + state: func(c *gomock.Controller) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(memberAddressState, nil) + s.EXPECT().SetAddressStates(memberAddress, memberAddressState^as.AddressStateConsortiumMember) + s.EXPECT().GetShortIDLink(memberAddress, state.ShortLinkKeyRegisterNode).Return(ids.ShortEmpty, database.ErrNotFound) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{ + Options: []dac.SimpleVoteOption[bool]{{Value: true, Weight: 1}, {Value: false}}, + }, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + fx := defaultFx(true) + err := tt.proposal.Visit(ProposalExecutor(tt.state(ctrl), fx)) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProposalBondTxIDsGetterExcludeMemberProposal(t *testing.T) { + memberAddress := ids.ShortID{1} + memberNodeShortID := ids.ShortID{2} + memberValidatorTxID := ids.ID{3} + + tests := map[string]struct { + state func(*gomock.Controller, *dac.ExcludeMemberProposalState) *state.MockDiff + proposal *dac.ExcludeMemberProposalState + expectedBondTxIDs []ids.ID + expectedErr error + }{ + "OK: not excluded": { + state: func(c *gomock.Controller, proposal *dac.ExcludeMemberProposalState) *state.MockDiff { + s := state.NewMockDiff(c) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true}, + {Value: false, Weight: 1}, + }}, + }, + }, + "OK": { + state: func(c *gomock.Controller, proposal *dac.ExcludeMemberProposalState) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID)). + Return(&state.Staker{TxID: memberValidatorTxID}, nil) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 1}, + {Value: false}, + }}, + }, + expectedBondTxIDs: []ids.ID{memberValidatorTxID}, + }, + "OK: no pending validator": { + state: func(c *gomock.Controller, proposal *dac.ExcludeMemberProposalState) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID)). + Return(nil, database.ErrNotFound) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 1}, + {Value: false}, + }}, + }, + }, + "OK: no registered node": { + state: func(c *gomock.Controller, proposal *dac.ExcludeMemberProposalState) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetShortIDLink(proposal.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(ids.ShortEmpty, database.ErrNotFound) + return s + }, + proposal: &dac.ExcludeMemberProposalState{ + MemberAddress: memberAddress, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 1}, + {Value: false}, + }}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + bondTxIDs, err := tt.proposal.GetBondTxIDs(ProposalBondTxIDsGetter(tt.state(ctrl, tt.proposal))) + require.ErrorIs(t, err, tt.expectedErr) + require.Equal(t, tt.expectedBondTxIDs, bondTxIDs) + }) + } +} diff --git a/vms/platformvm/txs/dac/camino_helpers_test.go b/vms/platformvm/txs/dac/camino_helpers_test.go new file mode 100644 index 000000000000..19a79d80ffbc --- /dev/null +++ b/vms/platformvm/txs/dac/camino_helpers_test.go @@ -0,0 +1,221 @@ +// Copyright (C) 2023, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package dac + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/prefixdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +var ( + defaultTxFee = uint64(100) + defaultMinStakingDuration = 24 * time.Hour + defaultGenesisTime = time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC) + defaultValidateStartTime = defaultGenesisTime + defaultValidateEndTime = defaultValidateStartTime.Add(10 * defaultMinStakingDuration) + xChainID = ids.Empty.Prefix(0) + cChainID = ids.Empty.Prefix(1) + avaxAssetID = ids.ID{'y', 'e', 'e', 't'} + + testKeyfactory secp256k1.Factory + + errMissing = errors.New("missing") +) + +type mutableSharedMemory struct { + atomic.SharedMemory +} + +func defaultCtx(db database.Database) *snow.Context { + ctx := snow.DefaultContextTest() + ctx.NetworkID = 10 + ctx.XChainID = xChainID + ctx.CChainID = cChainID + ctx.AVAXAssetID = avaxAssetID + + atomicDB := prefixdb.New([]byte{1}, db) + m := atomic.NewMemory(atomicDB) + + msm := &mutableSharedMemory{ + SharedMemory: m.NewSharedMemory(ctx.ChainID), + } + ctx.SharedMemory = msm + + ctx.ValidatorState = &validators.TestState{ + GetSubnetIDF: func(_ context.Context, chainID ids.ID) (ids.ID, error) { + subnetID, ok := map[ids.ID]ids.ID{ + constants.PlatformChainID: constants.PrimaryNetworkID, + xChainID: constants.PrimaryNetworkID, + cChainID: constants.PrimaryNetworkID, + }[chainID] + if !ok { + return ids.Empty, errMissing + } + return subnetID, nil + }, + } + + return ctx +} + +func defaultClock(postBanff bool) mockable.Clock { + now := defaultGenesisTime + if postBanff { + // 1 second after Banff fork + now = defaultValidateEndTime.Add(-2 * time.Second) + } + clk := mockable.Clock{} + clk.Set(now) + return clk +} + +type fxVMInt struct { + registry codec.Registry + clk *mockable.Clock + log logging.Logger +} + +func (fvi *fxVMInt) CodecRegistry() codec.Registry { + return fvi.registry +} + +func (fvi *fxVMInt) Clock() *mockable.Clock { + return fvi.clk +} + +func (fvi *fxVMInt) Logger() logging.Logger { + return fvi.log +} + +func defaultFx(isBootstrapped bool) fx.Fx { //nolint:unparam + clk := defaultClock(true) + fxVMInt := &fxVMInt{ + registry: linearcodec.NewDefault(), + clk: &clk, + log: snow.DefaultContextTest().Log, + } + res := &secp256k1fx.Fx{} + if err := res.Initialize(fxVMInt); err != nil { + panic(err) + } + if isBootstrapped { + if err := res.Bootstrapped(); err != nil { + panic(err) + } + } + return res +} + +func generateTestUTXO(txID ids.ID, assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID) *avax.UTXO { + 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 { + var out avax.TransferableOut = &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: outputOwners, + } + if depositTxID != ids.Empty || bondTxID != ids.Empty { + out = &locked.Out{ + IDs: locked.IDs{ + DepositTxID: depositTxID, + BondTxID: bondTxID, + }, + TransferableOut: out, + } + } + testUTXO := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: outIndex, + }, + Asset: avax.Asset{ID: assetID}, + Out: out, + } + if init { + testUTXO.InputID() + } + return testUTXO +} + +func generateTestOut(assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID) *avax.TransferableOutput { + var out avax.TransferableOut = &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: outputOwners, + } + if depositTxID != ids.Empty || bondTxID != ids.Empty { + out = &locked.Out{ + IDs: locked.IDs{ + DepositTxID: depositTxID, + BondTxID: bondTxID, + }, + TransferableOut: out, + } + } + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: assetID}, + Out: out, + } +} + +func generateTestInFromUTXO(utxo *avax.UTXO, sigIndices []uint32) *avax.TransferableInput { + var in avax.TransferableIn + switch out := utxo.Out.(type) { + case *secp256k1fx.TransferOutput: + in = &secp256k1fx.TransferInput{ + Amt: out.Amount(), + Input: secp256k1fx.Input{SigIndices: sigIndices}, + } + case *locked.Out: + in = &locked.In{ + IDs: out.IDs, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: out.Amount(), + Input: secp256k1fx.Input{SigIndices: sigIndices}, + }, + } + default: + panic("unknown utxo.Out type") + } + + // to be sure that utxoid.id is set in both entities + utxo.InputID() + return &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: in, + } +} + +func generateKeyAndOwner(t *testing.T) (*secp256k1.PrivateKey, ids.ShortID, secp256k1fx.OutputOwners) { + key, err := testKeyfactory.NewPrivateKey() + require.NoError(t, err) + addr := key.Address() + return key, addr, secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{addr}, + } +} diff --git a/vms/platformvm/txs/executor/camino_dac.go b/vms/platformvm/txs/executor/camino_dac.go deleted file mode 100644 index 1def30e7a658..000000000000 --- a/vms/platformvm/txs/executor/camino_dac.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (C) 2023, Chain4Travel AG. All rights reserved. -// See the file LICENSE for licensing terms. - -package executor - -import ( - "errors" - - as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" - "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) - - errConsortiumMember = errors.New("address is consortium member") - 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(as.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 -} - -func (e *proposalExecutor) BaseFeeProposal(proposal *dac.BaseFeeProposalState) error { - _, mostVotedOptionIndex, _ := proposal.GetMostVoted() - e.state.SetBaseFee(proposal.Options[mostVotedOptionIndex].Value) - return nil -} - -// AddMemberProposal - -func (e *proposalVerifier) AddMemberProposal(proposal *dac.AddMemberProposal) error { - // verify that address isn't consortium member - applicantAddress, err := e.state.GetAddressStates(proposal.ApplicantAddress) - if err != nil { - return err - } - - if applicantAddress.Is(as.AddressStateConsortiumMember) { - return errConsortiumMember - } - - // verify that there is no existing add member proposal for this address - proposalsIterator, err := e.state.GetProposalIterator() - if err != nil { - return err - } - defer proposalsIterator.Release() - for proposalsIterator.Next() { - existingProposal, err := proposalsIterator.Value() - if err != nil { - return err - } - addMemberProposal, ok := existingProposal.(*dac.AddMemberProposalState) - if ok && addMemberProposal.ApplicantAddress == proposal.ApplicantAddress { - return errAlreadyActiveProposal - } - } - - if err := proposalsIterator.Error(); err != nil { - return err - } - - return nil -} - -func (e *proposalExecutor) AddMemberProposal(proposal *dac.AddMemberProposalState) error { - if accepted, _, _ := proposal.Result(); accepted { - addrState, err := e.state.GetAddressStates(proposal.ApplicantAddress) - if err != nil { - return err - } - newAddrState := addrState | as.AddressStateConsortiumMember - if newAddrState == addrState { // c-member was already added via admin action after proposal creation - return nil - } - e.state.SetAddressStates(proposal.ApplicantAddress, newAddrState) - } - return nil -} diff --git a/vms/platformvm/txs/executor/camino_dac_test.go b/vms/platformvm/txs/executor/camino_dac_test.go deleted file mode 100644 index 137e38b90738..000000000000 --- a/vms/platformvm/txs/executor/camino_dac_test.go +++ /dev/null @@ -1,390 +0,0 @@ -// 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" - as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" - "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(as.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(as.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(as.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) - }) - } -} - -func TestProposalVerifierAddMemberProposal(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) - applicantAddress := ids.ShortID{1} - - 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.AddMemberProposal{End: 1, ApplicantAddress: applicantAddress}} - 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 - }{ - "Applicant address is consortium member": { - state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { - s := state.NewMockDiff(c) - s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateConsortiumMember, 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: errConsortiumMember, - }, - "Already active AddMemberProposal for this applicant": { - 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.AddMemberProposalState{ApplicantAddress: applicantAddress}, nil) - proposalsIterator.EXPECT().Release() - - s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateEmpty, 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(applicantAddress).Return(as.AddressStateEmpty, 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 TestProposalExecutorAddMemberProposal(t *testing.T) { - caminoGenesisConf := api.Camino{ - VerifyNodeSignature: true, - LockModeBondDeposit: true, - } - - applicantAddress := ids.ShortID{1} - applicantAddressState := as.AddressStateCaminoProposer // just not empty - - tests := map[string]struct { - state func(*gomock.Controller) *state.MockDiff - proposal dac.ProposalState - expectedErr error - }{ - "OK: rejected": { - state: state.NewMockDiff, - proposal: &dac.AddMemberProposalState{ - ApplicantAddress: applicantAddress, - SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ - {Value: true, Weight: 1}, - {Value: false, Weight: 2}, - }}, - }, - }, - "OK: accepted": { - state: func(c *gomock.Controller) *state.MockDiff { - s := state.NewMockDiff(c) - s.EXPECT().GetAddressStates(applicantAddress).Return(applicantAddressState, nil) - s.EXPECT().SetAddressStates(applicantAddress, applicantAddressState|as.AddressStateConsortiumMember) - return s - }, - proposal: &dac.AddMemberProposalState{ - ApplicantAddress: applicantAddress, - SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ - {Value: true, Weight: 2}, - {Value: false, 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 056e65d4c693..92e82b768927 100644 --- a/vms/platformvm/txs/executor/camino_helpers_test.go +++ b/vms/platformvm/txs/executor/camino_helpers_test.go @@ -736,6 +736,7 @@ func expectVerifyUnlockDeposit( expectGetMultisigAliases(t, s, addrs, aliases) } +// TODO @evlekht seems, that [addrs] actually not affecting anything and could be omitted func expectUnlock( t *testing.T, s *state.MockDiff, diff --git a/vms/platformvm/txs/executor/camino_tx_executor.go b/vms/platformvm/txs/executor/camino_tx_executor.go index 5a18bfe88c5a..2b7040c83094 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor.go +++ b/vms/platformvm/txs/executor/camino_tx_executor.go @@ -19,11 +19,12 @@ import ( "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/components/verify" as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" - dac "github.com/ava-labs/avalanchego/vms/platformvm/dac" + dacProposals "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/treasury" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/dac" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "golang.org/x/exp/slices" @@ -100,6 +101,8 @@ var ( errEarlyFinishedProposalsMismatch = errors.New("early proposals mismatch") errExpiredProposalsMismatch = errors.New("expired proposals mismatch") errWrongAdminProposal = errors.New("this type of proposal can't be admin-proposal") + errNotPermittedToCreateProposal = errors.New("don't have permission to create proposal of this type") + errInvalidProposal = errors.New("proposal is semantically invalid") ) type CaminoStandardTxExecutor struct { @@ -1721,7 +1724,7 @@ func (e *CaminoStandardTxExecutor) AddProposalTx(tx *txs.AddProposalTx) error { return err } - adminProposal, isAdminProposal := txProposal.(*dac.AdminProposal) + adminProposal, isAdminProposal := txProposal.(*dacProposals.AdminProposal) adminProposerAddressState := txProposal.AdminProposer() // verify proposal and proposer credential @@ -1762,8 +1765,8 @@ func (e *CaminoStandardTxExecutor) AddProposalTx(tx *txs.AddProposalTx) error { return fmt.Errorf("%w: %s", errProposerCredentialMismatch, err) } - if err := txProposal.Visit(e.proposalVerifier(tx)); err != nil { - return err + if err := txProposal.Visit(dac.ProposalVerifier(e.State, e.Fx, e.Tx, tx)); err != nil { + return fmt.Errorf("%w: %s", errInvalidProposal, err) } // verify the flowcheck @@ -1792,7 +1795,7 @@ func (e *CaminoStandardTxExecutor) AddProposalTx(tx *txs.AddProposalTx) error { txID := e.Tx.ID() allowedVoters := []ids.ShortID{} - var proposalState dac.ProposalState + var proposalState dacProposals.ProposalState if isAdminProposal { // currently we only have addMember and excludeMember proposals, and their option 0 is "success" option @@ -1992,7 +1995,29 @@ func (e *CaminoStandardTxExecutor) FinishProposalsTx(tx *txs.FinishProposalsTx) // verify ins and outs - expectedIns, expectedOuts, err := e.FlowChecker.Unlock(e.State, tx.ProposalIDs(), locked.StateBonded) + bondTxIDsGetter := dac.ProposalBondTxIDsGetter(e.State) + + proposalIDs := append(tx.EarlyFinishedSuccessfulProposalIDs, tx.ExpiredSuccessfulProposalIDs...) //nolint:gocritic + additionalLockTxIDs := []ids.ID{} + for _, proposalID := range proposalIDs { + proposal, err := e.State.GetProposal(proposalID) + if err != nil { + return err + } + lockTxIDs, err := proposal.GetBondTxIDs(bondTxIDsGetter) + if err != nil { + return err + } + additionalLockTxIDs = append(additionalLockTxIDs, lockTxIDs...) + } + + proposalIDs = append(proposalIDs, tx.EarlyFinishedFailedProposalIDs...) + proposalIDs = append(proposalIDs, tx.ExpiredFailedProposalIDs...) + expectedIns, expectedOuts, err := e.FlowChecker.Unlock( + e.State, + append(proposalIDs, additionalLockTxIDs...), + locked.StateBonded, + ) if err != nil { return err } @@ -2042,7 +2067,7 @@ func (e *CaminoStandardTxExecutor) FinishProposalsTx(tx *txs.FinishProposalsTx) } // try to execute proposal - if err := proposal.Visit(e.proposalExecutor()); err != nil { + if err := proposal.Visit(dac.ProposalExecutor(e.State, e.Fx)); err != nil { return err } @@ -2093,7 +2118,7 @@ func (e *CaminoStandardTxExecutor) FinishProposalsTx(tx *txs.FinishProposalsTx) } // try to execute proposal - if err := proposal.Visit(e.proposalExecutor()); err != nil { + if err := proposal.Visit(dac.ProposalExecutor(e.State, e.Fx)); err != nil { return err } diff --git a/vms/platformvm/txs/executor/camino_tx_executor_test.go b/vms/platformvm/txs/executor/camino_tx_executor_test.go index 3178784ec32b..69ae2f604e01 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor_test.go +++ b/vms/platformvm/txs/executor/camino_tx_executor_test.go @@ -4937,7 +4937,179 @@ func TestCaminoStandardTxExecutorRewardsImportTx(t *testing.T) { } } -func TestCaminoStandardTxExecutorSuspendValidator(t *testing.T) { +func TestCaminoStandardTxExecutorAddressStateTxSuspendValidator(t *testing.T) { + // finding first staker to remove + env := newCaminoEnvironment( /*postBanff*/ true, false, api.Camino{LockModeBondDeposit: true}) + stakerIterator, err := env.state.GetCurrentStakerIterator() + require.NoError(t, err) + require.True(t, stakerIterator.Next()) + stakerToRemove := stakerIterator.Value() + stakerIterator.Release() + nodeID := stakerToRemove.NodeID + consortiumMemberAddress, err := env.state.GetShortIDLink(ids.ShortID(nodeID), state.ShortLinkKeyRegisterNode) + require.NoError(t, err) + require.NoError(t, shutdownCaminoEnvironment(env)) + + initAdmin := caminoPreFundedKeys[0] + // other common data + + outputOwners := &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{consortiumMemberAddress}, + } + + type args struct { + address ids.ShortID + remove bool + keys []*secp256k1.PrivateKey + changeAddr *secp256k1fx.OutputOwners + } + tests := map[string]struct { + generateArgs func() args + preExecute func(*testing.T, *txs.Tx, state.State) + expectedErr error + assert func(*testing.T) + }{ + "Set state to deferred from address with no roles -> ErrAddrStateNotPermitted": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + keys: []*secp256k1.PrivateKey{caminoPreFundedKeys[1]}, // non-admin address + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) {}, + expectedErr: errAddrStateNotPermitted, + }, + "Set state to deferred from kyc address -> ErrAddrStateNotPermitted": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + keys: []*secp256k1.PrivateKey{caminoPreFundedKeys[1]}, + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) { + state.SetAddressStates(caminoPreFundedKeys[1].Address(), as.AddressStateRoleKYC) + }, + expectedErr: errAddrStateNotPermitted, + }, + "Happy path set state to deferred": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + keys: []*secp256k1.PrivateKey{initAdmin}, + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) {}, + expectedErr: nil, + }, + "Remove deferred state from non-admin address -> ErrAddrStateNotPermitted": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + remove: true, + keys: []*secp256k1.PrivateKey{caminoPreFundedKeys[1]}, // non-admin address + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) {}, + expectedErr: errAddrStateNotPermitted, + }, + "Happy path set state to active": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + remove: true, + keys: []*secp256k1.PrivateKey{initAdmin}, + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) { + stakerToTransfer, err := state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(t, err) + state.DeleteCurrentValidator(stakerToTransfer) + state.PutDeferredValidator(stakerToTransfer) + }, + expectedErr: nil, + }, + "Remove deferred state of an active validator": { + generateArgs: func() args { + return args{ + address: consortiumMemberAddress, + remove: true, + keys: []*secp256k1.PrivateKey{initAdmin}, + changeAddr: outputOwners, + } + }, + preExecute: func(t *testing.T, tx *txs.Tx, state state.State) {}, + expectedErr: errValidatorNotFound, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + caminoGenesisConf := api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + InitialAdmin: initAdmin.Address(), + } + env := newCaminoEnvironment( /*postBanff*/ true, false, caminoGenesisConf) + env.ctx.Lock.Lock() + defer func() { + if err := shutdownCaminoEnvironment(env); err != nil { + t.Fatal(err) + } + }() + + env.config.BanffTime = env.state.GetTimestamp() + require.NoError(t, env.state.Commit()) + + setAddressStateArgs := tt.generateArgs() + tx, err := env.txBuilder.NewAddressStateTx( + setAddressStateArgs.address, + setAddressStateArgs.remove, + as.AddressStateBitNodeDeferred, + setAddressStateArgs.keys, + setAddressStateArgs.changeAddr, + ) + require.NoError(t, err) + + tt.preExecute(t, tx, env.state) + onAcceptState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(t, err) + + executor := CaminoStandardTxExecutor{ + StandardTxExecutor{ + Backend: &env.backend, + State: onAcceptState, + Tx: tx, + }, + } + + err = tx.Unsigned.Visit(&executor) + require.ErrorIs(t, err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + var stakerIterator state.StakerIterator + if setAddressStateArgs.remove { + stakerIterator, err = onAcceptState.GetCurrentStakerIterator() + require.NoError(t, err) + } else { + stakerIterator, err = onAcceptState.GetDeferredStakerIterator() + require.NoError(t, err) + } + require.True(t, stakerIterator.Next()) + stakerToRemove := stakerIterator.Value() + stakerIterator.Release() + require.Equal(t, stakerToRemove.NodeID, nodeID) + }) + } +} + +func TestCaminoStandardTxExecutor(t *testing.T) { // finding first staker to remove env := newCaminoEnvironment( /*postBanff*/ true, false, api.Camino{LockModeBondDeposit: true}) stakerIterator, err := env.state.GetCurrentStakerIterator() @@ -6111,7 +6283,7 @@ func TestCaminoStandardTxExecutorAddProposalTx(t *testing.T) { signers: [][]*secp256k1.PrivateKey{ {feeOwnerKey}, {bondOwnerKey}, {proposerKey}, }, - expectedErr: errNotPermittedToCreateProposal, + expectedErr: errInvalidProposal, }, "OK": { state: func(t *testing.T, c *gomock.Controller, utx *txs.AddProposalTx, txID ids.ID, cfg *config.Config) *state.MockDiff { @@ -6685,52 +6857,75 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { LockModeBondDeposit: caminoGenesisConf.LockModeBondDeposit, } + proposalBondAmt := uint64(100) + 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) + bondOwnerAddr7 := ids.ShortID{7} + bondOwnerAddr8 := ids.ShortID{8} + bondOwnerAddr9 := ids.ShortID{9} + bondOwnerAddr10 := ids.ShortID{10} + voterAddr1 := ids.ShortID{11} + voterAddr2 := ids.ShortID{12} + memberNodeShortID1 := ids.ShortID{13} + memberNodeShortID2 := ids.ShortID{14} + bondOwner1 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr1}, Threshold: 1} + bondOwner2 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr2}, Threshold: 1} + bondOwner3 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr3}, Threshold: 1} + bondOwner4 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr4}, Threshold: 1} + bondOwner5 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr5}, Threshold: 1} + bondOwner6 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr6}, Threshold: 1} + bondOwner7 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr7}, Threshold: 1} + bondOwner8 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr8}, Threshold: 1} + bondOwner9 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr9}, Threshold: 1} + bondOwner10 := secp256k1fx.OutputOwners{Addrs: []ids.ShortID{bondOwnerAddr10}, Threshold: 1} + + earlySuccessfulProposalID := ids.ID{1, 1} + earlyFailedProposalID := ids.ID{2, 2} + expiredSuccessfulProposalID := ids.ID{3, 3} + expiredFailedProposalID := ids.ID{4, 4} + activeSuccessfulProposalID := ids.ID{5, 5} + activeFailedProposalID := ids.ID{6, 6} + earlySuccessfulProposalWithBondID := ids.ID{7, 7} + expiredSuccessfulProposalWithBondID := ids.ID{8, 8} + validatorTxID1 := ids.ID{9, 9} + validatorTxID2 := ids.ID{10, 10} + + earlySuccessfulProposalUTXO := generateTestUTXOWithIndex(earlySuccessfulProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner1, ids.Empty, earlySuccessfulProposalID, true) + earlyFailedProposalUTXO := generateTestUTXOWithIndex(earlyFailedProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner2, ids.Empty, earlyFailedProposalID, true) + expiredSuccessfulProposalUTXO := generateTestUTXOWithIndex(expiredSuccessfulProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner3, ids.Empty, expiredSuccessfulProposalID, true) + expiredFailedProposalUTXO := generateTestUTXOWithIndex(expiredFailedProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner4, ids.Empty, expiredFailedProposalID, true) + activeSuccessfulProposalUTXO := generateTestUTXOWithIndex(activeSuccessfulProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner5, ids.Empty, activeSuccessfulProposalID, true) + activeFailedProposalUTXO := generateTestUTXOWithIndex(activeFailedProposalID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner6, ids.Empty, activeFailedProposalID, true) + earlySuccessfulProposalWithBondUTXO := generateTestUTXOWithIndex(earlySuccessfulProposalWithBondID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner7, ids.Empty, earlySuccessfulProposalWithBondID, true) + expiredSuccessfulProposalWithBondUTXO := generateTestUTXOWithIndex(expiredSuccessfulProposalWithBondID, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner8, ids.Empty, expiredSuccessfulProposalWithBondID, true) + additionalBondUTXO1 := generateTestUTXOWithIndex(validatorTxID1, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner9, ids.Empty, validatorTxID1, true) + additionalBondUTXO2 := generateTestUTXOWithIndex(validatorTxID2, 0, ctx.AVAXAssetID, proposalBondAmt, bondOwner10, ids.Empty, validatorTxID2, true) + + pendingValidator1 := &state.Staker{TxID: validatorTxID1} + pendingValidator2 := &state.Staker{TxID: validatorTxID2} baseTx := txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, failedEarlyFinishedProposalUTXO, - successfulExpiredProposalUTXO, failedExpiredProposalUTXO, + earlySuccessfulProposalUTXO, earlyFailedProposalUTXO, + expiredSuccessfulProposalUTXO, expiredFailedProposalUTXO, }, []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), + generateTestOutFromUTXO(earlySuccessfulProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlyFailedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredFailedProposalUTXO, ids.Empty, ids.Empty), }, }} mostVotedIndex := uint32(1) - successfulEarlyFinishedProposal := &dac.BaseFeeProposalState{ + earlySuccessfulProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{voterAddr1}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6741,7 +6936,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 3, } - failedEarlyFinishedProposal := &dac.BaseFeeProposalState{ + earlyFailedProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6752,7 +6947,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 3, } - successfulExpiredProposal := &dac.BaseFeeProposalState{ + expiredSuccessfulProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{voterAddr1}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6763,7 +6958,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 4, } - failedExpiredProposal := &dac.BaseFeeProposalState{ + expiredFailedProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6774,7 +6969,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 4, } - successfulActiveProposal := &dac.BaseFeeProposalState{ + activeSuccessfulProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{voterAddr1}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6785,7 +6980,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 4, } - failedActiveProposal := &dac.BaseFeeProposalState{ + activeFailedProposal := &dac.BaseFeeProposalState{ AllowedVoters: []ids.ShortID{voterAddr1, voterAddr2}, Start: 100, End: 102, SimpleVoteOptions: dac.SimpleVoteOptions[uint64]{Options: []dac.SimpleVoteOption[uint64]{ @@ -6796,6 +6991,28 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { TotalAllowedVoters: 4, } + earlySuccessfulProposalWithBond := &dac.ExcludeMemberProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, + {Value: false}, + }}, + TotalAllowedVoters: 3, + MemberAddress: bondOwnerAddr6, + } + + expiredSuccessfulProposalWithBond := &dac.ExcludeMemberProposalState{ + AllowedVoters: []ids.ShortID{voterAddr1}, + Start: 100, End: 102, + SimpleVoteOptions: dac.SimpleVoteOptions[bool]{Options: []dac.SimpleVoteOption[bool]{ + {Value: true, Weight: 2}, + {Value: false, Weight: 1}, + }}, + TotalAllowedVoters: 4, + MemberAddress: bondOwnerAddr5, + } + tests := map[string]struct { state func(*testing.T, *gomock.Controller, *txs.FinishProposalsTx, ids.ID, *config.Config) *state.MockDiff utx func(*config.Config) *txs.FinishProposalsTx @@ -6814,10 +7031,10 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredFailedProposalID}, } }, expectedErr: errNotBerlinPhase, @@ -6834,10 +7051,10 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredFailedProposalID}, } }, signers: [][]*secp256k1.PrivateKey{{}}, @@ -6849,7 +7066,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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) + Return([]ids.ID{activeSuccessfulProposalID}, cfg.BerlinPhaseTime.Add(time.Second), nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) return s }, @@ -6859,13 +7076,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulActiveProposalUTXO, + activeSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(activeSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{activeSuccessfulProposalID}, } }, expectedErr: errProposalsAreNotExpiredYet, @@ -6876,7 +7093,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{successfulExpiredProposalID, failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredSuccessfulProposalID, expiredFailedProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) return s }, @@ -6886,13 +7103,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulExpiredProposalUTXO, + expiredSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, } }, expectedErr: errExpiredProposalsMismatch, @@ -6904,7 +7121,7 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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([]ids.ID{earlySuccessfulProposalID, earlyFailedProposalID}, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -6913,13 +7130,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulExpiredProposalUTXO, + expiredSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, } }, expectedErr: errEarlyFinishedProposalsMismatch, @@ -6930,13 +7147,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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 + Return([]ids.ID{expiredFailedProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{earlyFailedProposalID}, nil) + lockTxIDs := append(utx.EarlyFinishedFailedProposalIDs, utx.ExpiredFailedProposalIDs...) //nolint:gocritic expectUnlock(t, s, lockTxIDs, []ids.ShortID{ - bondOwnerAddr1, bondOwnerAddr3, + bondOwnerAddr2, bondOwnerAddr4, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + earlyFailedProposalUTXO, expiredFailedProposalUTXO, }, locked.StateBonded) return s }, @@ -6946,32 +7163,76 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: []*avax.TransferableInput{ // missing 2nd input - generateTestInFromUTXO(successfulEarlyFinishedProposalUTXO, []uint32{}), + generateTestInFromUTXO(earlyFailedProposalUTXO, []uint32{}), }, Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlyFailedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredFailedProposalID}, } }, expectedErr: errInvalidSystemTxBody, }, - "Invalid outs": { + "Invalid inputs: additional bond from proposal (e.g. excluded member pending validator)": { + 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) + s.EXPECT().GetProposal(earlySuccessfulProposalWithBondID).Return(earlySuccessfulProposalWithBond, nil) + + // * proposalBondTxIDsGetter + s.EXPECT().GetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID1, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(pendingValidator1, nil) + // * + + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, validatorTxID1) //nolint:gocritic + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr7, bondOwnerAddr9, + }, []*avax.UTXO{ + earlySuccessfulProposalWithBondUTXO, additionalBondUTXO1, + }, 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{ + generateTestInFromUTXO(earlySuccessfulProposalWithBondUTXO, []uint32{}), + // generateTestInFromUTXO(additionalBondUTXO1, []uint32{}), // missing pending validator bond + }, + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(earlySuccessfulProposalWithBondUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(additionalBondUTXO1, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalWithBondID}, + } + }, + expectedErr: errInvalidSystemTxBody, + }, + "Invalid outputs": { 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 + Return([]ids.ID{expiredFailedProposalID}, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{earlyFailedProposalID}, nil) + lockTxIDs := append(utx.EarlyFinishedFailedProposalIDs, utx.ExpiredFailedProposalIDs...) //nolint:gocritic expectUnlock(t, s, lockTxIDs, []ids.ShortID{ - bondOwnerAddr1, bondOwnerAddr3, + bondOwnerAddr2, bondOwnerAddr4, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + earlyFailedProposalUTXO, expiredFailedProposalUTXO, }, locked.StateBonded) return s }, @@ -6981,32 +7242,120 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + earlyFailedProposalUTXO, expiredFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), - generateTestOut(ctx.AVAXAssetID, proposalBondAmt, bond1Owner, ids.Empty, ids.Empty), // successfulExpiredProposalUTXO with different owner + generateTestOutFromUTXO(earlySuccessfulProposalUTXO, ids.Empty, ids.Empty), + generateTestOut(ctx.AVAXAssetID, proposalBondAmt, bondOwner1, ids.Empty, ids.Empty), // expiredFailedProposalUTXO with different owner }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredFailedProposalID}, } }, expectedErr: errInvalidSystemTxBody, }, - "Proposal not exist": { + "Invalid outputs: additional bond from proposal (e.g. excluded member pending validator)": { + 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) + s.EXPECT().GetProposal(earlySuccessfulProposalWithBondID).Return(earlySuccessfulProposalWithBond, nil) + + // * proposalBondTxIDsGetter + s.EXPECT().GetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID1, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(pendingValidator1, nil) + // * + + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, validatorTxID1) //nolint:gocritic + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr7, bondOwnerAddr9, + }, []*avax.UTXO{ + earlySuccessfulProposalWithBondUTXO, additionalBondUTXO1, + }, 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{ + generateTestInFromUTXO(earlySuccessfulProposalWithBondUTXO, []uint32{}), + generateTestInFromUTXO(additionalBondUTXO1, []uint32{}), + }, + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(earlySuccessfulProposalWithBondUTXO, ids.Empty, ids.Empty), + // generateTestOutFromUTXO(additionalBondUTXO1, ids.Empty, ids.Empty), // missing pending validator bond + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalWithBondID}, + } + }, + expectedErr: errInvalidSystemTxBody, + }, + "Invalid inputs/outputs: additional bond from proposal (e.g. excluded member pending validator)": { 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) + s.EXPECT().GetProposal(earlySuccessfulProposalWithBondID).Return(earlySuccessfulProposalWithBond, nil) + + // * proposalBondTxIDsGetter + s.EXPECT().GetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID1, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(pendingValidator1, nil) + // * + + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, validatorTxID1) //nolint:gocritic + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr7, bondOwnerAddr9, + }, []*avax.UTXO{ + earlySuccessfulProposalWithBondUTXO, additionalBondUTXO1, + }, 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{ + generateTestInFromUTXO(earlySuccessfulProposalWithBondUTXO, []uint32{}), + // generateTestInFromUTXO(additionalBondUTXO1, []uint32{}), // missing pending validator bond + }, + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(earlySuccessfulProposalWithBondUTXO, ids.Empty, ids.Empty), + // generateTestOutFromUTXO(additionalBondUTXO1, ids.Empty, ids.Empty), // missing pending validator bond + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalWithBondID}, + } + }, + 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.EarlyFinishedFailedProposalIDs, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ - bondOwnerAddr1, + bondOwnerAddr2, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, + earlyFailedProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulEarlyFinishedProposalID).Return(nil, database.ErrNotFound) + s.EXPECT().GetProposal(earlyFailedProposalID).Return(nil, database.ErrNotFound) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7015,13 +7364,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, + earlyFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlyFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, } }, expectedErr: database.ErrNotFound, @@ -7032,15 +7381,16 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredSuccessfulProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish(). - Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + Return([]ids.ID{earlySuccessfulProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr1, bondOwnerAddr3, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + earlySuccessfulProposalUTXO, expiredSuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + s.EXPECT().GetProposal(expiredSuccessfulProposalID).Return(expiredSuccessfulProposal, nil).Times(2) + s.EXPECT().GetProposal(earlySuccessfulProposalID).Return(earlySuccessfulProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7049,16 +7399,16 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, - successfulExpiredProposalUTXO, + earlySuccessfulProposalUTXO, + expiredSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlySuccessfulProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, } }, expectedErr: errNotEarlyFinishedProposal, @@ -7069,15 +7419,16 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredSuccessfulProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish(). - Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + Return([]ids.ID{earlySuccessfulProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr1, bondOwnerAddr3, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, successfulExpiredProposalUTXO, + earlySuccessfulProposalUTXO, expiredSuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + s.EXPECT().GetProposal(expiredSuccessfulProposalID).Return(expiredSuccessfulProposal, nil).Times(2) + s.EXPECT().GetProposal(earlySuccessfulProposalID).Return(earlySuccessfulProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7086,16 +7437,16 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, - successfulExpiredProposalUTXO, + earlySuccessfulProposalUTXO, + expiredSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlySuccessfulProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulExpiredProposalID}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, } }, expectedErr: errNotEarlyFinishedProposal, @@ -7107,13 +7458,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish(). - Return([]ids.ID{successfulEarlyFinishedProposalID}, nil) + Return([]ids.ID{earlySuccessfulProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr5, }, []*avax.UTXO{ - successfulActiveProposalUTXO, + activeSuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulActiveProposalID).Return(successfulActiveProposal, nil) + s.EXPECT().GetProposal(activeSuccessfulProposalID).Return(activeSuccessfulProposal, nil).Times(2) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7122,13 +7473,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulActiveProposalUTXO, + activeSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(activeSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{activeSuccessfulProposalID}, } }, expectedErr: errNotEarlyFinishedProposal, @@ -7140,13 +7491,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil).Return([]ids.ID{}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish(). - Return([]ids.ID{failedEarlyFinishedProposalID}, nil) + Return([]ids.ID{earlyFailedProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr6, }, []*avax.UTXO{ - failedActiveProposalUTXO, + activeFailedProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(failedActiveProposalID).Return(failedActiveProposal, nil) + s.EXPECT().GetProposal(activeFailedProposalID).Return(activeFailedProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7155,13 +7506,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - failedActiveProposalUTXO, + activeFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(failedActiveProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(activeFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedFailedProposalIDs: []ids.ID{failedActiveProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{activeFailedProposalID}, } }, expectedErr: errNotEarlyFinishedProposal, @@ -7172,14 +7523,14 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredSuccessfulProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr5, }, []*avax.UTXO{ - successfulActiveProposalUTXO, + activeSuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulActiveProposalID).Return(successfulActiveProposal, nil) + s.EXPECT().GetProposal(activeSuccessfulProposalID).Return(activeSuccessfulProposal, nil).Times(2) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7188,13 +7539,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulActiveProposalUTXO, + activeSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulActiveProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(activeSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredSuccessfulProposalIDs: []ids.ID{successfulActiveProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{activeSuccessfulProposalID}, } }, expectedErr: errNotExpiredProposal, @@ -7205,14 +7556,14 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredFailedProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr6, }, []*avax.UTXO{ - failedActiveProposalUTXO, + activeFailedProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(failedActiveProposalID).Return(failedActiveProposal, nil) + s.EXPECT().GetProposal(activeFailedProposalID).Return(activeFailedProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7221,13 +7572,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - failedActiveProposalUTXO, + activeFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(failedActiveProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(activeFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredFailedProposalIDs: []ids.ID{failedActiveProposalID}, + ExpiredFailedProposalIDs: []ids.ID{activeFailedProposalID}, } }, expectedErr: errNotExpiredProposal, @@ -7238,15 +7589,15 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{failedExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredFailedProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr4, }, []*avax.UTXO{ - failedExpiredProposalUTXO, + expiredFailedProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(failedExpiredProposalID).Return(failedExpiredProposal, nil) + s.EXPECT().GetProposal(expiredFailedProposalID).Return(expiredFailedProposal, nil).Times(2) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7255,13 +7606,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - failedExpiredProposalUTXO, + expiredFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(failedExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredSuccessfulProposalIDs: []ids.ID{failedExpiredProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredFailedProposalID}, } }, expectedErr: errNotSuccessfulProposal, @@ -7272,14 +7623,14 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{earlyFailedProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr2, }, []*avax.UTXO{ - failedEarlyFinishedProposalUTXO, + earlyFailedProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(failedEarlyFinishedProposalID).Return(failedEarlyFinishedProposal, nil) + s.EXPECT().GetProposal(earlyFailedProposalID).Return(earlyFailedProposal, nil).Times(2) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7288,13 +7639,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - failedEarlyFinishedProposalUTXO, + earlyFailedProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(failedEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlyFailedProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedSuccessfulProposalIDs: []ids.ID{failedEarlyFinishedProposalID}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlyFailedProposalID}, } }, expectedErr: errNotSuccessfulProposal, @@ -7305,15 +7656,15 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { s.EXPECT().CaminoConfig().Return(caminoStateConf, nil) s.EXPECT().GetTimestamp().Return(cfg.BerlinPhaseTime) s.EXPECT().GetNextToExpireProposalIDsAndTime(nil). - Return([]ids.ID{successfulExpiredProposalID}, cfg.BerlinPhaseTime, nil) + Return([]ids.ID{expiredSuccessfulProposalID}, cfg.BerlinPhaseTime, nil) s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr3, }, []*avax.UTXO{ - successfulExpiredProposalUTXO, + expiredSuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) + s.EXPECT().GetProposal(expiredSuccessfulProposalID).Return(expiredSuccessfulProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7322,13 +7673,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulExpiredProposalUTXO, + expiredSuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulExpiredProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - ExpiredFailedProposalIDs: []ids.ID{successfulExpiredProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredSuccessfulProposalID}, } }, expectedErr: errSuccessfulProposal, @@ -7339,14 +7690,14 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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) + s.EXPECT().GetProposalIDsToFinish().Return([]ids.ID{earlySuccessfulProposalID}, nil) expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr1, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, + earlySuccessfulProposalUTXO, }, locked.StateBonded) - s.EXPECT().GetProposal(successfulEarlyFinishedProposalID).Return(successfulEarlyFinishedProposal, nil) + s.EXPECT().GetProposal(earlySuccessfulProposalID).Return(earlySuccessfulProposal, nil) return s }, utx: func(cfg *config.Config) *txs.FinishProposalsTx { @@ -7355,13 +7706,13 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { NetworkID: ctx.NetworkID, BlockchainID: ctx.ChainID, Ins: generateInsFromUTXOsWithSigIndices([]*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, + earlySuccessfulProposalUTXO, }, []uint32{}), Outs: []*avax.TransferableOutput{ - generateTestOutFromUTXO(successfulEarlyFinishedProposalUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(earlySuccessfulProposalUTXO, ids.Empty, ids.Empty), }, }}, - EarlyFinishedFailedProposalIDs: []ids.ID{successfulEarlyFinishedProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlySuccessfulProposalID}, } }, expectedErr: errSuccessfulProposal, @@ -7377,25 +7728,25 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { expectUnlock(t, s, utx.ProposalIDs(), []ids.ShortID{ bondOwnerAddr1, bondOwnerAddr2, bondOwnerAddr3, bondOwnerAddr4, }, []*avax.UTXO{ - successfulEarlyFinishedProposalUTXO, failedEarlyFinishedProposalUTXO, - successfulExpiredProposalUTXO, failedExpiredProposalUTXO, + earlySuccessfulProposalUTXO, earlyFailedProposalUTXO, + expiredSuccessfulProposalUTXO, expiredFailedProposalUTXO, }, 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(earlySuccessfulProposalID).Return(earlySuccessfulProposal, nil).Times(2) + s.EXPECT().SetBaseFee(earlySuccessfulProposal.Options[mostVotedIndex].Value) // proposal executor + s.EXPECT().RemoveProposal(earlySuccessfulProposalID, earlySuccessfulProposal) + s.EXPECT().RemoveProposalIDToFinish(earlySuccessfulProposalID) - s.EXPECT().GetProposal(failedEarlyFinishedProposalID).Return(failedEarlyFinishedProposal, nil) - s.EXPECT().RemoveProposal(failedEarlyFinishedProposalID, failedEarlyFinishedProposal) - s.EXPECT().RemoveProposalIDToFinish(failedEarlyFinishedProposalID) + s.EXPECT().GetProposal(earlyFailedProposalID).Return(earlyFailedProposal, nil) + s.EXPECT().RemoveProposal(earlyFailedProposalID, earlyFailedProposal) + s.EXPECT().RemoveProposalIDToFinish(earlyFailedProposalID) - s.EXPECT().GetProposal(successfulExpiredProposalID).Return(successfulExpiredProposal, nil) - s.EXPECT().SetBaseFee(successfulExpiredProposal.Options[mostVotedIndex].Value) // proposal executor - s.EXPECT().RemoveProposal(successfulExpiredProposalID, successfulExpiredProposal) + s.EXPECT().GetProposal(expiredSuccessfulProposalID).Return(expiredSuccessfulProposal, nil).Times(2) + s.EXPECT().SetBaseFee(expiredSuccessfulProposal.Options[mostVotedIndex].Value) // proposal executor + s.EXPECT().RemoveProposal(expiredSuccessfulProposalID, expiredSuccessfulProposal) - s.EXPECT().GetProposal(failedExpiredProposalID).Return(failedExpiredProposal, nil) - s.EXPECT().RemoveProposal(failedExpiredProposalID, failedExpiredProposal) + s.EXPECT().GetProposal(expiredFailedProposalID).Return(expiredFailedProposal, nil) + s.EXPECT().RemoveProposal(expiredFailedProposalID, expiredFailedProposal) expectConsumeUTXOs(t, s, utx.Ins) expectProduceUTXOs(t, s, utx.Outs, txID, 0) @@ -7404,10 +7755,106 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { 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}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalID}, + EarlyFinishedFailedProposalIDs: []ids.ID{earlyFailedProposalID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalID}, + ExpiredFailedProposalIDs: []ids.ID{expiredFailedProposalID}, + } + }, + }, + "OK: additional bond from proposals (e.g. excluded member pending validator)": { + 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(utx.ExpiredSuccessfulProposalIDs, cfg.BerlinPhaseTime, nil) + s.EXPECT().GetProposalIDsToFinish(). + Return(utx.EarlyFinishedSuccessfulProposalIDs, nil) + s.EXPECT().GetProposal(earlySuccessfulProposalWithBondID).Return(earlySuccessfulProposalWithBond, nil) + s.EXPECT().GetProposal(expiredSuccessfulProposalWithBondID).Return(expiredSuccessfulProposalWithBond, nil) + + // * proposalBondTxIDsGetter + s.EXPECT().GetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID1, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(pendingValidator1, nil) + + s.EXPECT().GetShortIDLink(expiredSuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID2, nil) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID2)). + Return(pendingValidator2, nil) + // * + + lockTxIDs := append(utx.EarlyFinishedSuccessfulProposalIDs, utx.ExpiredSuccessfulProposalIDs...) //nolint:gocritic + lockTxIDs = append(lockTxIDs, validatorTxID1, validatorTxID2) + expectUnlock(t, s, lockTxIDs, []ids.ShortID{ + bondOwnerAddr7, bondOwnerAddr8, bondOwnerAddr9, bondOwnerAddr10, + }, []*avax.UTXO{ + earlySuccessfulProposalWithBondUTXO, expiredSuccessfulProposalWithBondUTXO, + additionalBondUTXO1, additionalBondUTXO2, + }, locked.StateBonded) + + s.EXPECT().GetProposal(earlySuccessfulProposalWithBondID).Return(earlySuccessfulProposalWithBond, nil) + s.EXPECT().RemoveProposal(earlySuccessfulProposalWithBondID, earlySuccessfulProposalWithBond) + s.EXPECT().RemoveProposalIDToFinish(earlySuccessfulProposalWithBondID) + + s.EXPECT().GetProposal(expiredSuccessfulProposalWithBondID).Return(expiredSuccessfulProposalWithBond, nil) + s.EXPECT().RemoveProposal(expiredSuccessfulProposalWithBondID, expiredSuccessfulProposalWithBond) + + // * proposalExecutor + s.EXPECT().GetAddressStates(earlySuccessfulProposalWithBond.MemberAddress). + Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().SetAddressStates(earlySuccessfulProposalWithBond.MemberAddress, as.AddressStateEmpty) + s.EXPECT().GetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID1, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID1, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(earlySuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(nil, database.ErrNotFound) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID1)). + Return(pendingValidator1, nil) + s.EXPECT().DeletePendingValidator(pendingValidator1) + + s.EXPECT().GetAddressStates(expiredSuccessfulProposalWithBond.MemberAddress). + Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().SetAddressStates(expiredSuccessfulProposalWithBond.MemberAddress, as.AddressStateEmpty) + s.EXPECT().GetShortIDLink(expiredSuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode). + Return(memberNodeShortID2, nil) + s.EXPECT().SetShortIDLink(memberNodeShortID2, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().SetShortIDLink(expiredSuccessfulProposalWithBond.MemberAddress, state.ShortLinkKeyRegisterNode, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID2)). + Return(nil, database.ErrNotFound) + s.EXPECT().GetPendingValidator(constants.PrimaryNetworkID, ids.NodeID(memberNodeShortID2)). + Return(pendingValidator2, nil) + s.EXPECT().DeletePendingValidator(pendingValidator2) + // * + + 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: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{ + generateTestInFromUTXO(earlySuccessfulProposalWithBondUTXO, []uint32{}), + generateTestInFromUTXO(expiredSuccessfulProposalWithBondUTXO, []uint32{}), + generateTestInFromUTXO(additionalBondUTXO1, []uint32{}), + generateTestInFromUTXO(additionalBondUTXO2, []uint32{}), + }, + Outs: []*avax.TransferableOutput{ + generateTestOutFromUTXO(earlySuccessfulProposalWithBondUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(expiredSuccessfulProposalWithBondUTXO, ids.Empty, ids.Empty), + generateTestOutFromUTXO(additionalBondUTXO1, ids.Empty, ids.Empty), + generateTestOutFromUTXO(additionalBondUTXO2, ids.Empty, ids.Empty), + }, + }}, + EarlyFinishedSuccessfulProposalIDs: []ids.ID{earlySuccessfulProposalWithBondID}, + ExpiredSuccessfulProposalIDs: []ids.ID{expiredSuccessfulProposalWithBondID}, } }, }, @@ -7419,10 +7866,10 @@ func TestCaminoStandardTxExecutorFinishProposalsTx(t *testing.T) { env := newCaminoEnvironmentWithMocks(caminoGenesisConf, nil) defer func() { require.NoError(t, shutdownCaminoEnvironment(env)) }() - env.config.BerlinPhaseTime = successfulEarlyFinishedProposal.StartTime().Add(-1 * time.Second) + env.config.BerlinPhaseTime = earlySuccessfulProposal.StartTime().Add(-1 * time.Second) utx := tt.utx(env.config) - avax.SortTransferableInputsWithSigners(utx.Ins, tt.signers) + avax.SortTransferableInputs(utx.Ins) avax.SortTransferableOutputs(utx.Outs, txs.Codec) tx, err := txs.NewSigned(utx, txs.Codec, tt.signers) require.NoError(t, err)