diff --git a/vms/platformvm/camino_helpers_test.go b/vms/platformvm/camino_helpers_test.go index 98b403b12bd0..d023ebf373a1 100644 --- a/vms/platformvm/camino_helpers_test.go +++ b/vms/platformvm/camino_helpers_test.go @@ -181,7 +181,7 @@ func newCaminoGenesisWithUTXOs(caminoGenesisConfig api.Camino, genesisUTXOs []ap Addresses: []string{addr}, }, Staked: []api.UTXO{{ - Amount: json.Uint64(defaultWeight), + Amount: json.Uint64(defaultCaminoValidatorWeight), Address: addr, }}, } diff --git a/vms/platformvm/camino_service_test.go b/vms/platformvm/camino_service_test.go index 1cc160bae778..2beb5b61acb9 100644 --- a/vms/platformvm/camino_service_test.go +++ b/vms/platformvm/camino_service_test.go @@ -30,17 +30,17 @@ import ( func TestGetCaminoBalance(t *testing.T) { hrp := constants.NetworkIDToHRP[testNetworkID] - id := keys[0].PublicKey().Address() + id := caminoPreFundedKeys[0].PublicKey().Address() addr, err := address.FormatBech32(hrp, id.Bytes()) require.NoError(t, err) tests := map[string]struct { camino api.Camino - genesisUTXOs []api.UTXO + genesisUTXOs []api.UTXO // unlocked utxos address string - bonded uint64 - deposited uint64 - depositedBonded uint64 + bonded uint64 // additional (to existing genesis validator bond) bonded utxos + deposited uint64 // additional deposited utxos + depositedBonded uint64 // additional depositedBonded utxos expectedError error }{ "Genesis Validator with added balance": { @@ -135,6 +135,18 @@ func TestGetCaminoBalance(t *testing.T) { require.NoError(t, err) } + if tt.bonded != 0 { + outputOwners := secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + } + utxo := generateTestUTXO(ids.GenerateTestID(), avaxAssetID, tt.bonded, outputOwners, ids.Empty, ids.GenerateTestID()) + service.vm.state.AddUTXO(utxo) + err := service.vm.state.Commit() + require.NoError(t, err) + } + if tt.depositedBonded != 0 { outputOwners := secp256k1fx.OutputOwners{ Locktime: 0, @@ -153,7 +165,7 @@ func TestGetCaminoBalance(t *testing.T) { return } require.NoError(t, err) - expectedBalance := json.Uint64(defaultBalance + tt.bonded + tt.deposited + tt.depositedBonded) + expectedBalance := json.Uint64(defaultCaminoValidatorWeight + defaultBalance + tt.bonded + tt.deposited + tt.depositedBonded) if !tt.camino.LockModeBondDeposit { response := responseWrapper.avax @@ -163,8 +175,9 @@ func TestGetCaminoBalance(t *testing.T) { require.Equal(t, json.Uint64(defaultBalance), response.Unlocked, "Wrong unlocked balance. Expected %d ; Returned %d", defaultBalance, response.Unlocked) } else { response := responseWrapper.camino - require.Equal(t, json.Uint64(defaultBalance+tt.bonded+tt.deposited+tt.depositedBonded), response.Balances[avaxAssetID], "Wrong balance. Expected %d ; Returned %d", expectedBalance, response.Balances[avaxAssetID]) + require.Equal(t, json.Uint64(defaultCaminoValidatorWeight+defaultBalance+tt.bonded+tt.deposited+tt.depositedBonded), response.Balances[avaxAssetID], "Wrong balance. Expected %d ; Returned %d", expectedBalance, response.Balances[avaxAssetID]) require.Equal(t, json.Uint64(tt.deposited), response.DepositedOutputs[avaxAssetID], "Wrong deposited balance. Expected %d ; Returned %d", tt.deposited, response.DepositedOutputs[avaxAssetID]) + require.Equal(t, json.Uint64(defaultCaminoValidatorWeight+tt.bonded), response.BondedOutputs[avaxAssetID], "Wrong bonded balance. Expected %d ; Returned %d", tt.bonded, response.BondedOutputs[avaxAssetID]) require.Equal(t, json.Uint64(tt.depositedBonded), response.DepositedBondedOutputs[avaxAssetID], "Wrong depositedBonded balance. Expected %d ; Returned %d", tt.depositedBonded, response.DepositedBondedOutputs[avaxAssetID]) require.Equal(t, json.Uint64(defaultBalance), response.UnlockedOutputs[avaxAssetID], "Wrong unlocked balance. Expected %d ; Returned %d", defaultBalance, response.UnlockedOutputs[avaxAssetID]) } diff --git a/vms/platformvm/camino_vm_test.go b/vms/platformvm/camino_vm_test.go index 5c0eb0f61690..507dfcd8ce78 100644 --- a/vms/platformvm/camino_vm_test.go +++ b/vms/platformvm/camino_vm_test.go @@ -91,6 +91,15 @@ func TestRemoveDeferredValidator(t *testing.T) { ) require.NoError(err) _ = buildAndAcceptBlock(t, vm, tx) + addrStateTx, err := vm.txBuilder.NewAddressStateTx( + consortiumMemberKey.Address(), + false, + as.AddressStateBitKYCVerified, + []*secp256k1.PrivateKey{caminoPreFundedKeys[0]}, + nil, + ) + require.NoError(err) + _ = buildAndAcceptBlock(t, vm, addrStateTx) 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 @@ -158,7 +167,7 @@ func TestRemoveDeferredValidator(t *testing.T) { // Verify that the validator's owner's deferred state and consortium member is true ownerState, _ := vm.state.GetAddressStates(consortiumMemberKey.Address()) - require.Equal(ownerState, as.AddressStateNodeDeferred|as.AddressStateConsortiumMember) + require.Equal(ownerState, as.AddressStateNodeDeferred|as.AddressStateConsortiumMember|as.AddressStateKYCVerified) // Fast-forward clock to time for validator to be rewarded vm.clock.Set(endTime) @@ -209,7 +218,7 @@ func TestRemoveDeferredValidator(t *testing.T) { // Verify that the validator's owner's deferred state is false ownerState, _ = vm.state.GetAddressStates(consortiumMemberKey.Address()) - require.Equal(ownerState, as.AddressStateConsortiumMember) + require.Equal(ownerState, as.AddressStateConsortiumMember|as.AddressStateKYCVerified) timestamp := vm.state.GetTimestamp() require.Equal(endTime.Unix(), timestamp.Unix()) @@ -268,6 +277,15 @@ func TestRemoveReactivatedValidator(t *testing.T) { ) require.NoError(err) _ = buildAndAcceptBlock(t, vm, tx) + addrStateTx, err := vm.txBuilder.NewAddressStateTx( + consortiumMemberKey.Address(), + false, + as.AddressStateBitKYCVerified, + []*secp256k1.PrivateKey{caminoPreFundedKeys[0]}, + nil, + ) + require.NoError(err) + _ = buildAndAcceptBlock(t, vm, addrStateTx) 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 @@ -704,7 +722,7 @@ func TestAdminProposals(t *testing.T) { Address: proposerAddrStr, }, { - Amount: json.Uint64(defaultTxFee), + Amount: json.Uint64(defaultTxFee * 2), Address: caminoPreFundedKey0AddrStr, }, }, &defaultConfig.BanffTime) @@ -734,6 +752,22 @@ func TestAdminProposals(t *testing.T) { require.NoError(err) require.True(applicantAddrState.IsNot(as.AddressStateConsortiumMember)) + // Make applicant (see admin proposal below) kyc-verified + addrStateTx, err = vm.txBuilder.NewAddressStateTx( + applicantAddr, + false, + as.AddressStateBitKYCVerified, + []*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()) + applicantAddrState, err = vm.state.GetAddressStates(applicantAddr) + require.NoError(err) + require.True(applicantAddrState.Is(as.AddressStateKYCVerified)) + // Add admin proposal chainTime := vm.state.GetTimestamp() proposalTx := buildAddMemberProposalTx(t, vm, proposerKey, proposalBondAmount, fee, @@ -775,21 +809,35 @@ func TestAdminProposals(t *testing.T) { } 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) + // member to exclude + memberToExcludeKey, memberToExcludeAddr, _ := generateKeyAndOwner(t) + memberToExcludeNodeKey, memberToExcludeNodeShortID, _ := generateKeyAndOwner(t) + memberToExcludeNodeID := ids.NodeID(memberToExcludeNodeShortID) + + // admin & funds & proposer rootAdminKey := caminoPreFundedKeys[0] - adminProposerKey := caminoPreFundedKeys[0] + consortiumAdminKey := caminoPreFundedKeys[0] + proposerMemberKey := caminoPreFundedKeys[0] + fundsKey := caminoPreFundedKeys[0] + fundsAddr := caminoPreFundedKeys[0].Address() + fundsKeyAddrStr, err := address.FormatBech32(constants.NetworkIDToHRP[testNetworkID], fundsKey.Address().Bytes()) + require.NoError(t, err) defaultConfig := defaultCaminoConfig(true) fee := defaultConfig.TxFee addValidatorFee := defaultConfig.AddPrimaryNetworkValidatorFee proposalBondAmount := defaultConfig.CaminoConfig.DACProposalBondAmount validatorBondAmount := defaultConfig.MaxValidatorStake // is equal to min + // for simplicity: member == member that will be excluded + // proposer == member that creates excludeMember proposal + initialBalance := validatorBondAmount + // proposer validator's bond + validatorBondAmount + addValidatorFee*2 + // bond and fee for validators (both pending and current just in case) of member + proposalBondAmount + fee + // bond and fee for admin proposal to add member that we'll try to exclude later + proposalBondAmount + fee + // bond and fee for excludeMember proposal + proposalBondAmount + fee + // bond and fee for 2nd excludeMember proposal + fee + // fee to give KYC verified status to member before adding him to consortium + fee // fee to give consortiumAdmin role to root admin so he can add member to consortium + initialHeight := uint64(3) tests := map[string]struct { moreExlcude bool // try to exclude member with additional proposal @@ -858,78 +906,130 @@ func TestExcludeMemberProposals(t *testing.T) { if tt.currentValidator { numberOfValidatorsInNetwork++ } + balance := initialBalance burnedAmt := uint64(0) - bondedAmt := uint64(0) - balance := proposalBondAmount + defaultTxFee*8 + validatorBondAmount*2 + addValidatorFee*2 + bondedAmt := validatorBondAmount // proposer validator's bond from genesis + expectedHeight := initialHeight + // 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) + InitialAdmin: rootAdminKey.Address(), + }, []api.UTXO{{Amount: json.Uint64(initialBalance - defaultCaminoValidatorWeight), Address: fundsKeyAddrStr}}, &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 + height, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) + checkBalance(t, vm.state, fundsAddr, + balance, // total // nothing is burned yet + bondedAmt, // bonded + 0, 0, balance-bondedAmt, // unlocked + ) + + // give root admin consortiumAdmin role + addrStateTx, err := vm.txBuilder.NewAddressStateTx( + consortiumAdminKey.Address(), + false, + as.AddressStateBitRoleConsortiumAdminProposer, + []*secp256k1.PrivateKey{rootAdminKey, fundsKey}, + nil, ) - memberAddrState, err := vm.state.GetAddressStates(memberAddr) + require.NoError(err) + _ = buildAndAcceptBlock(t, vm, addrStateTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) + burnedAmt += fee + checkBalance(t, vm.state, fundsAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-bondedAmt-burnedAmt, // unlocked + ) + + // Prepare member that will be excluded + memberAddrState, err := vm.state.GetAddressStates(memberToExcludeAddr) require.NoError(err) require.Equal(as.AddressStateEmpty, memberAddrState) - _, err = vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + _, err = vm.state.GetShortIDLink(memberToExcludeAddr, state.ShortLinkKeyRegisterNode) require.ErrorIs(err, database.ErrNotFound) - - // Make member actual consortium member - tx, err := vm.txBuilder.NewAddressStateTx( - adminProposerKey.Address(), + addrStateTx, err = vm.txBuilder.NewAddressStateTx( + memberToExcludeAddr, false, - as.AddressStateBitRoleConsortiumAdminProposer, + as.AddressStateBitKYCVerified, []*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) + _ = buildAndAcceptBlock(t, vm, addrStateTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) + burnedAmt += fee + checkBalance(t, vm.state, fundsAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-bondedAmt-burnedAmt, // unlocked + ) + + addMemberProposalTx := buildAddMemberProposalTx(t, vm, fundsKey, proposalBondAmount, defaultTxFee, + consortiumAdminKey, memberToExcludeAddr, vm.clock.Time(), true) _, _, _, _ = makeProposalWithTx(t, vm, addMemberProposalTx) // add admin proposal - _ = buildAndAcceptBlock(t, vm, nil) // execute admin proposal - memberAddrState, err = vm.state.GetAddressStates(memberAddr) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) require.NoError(err) - require.Equal(as.AddressStateConsortiumMember, memberAddrState) + require.Equal(expectedHeight, height) + burnedAmt += fee + bondedAmt += proposalBondAmount + checkBalance(t, vm.state, fundsAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-bondedAmt-burnedAmt, // unlocked + ) + + _ = buildAndAcceptBlock(t, vm, nil) // execute admin proposal + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) + memberAddrState, err = vm.state.GetAddressStates(memberToExcludeAddr) + require.NoError(err) + require.Equal(as.AddressStateConsortiumMember|as.AddressStateKYCVerified, memberAddrState) + bondedAmt -= proposalBondAmount + checkBalance(t, vm.state, fundsAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-bondedAmt-burnedAmt, // unlocked + ) // 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}, - }, + memberToExcludeNodeID, + memberToExcludeAddr, + []*secp256k1.PrivateKey{memberToExcludeKey, memberToExcludeNodeKey, fundsKey}, + nil, ) require.NoError(err) blk := buildAndAcceptBlock(t, vm, registerNodeTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) require.Len(blk.Txs(), 1) checkTx(t, vm, blk.ID(), registerNodeTx.ID()) - registeredMemberNodeID, err := vm.state.GetShortIDLink(memberAddr, state.ShortLinkKeyRegisterNode) + registeredMemberNodeID, err := vm.state.GetShortIDLink(memberToExcludeAddr, state.ShortLinkKeyRegisterNode) require.NoError(err) - require.Equal(memberNodeShortID, registeredMemberNodeID) + require.Equal(memberToExcludeNodeShortID, registeredMemberNodeID) burnedAmt += fee - checkBalance(t, vm.state, memberAddr, - balance-burnedAmt, // total - 0, 0, 0, balance-burnedAmt, // unlocked + checkBalance(t, vm.state, fundsAddr, + balance-burnedAmt, // total + bondedAmt, // bonded + 0, 0, balance-bondedAmt-burnedAmt, // unlocked ) } @@ -944,27 +1044,31 @@ func TestExcludeMemberProposals(t *testing.T) { validatorBondAmount, uint64(pendingValidatorStartTime.Unix()), uint64(validatorsEndTime.Unix()), - memberNodeID, - memberAddr, - memberAddr, + memberToExcludeNodeID, + memberToExcludeAddr, + memberToExcludeAddr, 0, - []*secp256k1.PrivateKey{memberKey}, - memberAddr, + []*secp256k1.PrivateKey{memberToExcludeKey, fundsKey}, + fundsAddr, ) require.NoError(err) blk := buildAndAcceptBlock(t, vm, addValidatorTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) require.Len(blk.Txs(), 1) checkTx(t, vm, blk.ID(), addValidatorTx.ID()) - _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.NoError(err) - _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.ErrorIs(err, database.ErrNotFound) burnedAmt += addValidatorFee bondedAmt += validatorBondAmount - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded - 0, 0, balance-burnedAmt-bondedAmt, // unlocked + 0, 0, balance-bondedAmt-burnedAmt, // unlocked ) } @@ -975,52 +1079,64 @@ func TestExcludeMemberProposals(t *testing.T) { validatorBondAmount, uint64(currentValidatorStartTime.Unix()), uint64(validatorsEndTime.Unix()), - memberNodeID, - memberAddr, - memberAddr, + memberToExcludeNodeID, + memberToExcludeAddr, + memberToExcludeAddr, 0, - []*secp256k1.PrivateKey{memberKey}, - memberAddr, + []*secp256k1.PrivateKey{memberToExcludeKey, fundsKey}, + fundsAddr, ) require.NoError(err) blk := buildAndAcceptBlock(t, vm, addValidatorTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) require.Len(blk.Txs(), 1) checkTx(t, vm, blk.ID(), addValidatorTx.ID()) - _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.NoError(err) - _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.ErrorIs(err, database.ErrNotFound) // Advance time, so pending validator will become current vm.clock.Set(currentValidatorStartTime) blk = buildAndAcceptBlock(t, vm, nil) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) require.Empty(blk.Txs()) - _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.ErrorIs(err, database.ErrNotFound) - _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) require.NoError(err) burnedAmt += addValidatorFee bondedAmt += validatorBondAmount - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded - 0, 0, balance-burnedAmt-bondedAmt, // unlocked + 0, 0, balance-bondedAmt-burnedAmt, // unlocked ) } chainTime = vm.state.GetTimestamp() - // Add proposal (member proposes to exclude himself using his own funds) + // Add proposal (one member proposes to exclude another member) 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) + excludeMemberProposalTx := buildExcludeMemberProposalTx(t, vm, fundsKey, proposalBondAmount, fee, + proposerMemberKey, memberToExcludeAddr, proposalStartTime, proposalEndTime, false) proposalState, _, _, _ := makeProposalWithTx(t, vm, excludeMemberProposalTx) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) excludeMemberProposalState, ok := proposalState.(*dac.ExcludeMemberProposalState) require.True(ok) burnedAmt += fee bondedAmt += proposalBondAmount - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded 0, 0, balance-burnedAmt-bondedAmt, // unlocked @@ -1029,9 +1145,12 @@ func TestExcludeMemberProposals(t *testing.T) { 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) + excludeMemberProposalTx := buildExcludeMemberProposalTx(t, vm, fundsKey, proposalBondAmount, fee, + consortiumAdminKey, memberToExcludeAddr, proposalStartTime, proposalStartTime.Add(time.Duration(dac.ExcludeMemberProposalMinDuration)*time.Second), true) require.Error(vm.Builder.AddUnverifiedTx(excludeMemberProposalTx)) + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) return } @@ -1050,10 +1169,14 @@ func TestExcludeMemberProposals(t *testing.T) { for i := 0; i < numberOfVotesToSuccess; i++ { optionWeights[optionIndex]++ - voteTx := buildSimpleVoteTx(t, vm, memberKey, fee, excludeMemberProposalTx.ID(), caminoPreFundedKeys[i], optionIndex) + voteTx := buildSimpleVoteTx(t, vm, fundsKey, fee, excludeMemberProposalTx.ID(), caminoPreFundedKeys[i], optionIndex) proposalState = voteWithTx(t, vm, voteTx, excludeMemberProposalTx.ID(), optionWeights) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) burnedAmt += fee - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded 0, 0, balance-burnedAmt-bondedAmt, // unlocked @@ -1068,20 +1191,24 @@ func TestExcludeMemberProposals(t *testing.T) { // Build block with FinishProposalsTx blk := buildAndAcceptBlock(t, vm, nil) + expectedHeight++ + height, err = vm.GetCurrentHeight(context.Background()) + require.NoError(err) + require.Equal(expectedHeight, height) 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) + memberAddrState, err = vm.state.GetAddressStates(memberToExcludeAddr) 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) + _, pendingValidatorErr := vm.state.GetPendingValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) + _, currentValidatorErr := vm.state.GetCurrentValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) + _, suspendedValidatorErr := vm.state.GetDeferredValidator(constants.PrimaryNetworkID, memberToExcludeNodeID) + registeredMemberNodeID, registeredNodeErr := vm.state.GetShortIDLink(memberToExcludeAddr, state.ShortLinkKeyRegisterNode) if tt.excluded { - require.Equal(as.AddressStateEmpty, memberAddrState) + require.Equal(as.AddressStateKYCVerified, memberAddrState) require.ErrorIs(pendingValidatorErr, database.ErrNotFound) require.ErrorIs(currentValidatorErr, database.ErrNotFound) require.ErrorIs(registeredNodeErr, database.ErrNotFound) @@ -1093,13 +1220,13 @@ func TestExcludeMemberProposals(t *testing.T) { if tt.pendingValidator { bondedAmt -= validatorBondAmount } - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded 0, 0, balance-burnedAmt-bondedAmt, // unlocked ) } else { - require.Equal(as.AddressStateConsortiumMember, memberAddrState) + require.Equal(as.AddressStateConsortiumMember|as.AddressStateKYCVerified, memberAddrState) if tt.pendingValidator { require.NoError(pendingValidatorErr) } else { @@ -1112,12 +1239,12 @@ func TestExcludeMemberProposals(t *testing.T) { } if tt.registerNode { require.NoError(registeredNodeErr) - require.Equal(memberNodeShortID, registeredMemberNodeID) + require.Equal(memberToExcludeNodeShortID, registeredMemberNodeID) } else { require.ErrorIs(registeredNodeErr, database.ErrNotFound) } require.ErrorIs(suspendedValidatorErr, database.ErrNotFound) - checkBalance(t, vm.state, memberAddr, + checkBalance(t, vm.state, fundsAddr, balance-burnedAmt, // total bondedAmt, // bonded 0, 0, balance-burnedAmt-bondedAmt, // unlocked @@ -1187,11 +1314,11 @@ func checkBalance( ) { t.Helper() total, bonded, deposited, depositBonded, unlocked := getBalance(t, db, addr) - 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") + require.Equal(t, expectedTotal, total, "total balance (expected: %d, actual: %d)", expectedTotal, total) + require.Equal(t, expectedBonded, bonded, "bonded balance (expected: %d, actual: %d)", expectedBonded, bonded) + require.Equal(t, expectedDeposited, deposited, "deposited balance (expected: %d, actual: %d)", expectedDeposited, deposited) + require.Equal(t, expectedDepositBonded, depositBonded, "depositBonded balance (expected: %d, actual: %d)", expectedDepositBonded, depositBonded) + require.Equal(t, expectedUnlocked, unlocked, "unlocked balance (expected: %d, actual: %d)", expectedUnlocked, unlocked) } func checkTx(t *testing.T, vm *VM, blkID, txID ids.ID) { diff --git a/vms/platformvm/dac/camino_add_member_proposal_test.go b/vms/platformvm/dac/camino_add_member_proposal_test.go index e60adeeabd29..9b2ba5e98e08 100644 --- a/vms/platformvm/dac/camino_add_member_proposal_test.go +++ b/vms/platformvm/dac/camino_add_member_proposal_test.go @@ -10,6 +10,75 @@ import ( "github.com/stretchr/testify/require" ) +func TestAddMemberProposalVerify(t *testing.T) { + tests := map[string]struct { + proposal *AddMemberProposal + expectedProposal *AddMemberProposal + expectedErr error + }{ + "End-time is equal to start-time": { + proposal: &AddMemberProposal{ + Start: 100, + End: 100, + }, + expectedProposal: &AddMemberProposal{ + Start: 100, + End: 100, + }, + expectedErr: errEndNotAfterStart, + }, + "End-time is less than start-time": { + proposal: &AddMemberProposal{ + Start: 100, + End: 99, + }, + expectedProposal: &AddMemberProposal{ + Start: 100, + End: 99, + }, + expectedErr: errEndNotAfterStart, + }, + "To small duration": { + proposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration - 1, + }, + expectedProposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration - 1, + }, + expectedErr: errWrongDuration, + }, + "To big duration": { + proposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration + 1, + }, + expectedProposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration + 1, + }, + expectedErr: errWrongDuration, + }, + "OK": { + proposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration, + }, + expectedProposal: &AddMemberProposal{ + Start: 100, + End: 100 + AddMemberProposalDuration, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.proposal.Verify(), tt.expectedErr) + require.Equal(t, tt.expectedProposal, tt.proposal) + }) + } +} + func TestAddMemberProposalCreateProposalState(t *testing.T) { tests := map[string]struct { proposal *AddMemberProposal diff --git a/vms/platformvm/dac/camino_base_fee_proposal.go b/vms/platformvm/dac/camino_base_fee_proposal.go index a6fceb33e790..220de5b25311 100644 --- a/vms/platformvm/dac/camino_base_fee_proposal.go +++ b/vms/platformvm/dac/camino_base_fee_proposal.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" "golang.org/x/exp/slices" ) @@ -48,16 +49,25 @@ func (*BaseFeeProposal) AdminProposer() as.AddressState { func (p *BaseFeeProposal) Verify() error { switch { + case len(p.Options) == 0: + return errNoOptions case len(p.Options) > baseFeeProposalMaxOptionsCount: return fmt.Errorf("%w (expected: no more than %d, actual: %d)", errWrongOptionsCount, baseFeeProposalMaxOptionsCount, len(p.Options)) case p.Start >= p.End: return errEndNotAfterStart } + + unique := set.NewSet[uint64](len(p.Options)) for _, fee := range p.Options { if fee == 0 { return errZeroFee } + if unique.Contains(fee) { + return errNotUniqueOption + } + unique.Add(fee) } + return nil } diff --git a/vms/platformvm/dac/camino_base_fee_proposal_test.go b/vms/platformvm/dac/camino_base_fee_proposal_test.go index 2bb3e2117add..5085a962938c 100644 --- a/vms/platformvm/dac/camino_base_fee_proposal_test.go +++ b/vms/platformvm/dac/camino_base_fee_proposal_test.go @@ -10,6 +10,123 @@ import ( "github.com/stretchr/testify/require" ) +func TestBaseFeeProposalVerify(t *testing.T) { + tests := map[string]struct { + proposal *BaseFeeProposal + expectedProposal *BaseFeeProposal + expectedErr error + }{ + "No options": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{}, + }, + expectedErr: errNoOptions, + }, + "To many options": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 3, 4}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 3, 4}, + }, + expectedErr: errWrongOptionsCount, + }, + "End-time is equal to start-time": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 100, + Options: []uint64{1, 2, 3}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 100, + Options: []uint64{1, 2, 3}, + }, + expectedErr: errEndNotAfterStart, + }, + "End-time is less than start-time": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 99, + Options: []uint64{1, 2, 3}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 99, + Options: []uint64{1, 2, 3}, + }, + expectedErr: errEndNotAfterStart, + }, + "Zero fee option": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 0, 3}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 0, 3}, + }, + expectedErr: errZeroFee, + }, + "Not unique fee option": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 1}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 1}, + }, + expectedErr: errNotUniqueOption, + }, + "OK: 1 option": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1}, + }, + }, + "OK: 3 options": { + proposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 3}, + }, + expectedProposal: &BaseFeeProposal{ + Start: 100, + End: 101, + Options: []uint64{1, 2, 3}, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.proposal.Verify(), tt.expectedErr) + require.Equal(t, tt.expectedProposal, tt.proposal) + }) + } +} + func TestBaseFeeProposalCreateProposalState(t *testing.T) { tests := map[string]struct { proposal *BaseFeeProposal diff --git a/vms/platformvm/dac/camino_exclude_member_proposal_test.go b/vms/platformvm/dac/camino_exclude_member_proposal_test.go index f59f9cb1ad98..d33000846bb9 100644 --- a/vms/platformvm/dac/camino_exclude_member_proposal_test.go +++ b/vms/platformvm/dac/camino_exclude_member_proposal_test.go @@ -10,6 +10,85 @@ import ( "github.com/stretchr/testify/require" ) +func TestExcludeMemberProposalVerify(t *testing.T) { + tests := map[string]struct { + proposal *ExcludeMemberProposal + expectedProposal *ExcludeMemberProposal + expectedErr error + }{ + "End-time is equal to start-time": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 100, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 100, + }, + expectedErr: errEndNotAfterStart, + }, + "End-time is less than start-time": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 99, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 99, + }, + expectedErr: errEndNotAfterStart, + }, + "To small duration": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMinDuration - 1, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMinDuration - 1, + }, + expectedErr: errWrongDuration, + }, + "To big duration": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMaxDuration + 1, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMaxDuration + 1, + }, + expectedErr: errWrongDuration, + }, + "OK: min duration": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMinDuration, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMinDuration, + }, + }, + "OK: max duration": { + proposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMaxDuration, + }, + expectedProposal: &ExcludeMemberProposal{ + Start: 100, + End: 100 + ExcludeMemberProposalMaxDuration, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + require.ErrorIs(t, tt.proposal.Verify(), tt.expectedErr) + require.Equal(t, tt.expectedProposal, tt.proposal) + }) + } +} + func TestExcludeMemberProposalCreateProposalState(t *testing.T) { tests := map[string]struct { proposal *ExcludeMemberProposal diff --git a/vms/platformvm/dac/camino_proposal.go b/vms/platformvm/dac/camino_proposal.go index c21dbeb30aa6..12c74303768b 100644 --- a/vms/platformvm/dac/camino_proposal.go +++ b/vms/platformvm/dac/camino_proposal.go @@ -13,6 +13,8 @@ import ( ) var ( + errNoOptions = errors.New("no options") + errNotUniqueOption = errors.New("not unique option") errWrongOptionIndex = errors.New("wrong option index") errEndNotAfterStart = errors.New("proposal end-time is not after start-time") errWrongDuration = errors.New("wrong proposal duration") @@ -61,8 +63,6 @@ type Proposal interface { } type ProposalState interface { - verify.Verifiable - EndTime() time.Time IsActiveAt(time time.Time) bool // Once a proposal has become Finishable, it cannot be undone by adding more votes. diff --git a/vms/platformvm/dac/camino_simple_vote.go b/vms/platformvm/dac/camino_simple_vote.go index c72d7f14c798..59406e1acb6d 100644 --- a/vms/platformvm/dac/camino_simple_vote.go +++ b/vms/platformvm/dac/camino_simple_vote.go @@ -3,18 +3,7 @@ package dac -import ( - "errors" - - "github.com/ava-labs/avalanchego/utils/set" -) - -var ( - _ Vote = (*SimpleVote)(nil) - - errNoOptions = errors.New("no options") - errNotUniqueOption = errors.New("not unique option") -) +var _ Vote = (*SimpleVote)(nil) type SimpleVote struct { OptionIndex uint32 `serialize:"true"` // Index of voted option @@ -33,27 +22,13 @@ type SimpleVoteOption[T any] struct { Weight uint32 `serialize:"true"` // How much this option was voted } -type SimpleVoteOptions[T comparable] struct { +type SimpleVoteOptions[T any] struct { Options []SimpleVoteOption[T] `serialize:"true"` mostVotedWeight uint32 // Weight of most voted option mostVotedOptionIndex uint32 // Index of most voted option unambiguous bool // True, if there is an option with weight > then other options weight } -func (p *SimpleVoteOptions[T]) Verify() error { - if len(p.Options) == 0 { - return errNoOptions - } - unique := set.NewSet[T](len(p.Options)) - for _, option := range p.Options { - if unique.Contains(option.Value) { - return errNotUniqueOption - } - unique.Add(option.Value) - } - return nil -} - func (p SimpleVoteOptions[T]) GetMostVoted() ( mostVotedWeight uint32, mostVotedIndex uint32, diff --git a/vms/platformvm/txs/camino_add_proposal_tx.go b/vms/platformvm/txs/camino_add_proposal_tx.go index 00e8cb02add1..a24a5c56d40d 100644 --- a/vms/platformvm/txs/camino_add_proposal_tx.go +++ b/vms/platformvm/txs/camino_add_proposal_tx.go @@ -13,20 +13,26 @@ import ( "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/locked" + "github.com/ava-labs/avalanchego/vms/types" ) +const maxProposalDescriptionSize = 2048 + var ( _ UnsignedTx = (*AddProposalTx)(nil) - errBadProposal = errors.New("bad proposal") - errBadProposerAuth = errors.New("bad proposer auth") - errTooBigBond = errors.New("to big bond") + errBadProposal = errors.New("bad proposal") + errBadProposerAuth = errors.New("bad proposer auth") + errTooBigBond = errors.New("too big bond") + errTooBigProposalDescription = errors.New("too big proposal description") ) // AddProposalTx is an unsigned addProposalTx type AddProposalTx struct { // Metadata, inputs and outputs BaseTx `serialize:"true"` + // Contains arbitrary bytes, up to maxProposalDescriptionSize + ProposalDescription types.JSONByteSlice `serialize:"true" json:"proposalDescription"` // Proposal bytes ProposalPayload []byte `serialize:"true" json:"proposalPayload"` // Address that can create proposals of this type @@ -43,6 +49,8 @@ func (tx *AddProposalTx) SyntacticVerify(ctx *snow.Context) error { switch { case tx == nil: return ErrNilTx + case len(tx.ProposalDescription) > maxProposalDescriptionSize: + return errTooBigProposalDescription case tx.SyntacticallyVerified: // already passed syntactic verification return nil } diff --git a/vms/platformvm/txs/camino_add_proposal_tx_test.go b/vms/platformvm/txs/camino_add_proposal_tx_test.go index 7b76cb99655e..7500e3a14d1f 100644 --- a/vms/platformvm/txs/camino_add_proposal_tx_test.go +++ b/vms/platformvm/txs/camino_add_proposal_tx_test.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/dac" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" "github.com/stretchr/testify/require" ) @@ -41,6 +42,12 @@ func TestAddProposalTxSyntacticVerify(t *testing.T) { "Nil tx": { expectedErr: ErrNilTx, }, + "Too big proposal description": { + tx: &AddProposalTx{ + ProposalDescription: make(types.JSONByteSlice, maxProposalDescriptionSize+1), + }, + expectedErr: errTooBigProposalDescription, + }, "Fail to unmarshal proposal": { tx: &AddProposalTx{ BaseTx: baseTx, @@ -91,13 +98,21 @@ func TestAddProposalTxSyntacticVerify(t *testing.T) { }, expectedErr: locked.ErrWrongOutType, }, - "OK": { + "OK: no proposal description": { tx: &AddProposalTx{ BaseTx: baseTx, ProposalPayload: proposalBytes, ProposerAuth: &secp256k1fx.Input{}, }, }, + "OK": { + tx: &AddProposalTx{ + BaseTx: baseTx, + ProposalDescription: make(types.JSONByteSlice, maxProposalDescriptionSize), + ProposalPayload: proposalBytes, + ProposerAuth: &secp256k1fx.Input{}, + }, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { diff --git a/vms/platformvm/txs/executor/camino_tx_executor.go b/vms/platformvm/txs/executor/camino_tx_executor.go index 3cada9491e9a..23e5979b7d9f 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor.go +++ b/vms/platformvm/txs/executor/camino_tx_executor.go @@ -1765,7 +1765,7 @@ func (e *CaminoStandardTxExecutor) AddProposalTx(tx *txs.AddProposalTx) error { return fmt.Errorf("%w: %s", errProposerCredentialMismatch, err) } - if err := txProposal.VerifyWith(dac.NewProposalVerifier(e.State, e.Fx, e.Tx, tx)); err != nil { + if err := txProposal.VerifyWith(dac.NewProposalVerifier(e.State, e.Fx, e.Tx, tx, isAdminProposal)); err != nil { return fmt.Errorf("%w: %s", errInvalidProposal, err) } diff --git a/vms/platformvm/txs/executor/camino_tx_executor_test.go b/vms/platformvm/txs/executor/camino_tx_executor_test.go index ce9dab4450d6..1df42052ff45 100644 --- a/vms/platformvm/txs/executor/camino_tx_executor_test.go +++ b/vms/platformvm/txs/executor/camino_tx_executor_test.go @@ -6194,7 +6194,7 @@ func TestCaminoStandardTxExecutorAddProposalTx(t *testing.T) { proposalsIterator.EXPECT().Release() proposalsIterator.EXPECT().Error().Return(nil) - s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateEmpty, nil) + s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateKYCVerified, nil) s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) // * diff --git a/vms/platformvm/txs/executor/dac/camino_dac.go b/vms/platformvm/txs/executor/dac/camino_dac.go index bde394a512c7..1aa1536c1258 100644 --- a/vms/platformvm/txs/executor/dac/camino_dac.go +++ b/vms/platformvm/txs/executor/dac/camino_dac.go @@ -5,6 +5,7 @@ package dac import ( "errors" + "fmt" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" @@ -21,10 +22,12 @@ var ( _ dac.Executor = (*proposalExecutor)(nil) _ dac.BondTxIDsGetter = (*proposalBondTxIDsGetter)(nil) + errNotKYCVerified = errors.New("address is not KYC verified") 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") + errNoActiveValidator = errors.New("no active validator") ) type proposalVerifier struct { @@ -32,6 +35,7 @@ type proposalVerifier struct { fx fx.Fx signedAddProposalTx *txs.Tx addProposalTx *txs.AddProposalTx + isAdminProposal bool } // Executor calls should never error. @@ -52,12 +56,13 @@ type proposalBondTxIDsGetter struct { state state.Chain } -func NewProposalVerifier(state state.Chain, fx fx.Fx, signedTx *txs.Tx, tx *txs.AddProposalTx) dac.Verifier { +func NewProposalVerifier(state state.Chain, fx fx.Fx, signedTx *txs.Tx, tx *txs.AddProposalTx, isAdminProposal bool) dac.Verifier { return &proposalVerifier{ state: state, fx: fx, signedAddProposalTx: signedTx, addProposalTx: tx, + isAdminProposal: isAdminProposal, } } @@ -136,14 +141,15 @@ func (*proposalBondTxIDsGetter) BaseFeeProposal(*dac.BaseFeeProposalState) ([]id // AddMemberProposal func (e *proposalVerifier) AddMemberProposal(proposal *dac.AddMemberProposal) error { - // verify that address isn't consortium member + // verify that address isn't consortium member and is KYC verified applicantAddress, err := e.state.GetAddressStates(proposal.ApplicantAddress) - if err != nil { + switch { + case err != nil: return err - } - - if applicantAddress.Is(as.AddressStateConsortiumMember) { - return errConsortiumMember + case applicantAddress.Is(as.AddressStateConsortiumMember): + return fmt.Errorf("%w (applicant)", errConsortiumMember) + case applicantAddress.IsNot(as.AddressStateKYCVerified): + return fmt.Errorf("%w (applicant)", errNotKYCVerified) } // verify that there is no existing add member proposal for this address @@ -197,14 +203,45 @@ func (*proposalBondTxIDsGetter) AddMemberProposal(*dac.AddMemberProposalState) ( // ExcludeMemberProposal func (e *proposalVerifier) ExcludeMemberProposal(proposal *dac.ExcludeMemberProposal) error { - // verify that address is consortium member + // verify that member-to-exclude is consortium member memberAddressState, err := e.state.GetAddressStates(proposal.MemberAddress) - if err != nil { + switch { + case err != nil: return err + case memberAddressState.IsNot(as.AddressStateConsortiumMember): + return fmt.Errorf("%w (member)", errNotConsortiumMember) } - if memberAddressState.IsNot(as.AddressStateConsortiumMember) { - return errNotConsortiumMember + if !e.isAdminProposal { // if its admin proposal, we don't care about this check + // verify that proposer is consortium member + proposerAddressState, err := e.state.GetAddressStates(e.addProposalTx.ProposerAddress) + switch { + case err != nil: + return err + case proposerAddressState.IsNot(as.AddressStateConsortiumMember): + return fmt.Errorf("%w (proposer)", errNotConsortiumMember) + } + + // verify that proposer has active validator + + // get proposer nodeID + proposerNodeShortID, err := e.state.GetShortIDLink(e.addProposalTx.ProposerAddress, state.ShortLinkKeyRegisterNode) + switch { + case err == database.ErrNotFound: + return errNoActiveValidator + case err != nil: + return err + } + proposerNodeID := ids.NodeID(proposerNodeShortID) + + // get proposer active validator + _, err = e.state.GetCurrentValidator(constants.PrimaryNetworkID, proposerNodeID) + switch { + case err == database.ErrNotFound: + return errNoActiveValidator + case err != nil: + return err + } } // verify that there is no existing exclude member proposal for this address diff --git a/vms/platformvm/txs/executor/dac/camino_dac_test.go b/vms/platformvm/txs/executor/dac/camino_dac_test.go index a7e4e8079f97..a3a6464e7e25 100644 --- a/vms/platformvm/txs/executor/dac/camino_dac_test.go +++ b/vms/platformvm/txs/executor/dac/camino_dac_test.go @@ -49,10 +49,11 @@ func TestProposalVerifierBaseFeeProposal(t *testing.T) { }} tests := map[string]struct { - state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff - utx func() *txs.AddProposalTx - signers [][]*secp256k1.PrivateKey - expectedErr error + state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff + utx func() *txs.AddProposalTx + signers [][]*secp256k1.PrivateKey + isAdminProposal bool + expectedErr error }{ "Proposer isn't caminoProposer": { state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { @@ -137,7 +138,7 @@ func TestProposalVerifierBaseFeeProposal(t *testing.T) { proposal, err := utx.Proposal() require.NoError(t, err) - err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx, tt.isAdminProposal)) require.ErrorIs(t, err, tt.expectedErr) }) } @@ -204,10 +205,11 @@ func TestProposalVerifierAddMemberProposal(t *testing.T) { }} tests := map[string]struct { - state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff - utx func() *txs.AddProposalTx - signers [][]*secp256k1.PrivateKey - expectedErr error + state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff + utx func() *txs.AddProposalTx + signers [][]*secp256k1.PrivateKey + isAdminProposal bool + expectedErr error }{ "Applicant address is consortium member": { state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { @@ -228,6 +230,25 @@ func TestProposalVerifierAddMemberProposal(t *testing.T) { }, expectedErr: errConsortiumMember, }, + "Applicant address is not kyc verified": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(applicantAddress).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: errNotKYCVerified, + }, "Already active AddMemberProposal for this applicant": { state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { s := state.NewMockDiff(c) @@ -236,7 +257,7 @@ func TestProposalVerifierAddMemberProposal(t *testing.T) { proposalsIterator.EXPECT().Value().Return(&dac.AddMemberProposalState{ApplicantAddress: applicantAddress}, nil) proposalsIterator.EXPECT().Release() - s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateEmpty, nil) + s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateKYCVerified, nil) s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) return s }, @@ -261,7 +282,7 @@ func TestProposalVerifierAddMemberProposal(t *testing.T) { proposalsIterator.EXPECT().Release() proposalsIterator.EXPECT().Error().Return(nil) - s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateEmpty, nil) + s.EXPECT().GetAddressStates(applicantAddress).Return(as.AddressStateKYCVerified, nil) s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) return s }, @@ -292,7 +313,7 @@ func TestProposalVerifierAddMemberProposal(t *testing.T) { proposal, err := utx.Proposal() require.NoError(t, err) - err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx, tt.isAdminProposal)) require.ErrorIs(t, err, tt.expectedErr) }) } @@ -341,6 +362,9 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { bondOwnerKey, _, bondOwner := generateKeyAndOwner(t) proposerKey, proposerAddr, _ := generateKeyAndOwner(t) memberAddress := ids.ShortID{1} + memberNodeShortID := ids.ShortID{2} + memberNodeID := ids.NodeID(memberNodeShortID) + memberValidator := &state.Staker{TxID: ids.ID{3}} proposalBondAmt := uint64(100) feeUTXO := generateTestUTXO(ids.ID{1, 2, 3, 4, 5}, ctx.AVAXAssetID, defaultTxFee, feeOwner, ids.Empty, ids.Empty) @@ -363,12 +387,13 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { }} tests := map[string]struct { - state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff - utx func() *txs.AddProposalTx - signers [][]*secp256k1.PrivateKey - expectedErr error + state func(*gomock.Controller, *txs.AddProposalTx) *state.MockDiff + utx func() *txs.AddProposalTx + signers [][]*secp256k1.PrivateKey + isAdminProposal bool + expectedErr error }{ - "Applicant address is not consortium member": { + "Member-to-exclude 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) @@ -387,6 +412,69 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { }, expectedErr: errNotConsortiumMember, }, + "Proposer is not consortium member": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).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, + }, + "Proposer doesn't have registered node": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetShortIDLink(utx.ProposerAddress, state.ShortLinkKeyRegisterNode).Return(ids.ShortEmpty, database.ErrNotFound) + 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: errNoActiveValidator, + }, + "Proposer doesn't have active validator": { + state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { + s := state.NewMockDiff(c) + s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetShortIDLink(utx.ProposerAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(nil, database.ErrNotFound) + 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: errNoActiveValidator, + }, "Already active ExcludeMemberProposal for this member": { state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { s := state.NewMockDiff(c) @@ -396,6 +484,9 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { proposalsIterator.EXPECT().Release() s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetShortIDLink(utx.ProposerAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) return s }, @@ -412,6 +503,31 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { }, expectedErr: errAlreadyActiveProposal, }, + "OK: admin proposal": { + 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}, + }, + isAdminProposal: true, + }, "OK": { state: func(c *gomock.Controller, utx *txs.AddProposalTx) *state.MockDiff { s := state.NewMockDiff(c) @@ -421,6 +537,9 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { proposalsIterator.EXPECT().Error().Return(nil) s.EXPECT().GetAddressStates(memberAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetAddressStates(utx.ProposerAddress).Return(as.AddressStateConsortiumMember, nil) + s.EXPECT().GetShortIDLink(utx.ProposerAddress, state.ShortLinkKeyRegisterNode).Return(memberNodeShortID, nil) + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, memberNodeID).Return(memberValidator, nil) s.EXPECT().GetProposalIterator().Return(proposalsIterator, nil) return s }, @@ -451,7 +570,7 @@ func TestProposalVerifierExcludeMemberProposal(t *testing.T) { proposal, err := utx.Proposal() require.NoError(t, err) - err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx)) + err = proposal.VerifyWith(NewProposalVerifier(tt.state(ctrl, utx), fx, tx, utx, tt.isAdminProposal)) require.ErrorIs(t, err, tt.expectedErr) }) }