From def03016224912258ea0a2b834b4cff03f0a09e6 Mon Sep 17 00:00:00 2001 From: evlekht Date: Thu, 5 Oct 2023 17:29:25 +0400 Subject: [PATCH] wip --- vms/platformvm/camino_vm_test.go | 342 +++++++++++++++++- .../dac/camino_exclude_member_proposal.go | 12 +- vms/platformvm/dac/camino_proposal.go | 2 +- vms/platformvm/txs/builder/camino_builder.go | 2 +- vms/platformvm/txs/dac/camino_dac.go | 4 +- vms/platformvm/txs/dac/camino_helpers_test.go | 4 +- 6 files changed, 344 insertions(+), 22 deletions(-) diff --git a/vms/platformvm/camino_vm_test.go b/vms/platformvm/camino_vm_test.go index e2bea9d718a6..3acbdcb1a341 100644 --- a/vms/platformvm/camino_vm_test.go +++ b/vms/platformvm/camino_vm_test.go @@ -645,7 +645,9 @@ func TestProposals(t *testing.T) { proposalTx := buildBaseFeeProposalTx(t, vm, proposerKey, proposalBondAmount, fee, proposerKey, tt.feeOptions, chainTime.Add(100*time.Second), chainTime.Add(200*time.Second)) proposalState, nextProposalIDsToExpire, nexExpirationTime, proposalIDsToFinish := makeProposalWithTx(t, vm, proposalTx) - require.EqualValues(5, proposalState.TotalAllowedVoters) // all 5 validators must vote + baseFeeProposalState, ok := proposalState.(*dac.BaseFeeProposalState) + require.True(ok) + require.EqualValues(5, baseFeeProposalState.TotalAllowedVoters) // all 5 validators must vote require.Equal([]ids.ID{proposalTx.ID()}, nextProposalIDsToExpire) // we have only one proposal require.Equal(proposalState.EndTime(), nexExpirationTime) require.Empty(proposalIDsToFinish) // no early-finished proposals @@ -658,16 +660,16 @@ func TestProposals(t *testing.T) { // Fast-forward clock to time a bit forward, but still before proposals start // Try to vote on proposal, expect to fail - vm.clock.Set(proposalState.StartTime().Add(-time.Second)) + vm.clock.Set(baseFeeProposalState.StartTime().Add(-time.Second)) addVoteTx := buildSimpleVoteTx(t, vm, proposerKey, fee, proposalTx.ID(), caminoPreFundedKeys[0], 0) require.Error(vm.Builder.AddUnverifiedTx(addVoteTx)) - vm.clock.Set(proposalState.StartTime()) + vm.clock.Set(baseFeeProposalState.StartTime()) - optionWeights := make([]uint32, len(proposalState.Options)) + optionWeights := make([]uint32, len(baseFeeProposalState.Options)) for i, vote := range tt.votes { optionWeights[vote.option]++ voteTx := buildSimpleVoteTx(t, vm, proposerKey, fee, proposalTx.ID(), caminoPreFundedKeys[i], vote.option) - proposalIDsToFinish, proposalState = voteOnBaseFeeWithTx(t, vm, voteTx, proposalTx.ID(), optionWeights) + proposalIDsToFinish, proposalState = voteWithTx(t, vm, voteTx, proposalTx.ID(), optionWeights) if tt.earlyFinish && i == len(tt.votes)-1 { require.Equal([]ids.ID{proposalTx.ID()}, proposalIDsToFinish) // proposal has finished early } else { @@ -724,6 +726,283 @@ func TestProposals(t *testing.T) { } } +func TestExcludeMemberProposals(t *testing.T) { + 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) + + const numberOfValidatorsInNetwork = 5 + // +1 validator cause we'll add one, and then we'll try to exclude it with proposal + // more, than 50% of validators must vote yes + const numberOfVotesToSuccess = (numberOfValidatorsInNetwork+1)/2 + 1 + + // TODO@ balance checks + + defaultConfig := defaultCaminoConfig(true) + proposalBondAmount := defaultConfig.CaminoConfig.DACProposalBondAmount + validatorBondAmount := defaultConfig.MaxValidatorStake // is equal to min + balance := validatorBondAmount + proposalBondAmount + defaultTxFee*(numberOfVotesToSuccess+1) + + tests := map[string]struct { + currentValidator bool + pendingValidator bool + expire bool + success bool + }{ + // TODO@ most voted option[1] (false) must fail proposal! + "Success: no validators": { + success: true, + }, + "Success: has pending validator": { + pendingValidator: true, + success: true, + }, + "Success: has active validator": { + currentValidator: true, + success: true, + }, + "Fail: no validators": { + success: false, + }, + "Fail: expire": { // TODO@ do we need it? // TODO@ move time + expire: true, + success: false, + }, + "Fail: has pending validator": { + pendingValidator: true, + success: false, + }, + "Fail: has active validator": { + currentValidator: true, + success: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + fee := defaultTxFee + burnedAmt := uint64(0) + + // Prepare vm + vm := newCaminoVM(api.Camino{ + VerifyNodeSignature: true, + LockModeBondDeposit: true, + InitialAdmin: caminoPreFundedKeys[0].Address(), + }, []api.UTXO{{ + Amount: json.Uint64(balance), + Address: memberAddrStr, + }}, &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(txs.AddressStateEmpty, memberAddrState) + _, err = vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + require.ErrorIs(err, database.ErrNotFound) + + // Make member actual consortium member + addrStateTx, err := vm.txBuilder.NewAddressStateTx( + memberAddr, + false, + txs.AddressStateBitConsortium, + []*secp256k1.PrivateKey{caminoPreFundedKeys[0]}, + nil, + ) + require.NoError(err) + blk := buildAndAcceptBlock(t, vm, addrStateTx) + require.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), addrStateTx.ID()) + memberAddrState, err = vm.state.GetAddressStates(memberAddr) + require.NoError(err) + require.Equal(txs.AddressStateConsortiumMember, memberAddrState) + + // Register member's node + 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) + + chainTime := vm.state.GetTimestamp() + pendingValidatorStartTime := chainTime.Add(120 * time.Second) + currentValidatorStartTime := chainTime.Add(240 * time.Second) + validatorsEndTime := currentValidatorStartTime.Add(defaultConfig.MinStakeDuration) + + // TODO@ what is happening with registered node after exclusion? + + // 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) + } + + // 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.Len(blk.Txs(), 1) + checkTx(t, vm, blk.ID(), blk.Txs()[0].ID()) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + require.ErrorIs(err, database.ErrNotFound) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + require.NoError(err) + } + + // Add proposal (member proposes to exclude himself using his own funds) + proposalTx := buildExcludeMemberProposalTx(t, vm, memberKey, proposalBondAmount, fee, + memberKey, memberAddr, chainTime.Add(100*time.Second), chainTime.Add(200*time.Second)) + proposalState, _, _, _ := makeProposalWithTx(t, vm, proposalTx) + excludeMemberProposalState, ok := proposalState.(*dac.ExcludeMemberProposalState) + require.True(ok) + burnedAmt += fee + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + proposalBondAmount, // bonded + 0, 0, balance-proposalBondAmount-burnedAmt, // unlocked + ) + + vm.clock.Set(excludeMemberProposalState.StartTime()) + + // If we want proposal to succeed, pick option 0, to fail - option 1 + optionIndex := uint32(1) + if tt.success { + optionIndex = 0 + } + optionWeights := make([]uint32, len(excludeMemberProposalState.Options)) + + // If proposal should be early finished, than we're voting for it with enough votes + if !tt.expire { + for i := 0; i < numberOfVotesToSuccess; i++ { + optionWeights[optionIndex]++ + voteTx := buildSimpleVoteTx(t, vm, memberKey, fee, proposalTx.ID(), caminoPreFundedKeys[i], optionIndex) + _, proposalState = voteWithTx(t, vm, voteTx, proposalTx.ID(), optionWeights) + burnedAmt += fee + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + proposalBondAmount, // bonded + 0, 0, balance-proposalBondAmount-burnedAmt, // 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(proposalTx.ID()) + require.ErrorIs(err, database.ErrNotFound) + checkBalance(t, vm.state, memberAddr, + balance-burnedAmt, // total + 0, 0, 0, balance-burnedAmt, // unlocked + ) + + 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) + if tt.success { + // TODO@ check that member is excluded and validators removed/suspended + // TODO@ check member node + require.Equal(txs.AddressStateEmpty, memberAddrState) + require.ErrorIs(pendingValidatorErr, database.ErrNotFound) + require.ErrorIs(currentValidatorErr, database.ErrNotFound) + if tt.currentValidator { + require.NoError(suspendedValidatorErr) + } else { + require.ErrorIs(suspendedValidatorErr, database.ErrNotFound) + } + checkBalance(t, vm.state, memberAddr, + initialMemberBalance, // total + 0, 0, 0, initialMemberBalance, // unlocked + ) + } else { + require.Equal(txs.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) + } + require.ErrorIs(suspendedValidatorErr, database.ErrNotFound) + checkBalance(t, vm.state, memberAddr, + initialMemberBalance, // total + validatorBondAmount, // bonded + 0, 0, initialMemberBalance-validatorBondAmount, // unlocked + ) + } + }) + } +} + func buildAndAcceptBlock(t *testing.T, vm *VM, tx *txs.Tx) blocks.Block { t.Helper() if tx != nil { @@ -846,12 +1125,55 @@ func buildBaseFeeProposalTx( 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, +) *txs.Tx { + t.Helper() + ins, outs, signers, _, err := vm.txBuilder.Lock( + vm.state, + []*secp256k1.PrivateKey{fundsKey}, + amountToBond, + amountToBurn, + locked.StateBonded, + nil, nil, 0, + ) + require.NoError(t, err) + proposal := &txs.ProposalWrapper{Proposal: &dac.ExcludeMemberProposal{ + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + MemberAddress: memberAddress, + }} + proposalBytes, err := txs.Codec.Marshal(txs.Version, proposal) + require.NoError(t, err) + proposalTx, err := txs.NewSigned(&txs.AddProposalTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: outs, + }}, + ProposalPayload: proposalBytes, + ProposerAddress: proposerKey.Address(), + ProposerAuth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + }, txs.Codec, append(signers, []*secp256k1.PrivateKey{proposerKey})) + require.NoError(t, err) + return proposalTx +} + func makeProposalWithTx( t *testing.T, vm *VM, tx *txs.Tx, ) ( - proposal *dac.BaseFeeProposalState, + proposal dac.ProposalState, nextProposalIDsToExpire []ids.ID, nexExpirationTime time.Time, proposalIDsToFinish []ids.ID, @@ -862,13 +1184,11 @@ func makeProposalWithTx( checkTx(t, vm, blk.ID(), tx.ID()) proposalState, err := vm.state.GetProposal(tx.ID()) require.NoError(t, err) - baseFeeProposalState, ok := proposalState.(*dac.BaseFeeProposalState) - require.True(t, ok) nextProposalIDsToExpire, nexExpirationTime, err = vm.state.GetNextToExpireProposalIDsAndTime(nil) require.NoError(t, err) proposalIDsToFinish, err = vm.state.GetProposalIDsToFinish() require.NoError(t, err) - return baseFeeProposalState, nextProposalIDsToExpire, nexExpirationTime, proposalIDsToFinish + return proposalState, nextProposalIDsToExpire, nexExpirationTime, proposalIDsToFinish } func buildSimpleVoteTx( @@ -908,13 +1228,13 @@ func buildSimpleVoteTx( return addVoteTx } -func voteOnBaseFeeWithTx( +func voteWithTx( t *testing.T, vm *VM, tx *txs.Tx, proposalID ids.ID, expectedVoteWeights []uint32, -) (proposalIDsToFinish []ids.ID, baseFeeProposalState *dac.BaseFeeProposalState) { +) (proposalIDsToFinish []ids.ID, proposalState dac.ProposalState) { t.Helper() blk := buildAndAcceptBlock(t, vm, tx) require.Len(t, blk.Txs(), 1) diff --git a/vms/platformvm/dac/camino_exclude_member_proposal.go b/vms/platformvm/dac/camino_exclude_member_proposal.go index 461d2929ab85..1412859a405f 100644 --- a/vms/platformvm/dac/camino_exclude_member_proposal.go +++ b/vms/platformvm/dac/camino_exclude_member_proposal.go @@ -12,8 +12,10 @@ import ( "golang.org/x/exp/slices" ) -const excludeMemberProposalMinDuration = uint64(time.Hour * 24 * 7 / time.Second) // 7 days -const excludeMemberProposalMaxDuration = uint64(time.Hour * 24 * 30 / time.Second) // 30 days +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) @@ -34,7 +36,7 @@ func (p *ExcludeMemberProposal) EndTime() time.Time { return time.Unix(int64(p.End), 0) } -func (p *ExcludeMemberProposal) GetOptions() any { +func (*ExcludeMemberProposal) GetOptions() any { return []bool{true, false} } @@ -54,8 +56,8 @@ func (p *ExcludeMemberProposal) CreateProposalState(allowedVoters []ids.ShortID) stateProposal := &ExcludeMemberProposalState{ SimpleVoteOptions: SimpleVoteOptions[bool]{ Options: []SimpleVoteOption[bool]{ - SimpleVoteOption[bool]{Value: true}, - SimpleVoteOption[bool]{Value: false}, + {Value: true}, + {Value: false}, }, }, MemberAddress: p.MemberAddress, diff --git a/vms/platformvm/dac/camino_proposal.go b/vms/platformvm/dac/camino_proposal.go index 47a933dadd29..e9ec9ec3a4d7 100644 --- a/vms/platformvm/dac/camino_proposal.go +++ b/vms/platformvm/dac/camino_proposal.go @@ -56,7 +56,7 @@ 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 sucessfully finished. + // 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 24d2c5edec27..ff1fd41f1d3d 100644 --- a/vms/platformvm/txs/builder/camino_builder.go +++ b/vms/platformvm/txs/builder/camino_builder.go @@ -701,7 +701,7 @@ func (b *caminoBuilder) FinishProposalsTx( BlockchainID: b.ctx.ChainID, }}} - lockTxIDs := append(earlyFinishedProposalIDs, expiredProposalIDs...) + lockTxIDs := append(earlyFinishedProposalIDs, expiredProposalIDs...) //nolint:gocritic for _, proposalID := range earlyFinishedProposalIDs { proposal, err := state.GetProposal(proposalID) diff --git a/vms/platformvm/txs/dac/camino_dac.go b/vms/platformvm/txs/dac/camino_dac.go index cb883cc66062..807c0e16995f 100644 --- a/vms/platformvm/txs/dac/camino_dac.go +++ b/vms/platformvm/txs/dac/camino_dac.go @@ -109,7 +109,7 @@ func (e *proposalExecutor) BaseFeeProposal(proposal *dac.BaseFeeProposalState) e return nil } -func (e *proposalBondTxIDsGetter) BaseFeeProposal(proposal *dac.BaseFeeProposalState) ([]ids.ID, error) { +func (*proposalBondTxIDsGetter) BaseFeeProposal(*dac.BaseFeeProposalState) ([]ids.ID, error) { return nil, nil } @@ -159,7 +159,7 @@ func (e *proposalExecutor) AddMemberProposal(proposal *dac.AddMemberProposalStat return nil } -func (e *proposalBondTxIDsGetter) AddMemberProposal(proposal *dac.AddMemberProposalState) ([]ids.ID, error) { +func (*proposalBondTxIDsGetter) AddMemberProposal(*dac.AddMemberProposalState) ([]ids.ID, error) { return nil, nil } diff --git a/vms/platformvm/txs/dac/camino_helpers_test.go b/vms/platformvm/txs/dac/camino_helpers_test.go index b35864fb3e4f..19a79d80ffbc 100644 --- a/vms/platformvm/txs/dac/camino_helpers_test.go +++ b/vms/platformvm/txs/dac/camino_helpers_test.go @@ -109,7 +109,7 @@ func (fvi *fxVMInt) Logger() logging.Logger { return fvi.log } -func defaultFx(isBootstrapped bool) fx.Fx { +func defaultFx(isBootstrapped bool) fx.Fx { //nolint:unparam clk := defaultClock(true) fxVMInt := &fxVMInt{ registry: linearcodec.NewDefault(), @@ -132,7 +132,7 @@ func generateTestUTXO(txID ids.ID, assetID ids.ID, amount uint64, outputOwners s return generateTestUTXOWithIndex(txID, 0, assetID, amount, outputOwners, depositTxID, bondTxID, true) } -func generateTestUTXOWithIndex(txID ids.ID, outIndex uint32, assetID ids.ID, amount uint64, outputOwners secp256k1fx.OutputOwners, depositTxID, bondTxID ids.ID, init bool) *avax.UTXO { //nolint:unparam +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,