From 7a713e556f091d934ce5eb277b67b0402d8b5f25 Mon Sep 17 00:00:00 2001 From: Kiran Pachhai Date: Tue, 8 Oct 2024 13:34:12 -0400 Subject: [PATCH] Added unit tests for emission balancer actions and adding rows to dataset --- .../claim_delegation_stake_rewards_test.go | 134 +++++++++ actions/claim_validator_stake_rewards_test.go | 138 +++++++++ actions/complete_contribute_dataset.go | 36 +-- actions/complete_contribute_dataset_test.go | 263 ++++++++++++++++++ actions/delegate_user_stake_test.go | 161 +++++++++++ actions/initiate_contribute_dataset.go | 2 +- actions/initiate_contribute_dataset_test.go | 189 +++++++++++++ actions/mint_asset_nft_test.go | 2 +- actions/register_validator_stake_test.go | 224 +++++++++++++++ actions/undelegate_user_stake.go | 4 +- actions/undelegate_user_stake_test.go | 139 +++++++++ actions/withdraw_validator_stake_test.go | 130 +++++++++ storage/dataset.go | 8 +- 13 files changed, 1401 insertions(+), 29 deletions(-) create mode 100644 actions/claim_delegation_stake_rewards_test.go create mode 100644 actions/claim_validator_stake_rewards_test.go create mode 100644 actions/complete_contribute_dataset_test.go create mode 100644 actions/delegate_user_stake_test.go create mode 100644 actions/initiate_contribute_dataset_test.go create mode 100644 actions/register_validator_stake_test.go create mode 100644 actions/undelegate_user_stake_test.go create mode 100644 actions/withdraw_validator_stake_test.go diff --git a/actions/claim_delegation_stake_rewards_test.go b/actions/claim_delegation_stake_rewards_test.go new file mode 100644 index 0000000..95d7c66 --- /dev/null +++ b/actions/claim_delegation_stake_rewards_test.go @@ -0,0 +1,134 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestClaimDelegationStakeRewardsActionFailure(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 10, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "StakeMissing", + Actor: actor, + Action: &ClaimDelegationStakeRewards{ + NodeID: nodeID, // Non-existent stake + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrStakeMissing, + }, + { + Name: "StakeNotStarted", + Actor: actor, + Action: &ClaimDelegationStakeRewards{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block greater than the current block height + require.NoError(t, storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + return store + }(), + ExpectedErr: ErrStakeNotStarted, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func TestClaimDelegationStakeRewardsActionSuccess(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "ValidClaim", + ActionID: ids.GenerateTestID(), + Actor: actor, + Action: &ClaimDelegationStakeRewards{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block less than the current block height + require.NoError(t, storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, uint64(20), balance) + + // Check if the stake was claimed correctly + exists, _, _, _, rewardAddress, _, _ := storage.GetDelegatorStakeNoController(ctx, store, actor, nodeID) + require.True(t, exists) + require.Equal(t, actor, rewardAddress) + }, + ExpectedOutputs: &ClaimDelegationStakeRewardsResult{ + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: 1000, + BalanceBeforeClaim: 0, + BalanceAfterClaim: 20, + DistributedTo: actor, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +// BenchmarkClaimDelegationStakeRewards remains unchanged. +func BenchmarkClaimDelegationStakeRewards(b *testing.B) { + require := require.New(b) + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + claimStakeRewardsBenchmark := &chaintest.ActionBenchmark{ + Name: "ClaimStakeRewardsBenchmark", + Actor: actor, + Action: &ClaimDelegationStakeRewards{ + NodeID: nodeID, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block less than the current block height + require.NoError(storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, uint64(20), balance) + }, + } + + ctx := context.Background() + claimStakeRewardsBenchmark.Run(ctx, b) +} diff --git a/actions/claim_validator_stake_rewards_test.go b/actions/claim_validator_stake_rewards_test.go new file mode 100644 index 0000000..fe3a4b5 --- /dev/null +++ b/actions/claim_validator_stake_rewards_test.go @@ -0,0 +1,138 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestClaimValidatorStakeRewardsActionFailure(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 10, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "StakeMissing", + Actor: actor, + Action: &ClaimValidatorStakeRewards{ + NodeID: nodeID, // Non-existent stake + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrStakeMissing, + }, + { + Name: "StakeNotStarted", + Actor: actor, + Action: &ClaimValidatorStakeRewards{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set validator stake with end block greater than the current block height + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, 5000, 10, actor, actor)) + return store + }(), + ExpectedErr: ErrStakeNotStarted, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func TestClaimValidatorStakeRewardsActionSuccess(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "ValidClaim", + ActionID: ids.GenerateTestID(), + Actor: actor, + Action: &ClaimValidatorStakeRewards{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set validator stake with end block less than the current block height + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, emission.GetStakingConfig().MinValidatorStake, 10, actor, actor)) + // Set the balance for the validator + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, uint64(20), balance) + + // Check if the stake still exists after claiming rewards + exists, _, _, _, _, rewardAddress, _, _ := storage.GetValidatorStakeNoController(ctx, store, nodeID) + require.True(t, exists) + require.Equal(t, actor, rewardAddress) + }, + ExpectedOutputs: &ClaimValidatorStakeRewardsResult{ + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: emission.GetStakingConfig().MinValidatorStake, + DelegationFeeRate: 10, + BalanceBeforeClaim: 0, + BalanceAfterClaim: 20, + DistributedTo: actor, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkClaimValidatorStakeRewards(b *testing.B) { + require := require.New(b) + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + claimValidatorStakeRewardsBenchmark := &chaintest.ActionBenchmark{ + Name: "ClaimValidatorStakeRewardsBenchmark", + Actor: actor, + Action: &ClaimValidatorStakeRewards{ + NodeID: nodeID, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set validator stake with end block less than the current block height + require.NoError(storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, emission.GetStakingConfig().MinValidatorStake, 10, actor, actor)) + // Set the balance for the validator + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly updated after claiming rewards + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, uint64(20), balance) // Reward amount set by emission instance + }, + } + + ctx := context.Background() + claimValidatorStakeRewardsBenchmark.Run(ctx, b) +} diff --git a/actions/complete_contribute_dataset.go b/actions/complete_contribute_dataset.go index 3755a23..995631c 100644 --- a/actions/complete_contribute_dataset.go +++ b/actions/complete_contribute_dataset.go @@ -69,6 +69,19 @@ func (d *CompleteContributeDataset) Execute( actor codec.Address, _ ids.ID, ) (codec.Typed, error) { + // Check if the dataset exists + _, _, _, _, _, _, _, _, marketplaceAssetAddress, _, _, _, _, _, _, owner, err := storage.GetDatasetInfoNoController(ctx, mu, d.DatasetAddress) + if err != nil { + return nil, err + } + if actor != owner { + return nil, ErrWrongOwner + } + // Check if the dataset is already on sale + if marketplaceAssetAddress != codec.EmptyAddress { + return nil, ErrDatasetAlreadyOnSale + } + // Check if the dataset contribution exists datasetAddress, dataLocation, dataIdentifier, contributor, active, err := storage.GetDatasetContributionInfoNoController(ctx, mu, d.DatasetContributionID) if err != nil { @@ -84,32 +97,12 @@ func (d *CompleteContributeDataset) Execute( return nil, ErrDatasetContributorMismatch } - // Check if the dataset exists - _, _, _, _, _, _, _, _, marketplaceAssetAddress, _, _, _, _, _, _, owner, err := storage.GetDatasetInfoNoController(ctx, mu, d.DatasetAddress) - if err != nil { - return nil, err - } - if actor != owner { - return nil, ErrWrongOwner - } - - // Check if the dataset is already on sale - if marketplaceAssetAddress != codec.EmptyAddress { - return nil, ErrDatasetAlreadyOnSale - } - // Retrieve the asset info assetType, name, symbol, _, _, _, totalSupply, _, _, _, _, _, _, err := storage.GetAssetInfoNoController(ctx, mu, d.DatasetAddress) if err != nil { return nil, err } - // Check if the nftAddress already exists - nftAddress := codec.CreateAddress(nconsts.AssetFractionalTokenID, d.DatasetContributionID) - if storage.AssetExists(ctx, mu, nftAddress) { - return nil, ErrNFTAlreadyExists - } - // Minting logic for non-fungible tokens if _, err := storage.MintAsset(ctx, mu, d.DatasetAddress, d.DatasetContributor, 1); err != nil { return nil, err @@ -122,7 +115,8 @@ func (d *CompleteContributeDataset) Execute( if err != nil { return nil, err } - symbol = utils.CombineWithSuffix(symbol, totalSupply+1, storage.MaxSymbolSize) + nftAddress := codec.CreateAddress(nconsts.AssetFractionalTokenID, d.DatasetContributionID) + symbol = utils.CombineWithSuffix(symbol, totalSupply, storage.MaxSymbolSize) if err := storage.SetAssetInfo(ctx, mu, nftAddress, assetType, name, symbol, 0, metadataNFT, []byte(d.DatasetAddress.String()), 0, 1, d.DatasetContributor, codec.EmptyAddress, codec.EmptyAddress, codec.EmptyAddress, codec.EmptyAddress); err != nil { return nil, err } diff --git a/actions/complete_contribute_dataset_test.go b/actions/complete_contribute_dataset_test.go new file mode 100644 index 0000000..a18f5ea --- /dev/null +++ b/actions/complete_contribute_dataset_test.go @@ -0,0 +1,263 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/dataset" + "github.com/nuklai/nuklaivm/storage" + "github.com/nuklai/nuklaivm/utils" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" + + nconsts "github.com/nuklai/nuklaivm/consts" +) + +func TestCompleteContributeDatasetAction(t *testing.T) { + dataLocation := "default" + dataIdentifier := "data_id_1234" + + actor := codectest.NewRandomAddress() + datasetAddress := storage.AssetAddress(nconsts.AssetFractionalTokenID, []byte("Valid Name"), []byte("DATASET"), 0, []byte("metadata"), actor) + datasetContributionID := storage.DatasetContributionID(datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor) + nftAddress := codec.CreateAddress(nconsts.AssetFractionalTokenID, datasetContributionID) + + tests := []chaintest.ActionTest{ + { + Name: "WrongOwner", + Actor: codectest.NewRandomAddress(), // Not the owner of the dataset + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set dataset with a different owner + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrWrongOwner, + }, + { + Name: "DatasetAlreadyOnSale", + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set dataset that is already on sale + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codectest.NewRandomAddress(), codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDatasetAlreadyOnSale, + }, + { + Name: "ContributionAlreadyCompleted", + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid contribution + require.NoError(t, storage.SetDatasetContributionInfo(context.Background(), store, datasetContributionID, datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor, true)) + // Set valid dataset + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDatasetContributionAlreadyComplete, + }, + { + Name: "DatasetAddressMismatch", + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid contribution + require.NoError(t, storage.SetDatasetContributionInfo(context.Background(), store, datasetContributionID, codectest.NewRandomAddress(), []byte(dataLocation), []byte(dataIdentifier), actor, false)) + // Set valid dataset + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDatasetAddressMismatch, + }, + { + Name: "DatasetContributorMismatch", + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: codectest.NewRandomAddress(), + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid contribution + require.NoError(t, storage.SetDatasetContributionInfo(context.Background(), store, datasetContributionID, datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor, false)) + // Set valid dataset + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + + return store + }(), + ExpectedErr: ErrDatasetContributorMismatch, + }, + { + Name: "ValidCompletion", + ActionID: ids.GenerateTestID(), + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid contribution + require.NoError(t, storage.SetDatasetContributionInfo(context.Background(), store, datasetContributionID, datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor, false)) + // Set valid dataset + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + // Create existing NFT + require.NoError(t, storage.SetAssetInfo(context.Background(), store, datasetAddress, nconsts.AssetFractionalTokenID, []byte("name"), []byte("SYM"), 0, []byte("metadata"), []byte(datasetAddress.String()), 1, 0, actor, codec.EmptyAddress, codec.EmptyAddress, codec.EmptyAddress, codec.EmptyAddress)) + // Set balance to 0 + config := dataset.GetDatasetConfig() + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, config.CollateralAssetAddressForDataContribution, actor, 0)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + config := dataset.GetDatasetConfig() + + // Check if the balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, config.CollateralAssetAddressForDataContribution, actor) + require.NoError(t, err) + require.Equal(t, config.CollateralAmountForDataContribution, balance) // Collateral refunded + + // Ensure total supply was increased + _, _, _, _, _, _, totalSupply, _, _, _, _, _, _, err := storage.GetAssetInfoNoController(ctx, store, datasetAddress) + require.NoError(t, err) + require.Equal(t, uint64(2), totalSupply) + + // Check if NFT was created correctly + assetType, name, symbol, decimals, metadata, uri, totalSupply, maxSupply, owner, mintAdmin, pauseUnpauseAdmin, freezeUnfreezeAdmin, enableDisableKYCAccountAdmin, err := storage.GetAssetInfoNoController(ctx, store, nftAddress) + require.NoError(t, err) + require.Equal(t, nconsts.AssetFractionalTokenID, assetType) + require.Equal(t, "name", string(name)) + require.Equal(t, "SYM-1", string(symbol)) + require.Equal(t, uint8(0), decimals) + require.Equal(t, datasetAddress.String(), string(uri)) + require.Equal(t, uint64(1), totalSupply) + require.Equal(t, uint64(1), maxSupply) + require.Equal(t, actor, owner) + require.Equal(t, codec.EmptyAddress, mintAdmin) + require.Equal(t, codec.EmptyAddress, pauseUnpauseAdmin) + require.Equal(t, codec.EmptyAddress, freezeUnfreezeAdmin) + require.Equal(t, codec.EmptyAddress, enableDisableKYCAccountAdmin) + // Check metadata + metadataMap, err := utils.BytesToMap(metadata) + require.NoError(t, err) + require.Equal(t, "default", metadataMap["dataLocation"]) + require.Equal(t, "data_id_1234", metadataMap["dataIdentifier"]) + // Check NFT balance + balance, err = storage.GetAssetAccountBalanceNoController(ctx, store, nftAddress, actor) + require.NoError(t, err) + require.Equal(t, uint64(1), balance) + }, + ExpectedOutputs: &CompleteContributeDatasetResult{ + CollateralAssetAddress: dataset.GetDatasetConfig().CollateralAssetAddressForDataContribution, + CollateralAmountRefunded: dataset.GetDatasetConfig().CollateralAmountForDataContribution, + DatasetChildNftAddress: nftAddress, + To: actor, + DataLocation: dataLocation, + DataIdentifier: dataIdentifier, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkCompleteContributeDataset(b *testing.B) { + require := require.New(b) + dataLocation := "default" + dataIdentifier := "data_id_1234" + + actor := codectest.NewRandomAddress() + datasetAddress := storage.AssetAddress(nconsts.AssetFractionalTokenID, []byte("Valid Name"), []byte("DATASET"), 0, []byte("metadata"), actor) + datasetContributionID := storage.DatasetContributionID(datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor) + nftAddress := storage.AssetAddressNFT(datasetAddress, []byte("metadata"), actor) + + completeContributeDatasetBenchmark := &chaintest.ActionBenchmark{ + Name: "CompleteContributeDatasetBenchmark", + Actor: actor, + Action: &CompleteContributeDataset{ + DatasetContributionID: datasetContributionID, + DatasetAddress: datasetAddress, + DatasetContributor: actor, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid dataset + require.NoError(storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + require.NoError(storage.SetAssetInfo(context.Background(), store, datasetAddress, nconsts.AssetFractionalTokenID, []byte("name"), []byte("SYM"), 0, []byte("metadata"), []byte(datasetAddress.String()), 1, 0, actor, actor, codec.EmptyAddress, codec.EmptyAddress, codec.EmptyAddress)) + // Set balance to 0 + config := dataset.GetDatasetConfig() + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, config.CollateralAssetAddressForDataContribution, actor, 0)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + config := dataset.GetDatasetConfig() + + // Check if the balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, config.CollateralAssetAddressForDataContribution, actor) + require.NoError(err) + require.Equal(config.CollateralAmountForDataContribution, balance) // Collateral refunded + + // Ensure total supply was increased + _, _, _, _, _, _, totalSupply, _, _, _, _, _, _, err := storage.GetAssetInfoNoController(ctx, store, datasetAddress) + require.NoError(err) + require.Equal(uint64(2), totalSupply) + + // Check if NFT was created correctly + assetType, name, symbol, decimals, metadata, uri, totalSupply, maxSupply, owner, mintAdmin, pauseUnpauseAdmin, freezeUnfreezeAdmin, enableDisableKYCAccountAdmin, err := storage.GetAssetInfoNoController(ctx, store, nftAddress) + require.NoError(err) + require.Equal(nconsts.AssetFractionalTokenID, assetType) + require.Equal("name", string(name)) + require.Equal("SYM-1", string(symbol)) + require.Equal(uint8(0), decimals) + require.Equal("metadata", string(metadata)) + require.Equal(datasetAddress.String(), string(uri)) + require.Equal(uint64(1), totalSupply) + require.Equal(uint64(1), maxSupply) + require.Equal(actor, owner) + require.Equal(codec.EmptyAddress, mintAdmin) + require.Equal(codec.EmptyAddress, pauseUnpauseAdmin) + require.Equal(codec.EmptyAddress, freezeUnfreezeAdmin) + require.Equal(codec.EmptyAddress, enableDisableKYCAccountAdmin) + // Check NFT balance + balance, err = storage.GetAssetAccountBalanceNoController(ctx, store, nftAddress, actor) + require.NoError(err) + require.Equal(uint64(1), balance) + }, + } + + ctx := context.Background() + completeContributeDatasetBenchmark.Run(ctx, b) +} diff --git a/actions/delegate_user_stake_test.go b/actions/delegate_user_stake_test.go new file mode 100644 index 0000000..055985e --- /dev/null +++ b/actions/delegate_user_stake_test.go @@ -0,0 +1,161 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestDelegateUserStakeAction(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 10, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "ValidatorNotYetRegistered", + Actor: actor, + Action: &DelegateUserStake{ + NodeID: nodeID, // Non-existent validator node ID + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: 1000, + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrValidatorNotYetRegistered, + }, + { + Name: "UserAlreadyStaked", + Actor: actor, + Action: &DelegateUserStake{ + NodeID: nodeID, + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: 1000, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Register the validator + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, 5000, 10, actor, actor)) + // Set the user stake + require.NoError(t, storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + return store + }(), + ExpectedErr: ErrUserAlreadyStaked, + }, + { + Name: "InvalidStakedAmount", + Actor: actor, + Action: &DelegateUserStake{ + NodeID: nodeID, + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: 100, // Invalid staked amount, less than min stake + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Register the validator + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, 5000, 10, actor, actor)) + return store + }(), + ExpectedErr: ErrDelegateStakedAmountInvalid, + }, + { + Name: "ValidStake", + Actor: actor, + Action: &DelegateUserStake{ + NodeID: nodeID, + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: emission.GetStakingConfig().MinDelegatorStake, // Valid staked amount + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Register the validator + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, emission.GetStakingConfig().MinValidatorStake, 10, actor, actor)) + // Set the balance for the user + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, emission.GetStakingConfig().MinDelegatorStake*2)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, emission.GetStakingConfig().MinDelegatorStake, balance) + + // Check if the stake was created correctly + exists, stakeStartBlock, stakeEndBlock, stakedAmount, rewardAddress, _, _ := storage.GetDelegatorStakeNoController(ctx, store, actor, nodeID) + require.True(t, exists) + require.Equal(t, uint64(25), stakeStartBlock) + require.Equal(t, uint64(50), stakeEndBlock) + require.Equal(t, emission.GetStakingConfig().MinDelegatorStake, stakedAmount) + require.Equal(t, actor, rewardAddress) + }, + ExpectedOutputs: &DelegateUserStakeResult{ + StakedAmount: emission.GetStakingConfig().MinDelegatorStake, + BalanceBeforeStake: emission.GetStakingConfig().MinDelegatorStake * 2, + BalanceAfterStake: emission.GetStakingConfig().MinDelegatorStake, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkDelegateUserStake(b *testing.B) { + require := require.New(b) + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 10, StakeRewards: 20}) + + delegateUserStakeBenchmark := &chaintest.ActionBenchmark{ + Name: "DelegateUserStakeBenchmark", + Actor: actor, + Action: &DelegateUserStake{ + NodeID: nodeID, + StakeStartBlock: 25, + StakeEndBlock: 50, + StakedAmount: emission.GetStakingConfig().MinDelegatorStake, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Register the validator + require.NoError(storage.SetValidatorStake(context.Background(), store, nodeID, 25, 50, emission.GetStakingConfig().MinValidatorStake, 10, actor, actor)) + // Set the balance for the user + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, emission.GetStakingConfig().MinDelegatorStake*2)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, emission.GetStakingConfig().MinDelegatorStake, balance) + + // Check if the stake was created correctly + exists, stakeStartBlock, stakeEndBlock, stakedAmount, rewardAddress, _, _ := storage.GetDelegatorStakeNoController(ctx, store, actor, nodeID) + require.True(exists) + require.Equal(b, uint64(25), stakeStartBlock) + require.Equal(b, uint64(50), stakeEndBlock) + require.Equal(b, emission.GetStakingConfig().MinDelegatorStake, stakedAmount) + require.Equal(b, actor, rewardAddress) + }, + } + + ctx := context.Background() + delegateUserStakeBenchmark.Run(ctx, b) +} diff --git a/actions/initiate_contribute_dataset.go b/actions/initiate_contribute_dataset.go index 80c5958..ccc9ecc 100644 --- a/actions/initiate_contribute_dataset.go +++ b/actions/initiate_contribute_dataset.go @@ -51,7 +51,7 @@ func (*InitiateContributeDataset) GetTypeID() uint8 { func (d *InitiateContributeDataset) StateKeys(actor codec.Address) state.Keys { datasetContributionID := storage.DatasetContributionID(d.DatasetAddress, []byte(d.DataLocation), []byte(d.DataIdentifier), actor) return state.Keys{ - string(storage.DatasetContributionInfoKey(datasetContributionID)): state.Read, + string(storage.DatasetContributionInfoKey(datasetContributionID)): state.All, string(storage.DatasetInfoKey(d.DatasetAddress)): state.Read, string(storage.AssetAccountBalanceKey(dataset.GetDatasetConfig().CollateralAssetAddressForDataContribution, actor)): state.Read | state.Write, } diff --git a/actions/initiate_contribute_dataset_test.go b/actions/initiate_contribute_dataset_test.go new file mode 100644 index 0000000..db138d9 --- /dev/null +++ b/actions/initiate_contribute_dataset_test.go @@ -0,0 +1,189 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "strings" + "testing" + + nconsts "github.com/nuklai/nuklaivm/consts" + "github.com/nuklai/nuklaivm/dataset" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestInitiateContributeDatasetAction(t *testing.T) { + dataLocation := "default" + dataIdentifier := "data_id_1234" + + actor := codectest.NewRandomAddress() + datasetAddress := storage.AssetAddress(nconsts.AssetFractionalTokenID, []byte("Valid Name"), []byte("DATASET"), 0, []byte("metadata"), actor) + datasetContributionID := storage.DatasetContributionID(datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor) + + tests := []chaintest.ActionTest{ + { + Name: "DatasetNotOpenForContribution", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: dataLocation, + DataIdentifier: dataIdentifier, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set dataset that is not open for contributions + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), false, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDatasetNotOpenForContribution, + }, + { + Name: "DatasetAlreadyOnSale", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: dataLocation, + DataIdentifier: dataIdentifier, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set dataset that is already on sale + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codectest.NewRandomAddress(), codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDatasetAlreadyOnSale, + }, + { + Name: "InvalidDataLocation", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: strings.Repeat("d", 65), // Invalid data location (too long) + DataIdentifier: dataIdentifier, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid dataset open for contributions + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDataLocationInvalid, + }, + { + Name: "InvalidDataIdentifier", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: dataLocation, + DataIdentifier: "", // Invalid data identifier (empty) + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid dataset open for contributions + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + return store + }(), + ExpectedErr: ErrDataIdentifierInvalid, + }, + { + Name: "ValidContribution", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: dataLocation, + DataIdentifier: dataIdentifier, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid dataset open for contributions + require.NoError(t, storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + // Set sufficient balance for collateral + config := dataset.GetDatasetConfig() + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, config.CollateralAssetAddressForDataContribution, actor, config.CollateralAmountForDataContribution)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + config := dataset.GetDatasetConfig() + + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, config.CollateralAssetAddressForDataContribution, actor) + require.NoError(t, err) + require.Equal(t, uint64(0), balance) // Initial collateral balance should be zero after deduction + + // Verify that the contribution is initiated correctly + datasetAddress, dataLocation, dataIdentifier, contributor, active, err := storage.GetDatasetContributionInfoNoController(ctx, store, datasetContributionID) + require.NoError(t, err) + require.Equal(t, datasetAddress, datasetAddress) + require.Equal(t, "default", string(dataLocation)) + require.Equal(t, "data_id_1234", string(dataIdentifier)) + require.Equal(t, actor, contributor) + require.False(t, active) + }, + ExpectedOutputs: &InitiateContributeDatasetResult{ + DatasetContributionID: datasetContributionID, + CollateralAssetAddress: dataset.GetDatasetConfig().CollateralAssetAddressForDataContribution, + CollateralAmountTaken: dataset.GetDatasetConfig().CollateralAmountForDataContribution, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkInitiateContributeDataset(b *testing.B) { + require := require.New(b) + dataLocation := "default" + dataIdentifier := "data_id_1234" + + actor := codectest.NewRandomAddress() + datasetAddress := storage.AssetAddress(nconsts.AssetFractionalTokenID, []byte("Valid Name"), []byte("DATASET"), 0, []byte("metadata"), actor) + datasetContributionID := storage.DatasetContributionID(datasetAddress, []byte(dataLocation), []byte(dataIdentifier), actor) + + initiateContributeDatasetBenchmark := &chaintest.ActionBenchmark{ + Name: "InitiateContributeDatasetBenchmark", + Actor: actor, + Action: &InitiateContributeDataset{ + DatasetAddress: datasetAddress, + DataLocation: dataLocation, + DataIdentifier: dataIdentifier, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set valid dataset open for contributions + require.NoError(storage.SetDatasetInfo(context.Background(), store, datasetAddress, []byte("Valid Name"), []byte("Valid Description"), []byte("Science"), []byte("MIT"), []byte("MIT"), []byte("http://license-url.com"), []byte("Metadata"), true, codec.EmptyAddress, codec.EmptyAddress, 0, 100, 0, 100, 0, actor)) + // Set sufficient balance for collateral + config := dataset.GetDatasetConfig() + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, config.CollateralAssetAddressForDataContribution, actor, config.CollateralAmountForDataContribution)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + config := dataset.GetDatasetConfig() + + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, config.CollateralAssetAddressForDataContribution, actor) + require.NoError(err) + require.Equal(uint64(0), balance) // Initial collateral balance should be zero after deduction + + // Verify that the contribution is initiated correctly + datasetAddress, dataLocation, dataIdentifier, contributor, active, err := storage.GetDatasetContributionInfoNoController(ctx, store, datasetContributionID) + require.NoError(err) + require.Equal(datasetAddress, datasetAddress) + require.Equal("default", string(dataLocation)) + require.Equal("data_id_1234", string(dataIdentifier)) + require.Equal(actor, contributor) + require.False(active) + }, + } + + ctx := context.Background() + initiateContributeDatasetBenchmark.Run(ctx, b) +} diff --git a/actions/mint_asset_nft_test.go b/actions/mint_asset_nft_test.go index db93834..98597db 100644 --- a/actions/mint_asset_nft_test.go +++ b/actions/mint_asset_nft_test.go @@ -8,10 +8,10 @@ import ( "strings" "testing" + "github.com/ava-labs/avalanchego/ids" "github.com/nuklai/nuklaivm/storage" "github.com/stretchr/testify/require" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/hypersdk/chain/chaintest" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/codec/codectest" diff --git a/actions/register_validator_stake_test.go b/actions/register_validator_stake_test.go new file mode 100644 index 0000000..930f203 --- /dev/null +++ b/actions/register_validator_stake_test.go @@ -0,0 +1,224 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/cli" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/crypto/bls" + "github.com/ava-labs/hypersdk/state" +) + +func TestRegisterValidatorStakeAction(t *testing.T) { + nodeID := ids.GenerateTestNodeID() + otherNodeID := ids.GenerateTestNodeID() + + // Mock valid stake information + stakeInfo1, authSignature1, privateKey, publicKey := generateStakeInfoAndSignature(nodeID, 60, 200, emission.GetStakingConfig().MinValidatorStake, 10) + stakeInfo2, authSignature2, _, _ := generateStakeInfoAndSignature(nodeID, 60, 200, 10, 10) + + actor := privateKey.Address + emission.MockNewEmission(&emission.MockEmission{ + LastAcceptedBlockHeight: 50, // Mock block height + Validator: &emission.Validator{ + IsActive: true, + NodeID: nodeID, + PublicKey: publicKey, + StakedAmount: emission.GetStakingConfig().MinValidatorStake, + DelegationFeeRate: 10, + }, + }) + + tests := []chaintest.ActionTest{ + { + Name: "InvalidNodeID", + Actor: actor, + Action: &RegisterValidatorStake{ + NodeID: otherNodeID, // Different NodeID than the one in StakeInfo + StakeInfo: stakeInfo1, + AuthSignature: authSignature1, + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrInvalidNodeID, + }, + { + Name: "ValidatorAlreadyRegistered", + Actor: actor, + Action: &RegisterValidatorStake{ + NodeID: nodeID, + StakeInfo: stakeInfo1, + AuthSignature: authSignature1, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Register the validator + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 50, 150, 10000, 10, actor, actor)) + return store + }(), + ExpectedErr: ErrValidatorAlreadyRegistered, + }, + { + Name: "InvalidStakeAmount", + Actor: actor, + Action: &RegisterValidatorStake{ + NodeID: nodeID, + StakeInfo: stakeInfo2, + AuthSignature: authSignature2, + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrValidatorStakedAmountInvalid, + }, + { + Name: "ValidRegisterValidatorStake", + ActionID: ids.GenerateTestID(), + Actor: actor, + Action: &RegisterValidatorStake{ + NodeID: nodeID, + StakeInfo: stakeInfo1, + AuthSignature: authSignature1, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set the balance for the user + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, emission.GetStakingConfig().MinValidatorStake*2)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, emission.GetStakingConfig().MinValidatorStake, balance) + + // Check if the stake was created correctly + exists, stakeStartBlock, stakeEndBlock, stakedAmount, delegationFeeRate, rewardAddress, ownerAddress, _ := storage.GetValidatorStakeNoController(ctx, store, nodeID) + require.True(t, exists) + require.Equal(t, uint64(60), stakeStartBlock) + require.Equal(t, uint64(200), stakeEndBlock) + require.Equal(t, emission.GetStakingConfig().MinValidatorStake, stakedAmount) + require.Equal(t, uint64(10), delegationFeeRate) + require.Equal(t, actor, rewardAddress) + require.Equal(t, actor, ownerAddress) + }, + ExpectedOutputs: &RegisterValidatorStakeResult{ + NodeID: nodeID, + StakeStartBlock: 60, + StakeEndBlock: 200, + StakedAmount: emission.GetStakingConfig().MinValidatorStake, + DelegationFeeRate: 10, + RewardAddress: actor, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkRegisterValidatorStake(b *testing.B) { + require := require.New(b) + + nodeID := ids.GenerateTestNodeID() + + // Mock valid stake information + stakeInfo1, authSignature1, privateKey, publicKey := generateStakeInfoAndSignature(nodeID, 60, 200, emission.GetStakingConfig().MinValidatorStake, 10) + + actor := privateKey.Address + emission.MockNewEmission(&emission.MockEmission{ + LastAcceptedBlockHeight: 50, // Mock block height + Validator: &emission.Validator{ + IsActive: true, + NodeID: nodeID, + PublicKey: publicKey, + StakedAmount: emission.GetStakingConfig().MinValidatorStake, + DelegationFeeRate: 10, + }, + }) + + registerValidatorStakeBenchmark := &chaintest.ActionBenchmark{ + Name: "RegisterValidatorStakeBenchmark", + Actor: actor, + Action: &RegisterValidatorStake{ + NodeID: nodeID, + StakeInfo: stakeInfo1, + AuthSignature: authSignature1, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set the balance for the user + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, emission.GetStakingConfig().MinValidatorStake*2)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly deducted + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, emission.GetStakingConfig().MinValidatorStake, balance) + + // Check if the stake was created correctly + exists, stakeStartBlock, stakeEndBlock, stakedAmount, delegationFeeRate, rewardAddress, ownerAddress, _ := storage.GetValidatorStakeNoController(ctx, store, nodeID) + require.True(exists) + require.Equal(b, uint64(60), stakeStartBlock) + require.Equal(b, uint64(200), stakeEndBlock) + require.Equal(b, emission.GetStakingConfig().MinValidatorStake, stakedAmount) + require.Equal(b, uint64(10), delegationFeeRate) + require.Equal(b, actor, rewardAddress) + require.Equal(b, actor, ownerAddress) + }, + } + + ctx := context.Background() + registerValidatorStakeBenchmark.Run(ctx, b) +} + +func generateStakeInfoAndSignature(nodeID ids.NodeID, stakeStartBlock, stakeEndBlock, stakedAmount, delegationFeeRate uint64) ([]byte, []byte, *cli.PrivateKey, []byte) { + blsBase64Key, err := base64.StdEncoding.DecodeString("MdWjv5OOW/p/JKt673vYxwROsfTCO7iZ2jwWCnY18hw=") + if err != nil { + panic(err) + } + secretKey, err := bls.PrivateKeyFromBytes(blsBase64Key) + if err != nil { + panic(err) + } + blsPrivateKey := &cli.PrivateKey{ + Address: auth.NewBLSAddress(bls.PublicFromPrivateKey(secretKey)), + Bytes: bls.PrivateKeyToBytes(secretKey), + } + blsPublicKey := bls.PublicKeyToBytes(bls.PublicFromPrivateKey(secretKey)) + + stakeInfo := &RegisterValidatorStakeResult{ + NodeID: nodeID, + StakeStartBlock: stakeStartBlock, + StakeEndBlock: stakeEndBlock, + StakedAmount: stakedAmount, + DelegationFeeRate: delegationFeeRate, + RewardAddress: blsPrivateKey.Address, + } + packer := codec.NewWriter(stakeInfo.Size(), stakeInfo.Size()) + stakeInfo.Marshal(packer) + stakeInfoBytes := packer.Bytes() + if err != nil { + panic(packer.Err()) + } + authFactory := auth.NewBLSFactory(secretKey) + signature, err := authFactory.Sign(stakeInfoBytes) + if err != nil { + panic(err) + } + signaturePacker := codec.NewWriter(signature.Size(), signature.Size()) + signature.Marshal(signaturePacker) + authSignature := signaturePacker.Bytes() + return stakeInfoBytes, authSignature, blsPrivateKey, blsPublicKey +} diff --git a/actions/undelegate_user_stake.go b/actions/undelegate_user_stake.go index bf88c68..e23c491 100644 --- a/actions/undelegate_user_stake.go +++ b/actions/undelegate_user_stake.go @@ -91,8 +91,8 @@ func (u *UndelegateUserStake) Execute( StakeEndBlock: stakeEndBlock, UnstakedAmount: stakedAmount, RewardAmount: rewardAmount, - BalanceBeforeUnstake: balance - rewardAmount - stakedAmount, - BalanceAfterUnstake: balance, + BalanceBeforeUnstake: balance, + BalanceAfterUnstake: newBalance, DistributedTo: actor, }, nil } diff --git a/actions/undelegate_user_stake_test.go b/actions/undelegate_user_stake_test.go new file mode 100644 index 0000000..99828d0 --- /dev/null +++ b/actions/undelegate_user_stake_test.go @@ -0,0 +1,139 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestUndelegateUserStakeActionFailure(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 25, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "StakeMissing", + Actor: actor, + Action: &UndelegateUserStake{ + NodeID: nodeID, // Non-existent stake + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrStakeMissing, + }, + { + Name: "StakeNotEnded", + Actor: actor, + Action: &UndelegateUserStake{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block greater than the current block height + require.NoError(t, storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + return store + }(), + ExpectedErr: ErrStakeNotEnded, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func TestUndelegateUserStakeActionSuccess(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "ValidUnstake", + Actor: actor, + Action: &UndelegateUserStake{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block less than the current block height + require.NoError(t, storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + // Set user balance before unstaking + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly updated after unstaking + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, uint64(1020), balance) + + // Check if the stake was deleted + exists, _, _, _, _, _, _ := storage.GetDelegatorStakeNoController(ctx, store, actor, nodeID) + require.False(t, exists) + }, + ExpectedOutputs: &UndelegateUserStakeResult{ + StakeStartBlock: 25, + StakeEndBlock: 50, + UnstakedAmount: 1000, + RewardAmount: 20, + BalanceBeforeUnstake: 0, + BalanceAfterUnstake: 1020, + DistributedTo: actor, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkUndelegateUserStake(b *testing.B) { + require := require.New(b) + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + emission.MockNewEmission(&emission.MockEmission{LastAcceptedBlockHeight: 51, StakeRewards: 20}) + + undelegateUserStakeBenchmark := &chaintest.ActionBenchmark{ + Name: "UndelegateUserStakeBenchmark", + Actor: actor, + Action: &UndelegateUserStake{ + NodeID: nodeID, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set stake with end block less than the current block height + require.NoError(storage.SetDelegatorStake(context.Background(), store, actor, nodeID, 25, 50, 1000, actor)) + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly updated after unstaking + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, uint64(1020), balance) + + // Check if the stake was deleted + exists, _, _, _, _, _, _ := storage.GetDelegatorStakeNoController(ctx, store, actor, nodeID) + require.False(exists) + }, + } + + ctx := context.Background() + undelegateUserStakeBenchmark.Run(ctx, b) +} diff --git a/actions/withdraw_validator_stake_test.go b/actions/withdraw_validator_stake_test.go new file mode 100644 index 0000000..fb92722 --- /dev/null +++ b/actions/withdraw_validator_stake_test.go @@ -0,0 +1,130 @@ +// Copyright (C) 2024, Nuklai. All rights reserved. +// See the file LICENSE for licensing terms. + +package actions + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/nuklai/nuklaivm/emission" + "github.com/nuklai/nuklaivm/storage" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/codec/codectest" + "github.com/ava-labs/hypersdk/state" +) + +func TestWithdrawValidatorStakeAction(t *testing.T) { + emission.MockNewEmission(&emission.MockEmission{ + LastAcceptedBlockHeight: 200, // Mock block height + StakeRewards: 100, // Mock reward amount + }) + + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + tests := []chaintest.ActionTest{ + { + Name: "ValidatorNotYetRegistered", + Actor: actor, + Action: &WithdrawValidatorStake{ + NodeID: nodeID, // Non-existent validator + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: ErrNotValidator, + }, + { + Name: "StakeNotStarted", + Actor: actor, + Action: &WithdrawValidatorStake{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set the validator with stake end block greater than the current block height + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 150, 300, 10000, 10, actor, actor)) + return store + }(), + ExpectedErr: ErrStakeNotStarted, + }, + { + Name: "ValidWithdrawal", + ActionID: ids.GenerateTestID(), + Actor: actor, + Action: &WithdrawValidatorStake{ + NodeID: nodeID, + }, + State: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set validator stake with end block less than the current block height + require.NoError(t, storage.SetValidatorStake(context.Background(), store, nodeID, 50, 150, 10000, 10, actor, actor)) + // Set the balance for the validator + require.NoError(t, storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }(), + Assertion: func(ctx context.Context, t *testing.T, store state.Mutable) { + // Check if balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(t, err) + require.Equal(t, uint64(10100), balance) // Reward amount + staked amount + + // Check if the stake was successfully withdrawn + exists, _, _, _, _, _, _, _ := storage.GetValidatorStakeNoController(ctx, store, nodeID) + require.False(t, exists) // Stake should no longer exist + }, + ExpectedOutputs: &WithdrawValidatorStakeResult{ + StakeStartBlock: 50, + StakeEndBlock: 150, + UnstakedAmount: 10000, + DelegationFeeRate: 10, + RewardAmount: 100, + BalanceBeforeUnstake: 0, + BalanceAfterUnstake: 10100, // Reward + Stake amount + DistributedTo: actor, + }, + }, + } + + for _, tt := range tests { + tt.Run(context.Background(), t) + } +} + +func BenchmarkWithdrawValidatorStake(b *testing.B) { + require := require.New(b) + actor := codectest.NewRandomAddress() + nodeID := ids.GenerateTestNodeID() + + emission.MockNewEmission(&emission.MockEmission{ + LastAcceptedBlockHeight: 200, // Mock block height + StakeRewards: 100, // Mock reward amount + }) + + withdrawValidatorStakeBenchmark := &chaintest.ActionBenchmark{ + Name: "WithdrawValidatorStakeBenchmark", + Actor: actor, + Action: &WithdrawValidatorStake{ + NodeID: nodeID, + }, + CreateState: func() state.Mutable { + store := chaintest.NewInMemoryStore() + // Set validator stake with end block less than the current block height + require.NoError(storage.SetValidatorStake(context.Background(), store, nodeID, 50, 150, 10000, 10, actor, actor)) + // Set the balance for the validator + require.NoError(storage.SetAssetAccountBalance(context.Background(), store, storage.NAIAddress, actor, 0)) + return store + }, + Assertion: func(ctx context.Context, b *testing.B, store state.Mutable) { + // Check if balance is correctly updated + balance, err := storage.GetAssetAccountBalanceNoController(ctx, store, storage.NAIAddress, actor) + require.NoError(err) + require.Equal(b, uint64(10100), balance) // Reward amount + staked amount + }, + } + + ctx := context.Background() + withdrawValidatorStakeBenchmark.Run(ctx, b) +} diff --git a/storage/dataset.go b/storage/dataset.go index e46d63b..50064bb 100644 --- a/storage/dataset.go +++ b/storage/dataset.go @@ -217,10 +217,10 @@ func DatasetContributionID(datasetAddress codec.Address, dataLocation, dataIdent } func DatasetContributionInfoKey(contributionID ids.ID) (k []byte) { - k = make([]byte, 1+ids.IDLen+consts.Uint16Len) // Length of prefix + contributionID + MarketplaceContributionInfoChunks - k[0] = marketplaceContributionPrefix // marketplaceContributionPrefix is a constant representing the contribution category - copy(k[1:1+ids.IDLen], contributionID[:]) // Copy the contributionID - binary.BigEndian.PutUint16(k[1+codec.AddressLen:], DatasetContributionInfoChunks) // Adding MarketplaceContributionInfoChunks + k = make([]byte, 1+ids.IDLen+consts.Uint16Len) // Length of prefix + contributionID + MarketplaceContributionInfoChunks + k[0] = marketplaceContributionPrefix // marketplaceContributionPrefix is a constant representing the contribution category + copy(k[1:1+ids.IDLen], contributionID[:]) // Copy the contributionID + binary.BigEndian.PutUint16(k[1+ids.IDLen:], DatasetContributionInfoChunks) // Adding MarketplaceContributionInfoChunks return }