diff --git a/cmd/nuklai-cli/cmd/action.go b/cmd/nuklai-cli/cmd/action.go index c5307dd..34c51e8 100644 --- a/cmd/nuklai-cli/cmd/action.go +++ b/cmd/nuklai-cli/cmd/action.go @@ -580,7 +580,7 @@ var registerValidatorStakeCmd = &cobra.Command{ if autoRegister { hutils.Outf("{{yellow}}Loading private key for %s{{/}}\n", nodeNumber) - validatorSignerKey, err := loadPrivateKey("bls", fmt.Sprintf("/tmp/nuklaivm/nodes/%s-bls/signer.key", nodeNumber)) + validatorSignerKey, err := LoadPrivateKey("bls", fmt.Sprintf("/tmp/nuklaivm/nodes/%s-bls/signer.key", nodeNumber)) if err != nil { return err } diff --git a/cmd/nuklai-cli/cmd/key.go b/cmd/nuklai-cli/cmd/key.go index e78f2ba..0ddb42a 100644 --- a/cmd/nuklai-cli/cmd/key.go +++ b/cmd/nuklai-cli/cmd/key.go @@ -91,7 +91,7 @@ func generatePrivateKey(k string) (*cli.PrivateKey, error) { } } -func loadPrivateKey(k string, path string) (*cli.PrivateKey, error) { +func LoadPrivateKey(k string, path string) (*cli.PrivateKey, error) { switch k { case ed25519Key: p, err := hutils.LoadBytes(path, ed25519.PrivateKeyLen) @@ -187,7 +187,7 @@ var importKeyCmd = &cobra.Command{ return checkKeyType(args[0]) }, RunE: func(_ *cobra.Command, args []string) error { - priv, err := loadPrivateKey(args[0], args[1]) + priv, err := LoadPrivateKey(args[0], args[1]) if err != nil { return err } diff --git a/controller/controller.go b/controller/controller.go index 2d3bc5c..25e8b22 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -45,7 +45,7 @@ type Controller struct { metaDB database.Database - emission *emission.Emission // Emission Balancer for NuklaiVM + emission emission.Tracker // Emission Balancer for NuklaiVM } func New() *vm.VM { @@ -123,13 +123,23 @@ func (c *Controller) Initialize( // Create builder and gossiper var ( - build builder.Builder - gossip gossiper.Gossiper + build builder.Builder + gossip gossiper.Gossiper + tracker emission.Tracker ) + + // Initialize emission balancer + emissionAddr, err := codec.ParseAddressBech32(nconsts.HRP, c.genesis.EmissionBalancer.EmissionAddress) + if err != nil { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + } + if c.config.TestMode { c.inner.Logger().Info("running build and gossip in test mode") build = builder.NewManual(inner) gossip = gossiper.NewManual(inner) + tracker = emission.NewManual(c, c.inner, c.genesis.EmissionBalancer.TotalSupply, c.genesis.EmissionBalancer.MaxSupply, emissionAddr) + c.emission = tracker } else { build = builder.NewTime(inner) gcfg := gossiper.DefaultProposerConfig() @@ -142,15 +152,10 @@ func (c *Controller) Initialize( if err != nil { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err } + tracker = emission.NewEmission(c, c.inner, c.genesis.EmissionBalancer.TotalSupply, c.genesis.EmissionBalancer.MaxSupply, emissionAddr) + c.emission = tracker } - // Initialize emission balancer - emissionAddr, err := codec.ParseAddressBech32(nconsts.HRP, c.genesis.EmissionBalancer.EmissionAddress) - if err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err - } - c.emission = emission.New(c, c.inner, c.genesis.EmissionBalancer.TotalSupply, c.genesis.EmissionBalancer.MaxSupply, emissionAddr) - return c.config, c.genesis, build, gossip, blockDB, stateDB, apis, nconsts.ActionRegistry, nconsts.AuthRegistry, auth.Engines(), nil } @@ -255,14 +260,15 @@ func (c *Controller) Accepted(ctx context.Context, blk *chain.StatelessBlock) er } } + emissionAccount, totalSupply, maxSupply, _, _ := c.emission.GetInfo() // Distribute fees if totalFee > 0 { c.emission.DistributeFees(totalFee) - emissionAddress, err := codec.AddressBech32(nconsts.HRP, c.emission.EmissionAccount.Address) + emissionAddress, err := codec.AddressBech32(nconsts.HRP, emissionAccount.Address) if err != nil { return err // This should never happen } - c.inner.Logger().Info("distributed fees to Emission and Validators", zap.Uint64("current block height", c.inner.LastAcceptedBlock().Height()), zap.Uint64("total fee", totalFee), zap.Uint64("total supply", c.emission.TotalSupply), zap.Uint64("max supply", c.emission.MaxSupply), zap.Uint64("rewards per epock", c.emission.GetRewardsPerEpoch()), zap.String("emission address", emissionAddress), zap.Uint64("emission address unclaimed balance", c.emission.EmissionAccount.UnclaimedBalance)) + c.inner.Logger().Info("distributed fees to Emission and Validators", zap.Uint64("current block height", c.inner.LastAcceptedBlock().Height()), zap.Uint64("total fee", totalFee), zap.Uint64("total supply", totalSupply), zap.Uint64("max supply", maxSupply), zap.Uint64("rewards per epock", c.emission.GetRewardsPerEpoch()), zap.String("emission address", emissionAddress), zap.Uint64("emission address unclaimed balance", emissionAccount.UnclaimedBalance)) c.metrics.feesDistributed.Add(float64(totalFee)) } @@ -270,7 +276,7 @@ func (c *Controller) Accepted(ctx context.Context, blk *chain.StatelessBlock) er mintNewNAI := c.emission.MintNewNAI() if mintNewNAI > 0 { c.emission.AddToTotalSupply(mintNewNAI) - c.inner.Logger().Info("minted new NAI", zap.Uint64("current block height", c.inner.LastAcceptedBlock().Height()), zap.Uint64("newly minted NAI", mintNewNAI), zap.Uint64("total supply", c.emission.TotalSupply), zap.Uint64("max supply", c.emission.MaxSupply)) + c.inner.Logger().Info("minted new NAI", zap.Uint64("current block height", c.inner.LastAcceptedBlock().Height()), zap.Uint64("newly minted NAI", mintNewNAI), zap.Uint64("total supply", totalSupply), zap.Uint64("max supply", maxSupply)) } return batch.Write() diff --git a/controller/resolutions.go b/controller/resolutions.go index b2e0dbd..38d3436 100644 --- a/controller/resolutions.go +++ b/controller/resolutions.go @@ -59,7 +59,8 @@ func (c *Controller) GetLoanFromState( } func (c *Controller) GetEmissionInfo() (uint64, uint64, uint64, uint64, uint64, emission.EmissionAccount, emission.EpochTracker, error) { - return c.emission.GetLastAcceptedBlockHeight(), c.emission.TotalSupply, c.emission.MaxSupply, c.emission.TotalStaked, c.emission.GetRewardsPerEpoch(), c.emission.EmissionAccount, c.emission.EpochTracker, nil + emissionAccount, totalSupply, maxSupply, totalStaked, epochTracker := c.emission.GetInfo() + return c.emission.GetLastAcceptedBlockHeight(), totalSupply, maxSupply, totalStaked, c.emission.GetRewardsPerEpoch(), emissionAccount, epochTracker, nil } func (c *Controller) GetValidators(ctx context.Context, staked bool) ([]*emission.Validator, error) { diff --git a/emission/emission.go b/emission/emission.go index a21ad9f..2b9113e 100644 --- a/emission/emission.go +++ b/emission/emission.go @@ -15,37 +15,7 @@ import ( "github.com/nuklai/nuklaivm/storage" ) -var ( - emission *Emission - once sync.Once -) - -type Validator struct { - IsActive bool `json:"isActive"` // Indicates if the validator is currently active - NodeID ids.NodeID `json:"nodeID"` // Node ID of the validator - PublicKey []byte `json:"publicKey"` // Public key of the validator - StakedAmount uint64 `json:"stakedAmount"` // Total amount staked by the validator - UnclaimedStakedReward uint64 `json:"stakedReward"` // Total rewards accumulated by the validator - DelegationFeeRate float64 `json:"delegationFeeRate"` // Fee rate for delegations - DelegatedAmount uint64 `json:"delegatedAmount"` // Total amount delegated to the validator - UnclaimedDelegatedReward uint64 `json:"unclaimedDelegatedReward"` // Total rewards accumulated by the delegators - - delegatorsLastClaim map[codec.Address]uint64 // Map of delegator addresses to their last claim block height - epochRewards map[uint64]uint64 // Rewards per epoch - stakeStartBlock uint64 // Start block of the stake - stakeEndBlock uint64 // End block of the stake -} - -type EmissionAccount struct { - Address codec.Address `json:"address"` - UnclaimedBalance uint64 `json:"unclaimedBalance"` -} - -type EpochTracker struct { - BaseAPR float64 `json:"baseAPR"` // Base APR to use - BaseValidators uint64 `json:"baseValidators"` // Base number of validators to use - EpochLength uint64 `json:"epochLength"` // Number of blocks per reward epoch -} +var _ Tracker = (*Emission)(nil) type Emission struct { c Controller @@ -65,7 +35,7 @@ type Emission struct { // New initializes the Emission struct with initial parameters and sets up the validators heap // and indices map. -func New(c Controller, vm NuklaiVM, totalSupply, maxSupply uint64, emissionAddress codec.Address) *Emission { +func NewEmission(c Controller, vm NuklaiVM, totalSupply, maxSupply uint64, emissionAddress codec.Address) *Emission { once.Do(func() { c.Logger().Info("Initializing emission with max supply and rewards per block settings") @@ -91,12 +61,7 @@ func New(c Controller, vm NuklaiVM, totalSupply, maxSupply uint64, emissionAddre }, } }) - return emission -} - -// GetEmission returns the singleton instance of Emission -func GetEmission() *Emission { - return emission + return emission.(*Emission) } // AddToTotalSupply increases the total supply of NAI by a specified amount, ensuring it @@ -527,15 +492,6 @@ func (e *Emission) DistributeFees(fee uint64) { } } -func distributeValidatorRewards(totalValidatorReward uint64, delegationFeeRate float64, delegatedAmount uint64) (uint64, uint64) { - delegationRewards := uint64(0) - if delegatedAmount > 0 { - delegationRewards = uint64(float64(totalValidatorReward) * delegationFeeRate) - } - validatorRewards := totalValidatorReward - delegationRewards - return validatorRewards, delegationRewards -} - // GetStakedValidator retrieves the details of a specific validator by their NodeID. func (e *Emission) GetStakedValidator(nodeID ids.NodeID) []*Validator { e.c.Logger().Info("fetching staked validator") @@ -591,3 +547,12 @@ func (e *Emission) GetLastAcceptedBlockHeight() uint64 { e.c.Logger().Info("fetching last accepted block height") return e.nuklaivm.LastAcceptedBlock().Height() } + +func (e *Emission) GetEmissionValidators() map[ids.NodeID]*Validator { + e.c.Logger().Info("fetching emission validators") + return e.validators +} + +func (e *Emission) GetInfo() (emissionAccount EmissionAccount, totalSupply uint64, maxSupply uint64, totalStaked uint64, epochTracker EpochTracker) { + return e.EmissionAccount, e.TotalSupply, e.MaxSupply, e.TotalStaked, e.EpochTracker +} diff --git a/emission/helpers.go b/emission/helpers.go new file mode 100644 index 0000000..f577841 --- /dev/null +++ b/emission/helpers.go @@ -0,0 +1,49 @@ +package emission + +import ( + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/hypersdk/codec" +) + +var ( + once sync.Once + emission Tracker +) + +type Validator struct { + IsActive bool `json:"isActive"` // Indicates if the validator is currently active + NodeID ids.NodeID `json:"nodeID"` // Node ID of the validator + PublicKey []byte `json:"publicKey"` // Public key of the validator + StakedAmount uint64 `json:"stakedAmount"` // Total amount staked by the validator + UnclaimedStakedReward uint64 `json:"stakedReward"` // Total rewards accumulated by the validator + DelegationFeeRate float64 `json:"delegationFeeRate"` // Fee rate for delegations + DelegatedAmount uint64 `json:"delegatedAmount"` // Total amount delegated to the validator + UnclaimedDelegatedReward uint64 `json:"delegatedReward"` // Total rewards accumulated by the delegators + + delegatorsLastClaim map[codec.Address]uint64 // Map of delegator addresses to their last claim block height + epochRewards map[uint64]uint64 // Rewards per epoch + stakeStartBlock uint64 // Start block of the stake + stakeEndBlock uint64 // End block of the stake +} + +type EmissionAccount struct { + Address codec.Address `json:"address"` + UnclaimedBalance uint64 `json:"unclaimedBalance"` +} + +type EpochTracker struct { + BaseAPR float64 `json:"baseAPR"` // Base APR to use + BaseValidators uint64 `json:"baseValidators"` // Base number of validators to use + EpochLength uint64 `json:"epochLength"` // Number of blocks per reward epoch +} + +func distributeValidatorRewards(totalValidatorReward uint64, delegationFeeRate float64, delegatedAmount uint64) (uint64, uint64) { + delegationRewards := uint64(0) + if delegatedAmount > 0 { + delegationRewards = uint64(float64(totalValidatorReward) * delegationFeeRate) + } + validatorRewards := totalValidatorReward - delegationRewards + return validatorRewards, delegationRewards +} diff --git a/emission/mock.go b/emission/mock.go new file mode 100644 index 0000000..45dc0c0 --- /dev/null +++ b/emission/mock.go @@ -0,0 +1,552 @@ +// Copyright (C) 2024, AllianceBlock. All rights reserved. +// See the file LICENSE for licensing terms. + +package emission + +import ( + "context" + "sync" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/crypto/bls" + "github.com/ava-labs/hypersdk/state" + "github.com/nuklai/nuklaivm/storage" +) + +var _ Tracker = (*Manual)(nil) + +type Manual struct { + c Controller + nuklaivm NuklaiVM + + TotalSupply uint64 `json:"totalSupply"` // Total supply of NAI + MaxSupply uint64 `json:"maxSupply"` // Max supply of NAI + EmissionAccount EmissionAccount `json:"emissionAccount"` // Emission Account Info + + validators map[ids.NodeID]*Validator + CurrentValidators []*Validator + TotalStaked uint64 `json:"totalStaked"` // Total staked NAI + + EpochTracker EpochTracker `json:"epochTracker"` // Epoch Tracker Info + + lock sync.RWMutex +} + +// New initializes the Emission struct with initial parameters and sets up the validators heap +// and indices map. +func NewManual(c Controller, vm NuklaiVM, totalSupply, maxSupply uint64, emissionAddress codec.Address) *Manual { + once.Do(func() { + c.Logger().Info("Initializing emission with max supply and rewards per block settings") + + if maxSupply == 0 { + maxSupply = GetStakingConfig().RewardConfig.SupplyCap // Use the staking config's supply cap if maxSupply is not specified + } + + emission = &Manual{ // Create the Emission instance with initialized values + c: c, + nuklaivm: vm, + TotalSupply: totalSupply, + MaxSupply: maxSupply, + EmissionAccount: EmissionAccount{ // Setup the emission account with the provided address + Address: emissionAddress, + }, + validators: make(map[ids.NodeID]*Validator), + EpochTracker: EpochTracker{ + BaseAPR: 0.25, // 25% APR + BaseValidators: 100, + EpochLength: 10, + // TODO: Enable this in production + // EpochLength: 1200, // roughly 1 hour with 3 sec block time + }, + } + }) + return emission.(*Manual) +} + +// AddToTotalSupply increases the total supply of NAI by a specified amount, ensuring it +// does not exceed the max supply. +func (e *Manual) AddToTotalSupply(amount uint64) uint64 { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("adding to the total supply of NAI") + if e.TotalSupply+amount > e.MaxSupply { + amount = e.MaxSupply - e.TotalSupply // Adjust to not exceed max supply + } + e.TotalSupply += amount + return e.TotalSupply +} + +// GetNumDelegators returns the total number of delegators across all validators. +func (e *Manual) GetNumDelegators(nodeID ids.NodeID) int { + e.c.Logger().Info("fetching total number of delegators") + + numDelegators := 0 + // Get delegators for all validators + if nodeID == ids.EmptyNodeID { + for _, validator := range e.validators { + numDelegators += len(validator.delegatorsLastClaim) + } + } else { + // Get delegators for a specific validator + if validator, exists := e.validators[nodeID]; exists { + numDelegators = len(validator.delegatorsLastClaim) + } + } + + return numDelegators +} + +// GetAPRForValidators calculates the Annual Percentage Rate (APR) for validators +// based on the number of validators. +func (e *Manual) GetAPRForValidators() float64 { + e.c.Logger().Info("getting APR for validators") + + apr := e.EpochTracker.BaseAPR // APR is expressed per year as a decimal, e.g., 0.25 for 25% + // Beyond baseValidators, APR decreases proportionately + baseValidators := int(e.EpochTracker.BaseValidators) + if len(e.validators) > baseValidators { + apr /= float64(len(e.validators)) / float64(baseValidators) + } + return apr +} + +// GetRewardsPerEpoch calculates the rewards per epock based on the total staked amount +// and the APR for validators. +func (e *Manual) GetRewardsPerEpoch() uint64 { + e.c.Logger().Info("getting rewards per epock") + + // Calculate total rewards for the epoch based on APR and staked amount + rewardsPerBlock := uint64((float64(e.TotalStaked) * e.GetAPRForValidators() / 365 / 24 / 60 / 60) * (float64(e.EpochTracker.EpochLength) * 3)) // 3 seconds per block + + if e.TotalSupply+rewardsPerBlock > e.MaxSupply { + rewardsPerBlock = e.MaxSupply - e.TotalSupply // Adjust to not exceed max supply + } + return rewardsPerBlock +} + +// CalculateUserDelegationRewards computes the rewards for a user's delegated stake to a +// validator, factoring in the delegation duration and amount. +func (e *Manual) CalculateUserDelegationRewards(nodeID ids.NodeID, actor codec.Address, currentBlockHeight uint64) (uint64, error) { + e.c.Logger().Info("calculating rewards for user delegation") + + // Find the validator + validator, exists := e.validators[nodeID] + if !exists { + return 0, ErrValidatorNotFound + } + + // Check if the delegator exists + lastClaimHeight, exists := validator.delegatorsLastClaim[actor] + if !exists { + return 0, ErrDelegatorNotFound + } + + stateDB, err := e.nuklaivm.State() + if err != nil { + return 0, err + } + mu := state.NewSimpleMutable(stateDB) + + // Get user's delegation stake info + exists, _, userStakedAmount, _, _, _ := storage.GetDelegateUserStake(context.TODO(), mu, actor, nodeID) + if !exists { + return 0, ErrStakeNotFound + } + + // Iterate over each epoch since the last claim + startEpoch := lastClaimHeight / e.EpochTracker.EpochLength + endEpoch := currentBlockHeight / e.EpochTracker.EpochLength + totalReward := uint64(0) + + for epoch := startEpoch; epoch < endEpoch; epoch++ { + if reward, ok := validator.epochRewards[epoch]; ok { + // Calculate reward for this epoch + delegatorShare := float64(userStakedAmount) / float64(validator.DelegatedAmount) + epochReward := delegatorShare * float64(reward) + totalReward += uint64(epochReward) + } + } + + return totalReward, nil +} + +// RegisterValidatorStake adds a new validator to the heap with the specified staked amount +// and updates the total staked amount. +func (e *Manual) RegisterValidatorStake(nodeID ids.NodeID, nodePublicKey *bls.PublicKey, stakeStartBlock, stakeEndBlock, stakedAmount, delegationFeeRate uint64) error { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("registering validator stake") + + // Check if the validator was already registered and is active + validator, exists := e.validators[nodeID] + if exists && validator.IsActive { + return ErrValidatorAlreadyRegistered + } + + if exists { + // If validator exists, it's a re-registration, update necessary fields + validator.PublicKey = bls.PublicKeyToBytes(nodePublicKey) // Update public key if needed + validator.StakedAmount += stakedAmount // Adjust the staked amount + validator.DelegationFeeRate = float64(delegationFeeRate) / 100.0 // Update delegation fee rate if needed + validator.stakeStartBlock = stakeStartBlock + validator.stakeEndBlock = stakeEndBlock + // Note: We might want to keep some attributes unchanged, such as delegatorsLastClaim, epochRewards, etc. + } else { + // If validator does not exist, create a new entry + e.validators[nodeID] = &Validator{ + NodeID: nodeID, + PublicKey: bls.PublicKeyToBytes(nodePublicKey), + StakedAmount: stakedAmount, + DelegationFeeRate: float64(delegationFeeRate) / 100.0, // Convert to decimal + delegatorsLastClaim: make(map[codec.Address]uint64), + epochRewards: make(map[uint64]uint64), + stakeStartBlock: stakeStartBlock, + stakeEndBlock: stakeEndBlock, + } + } + + return nil +} + +// WithdrawValidatorStake removes a validator from the heap and updates the total +// staked amount accordingly. +func (e *Manual) WithdrawValidatorStake(nodeID ids.NodeID) (uint64, error) { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("unregistering validator stake") + + // Find the validator + validator, exists := e.validators[nodeID] + if !exists { + return 0, ErrValidatorNotFound + } + + // Validator claiming their rewards and resetting unclaimed rewards + rewardAmount := validator.UnclaimedStakedReward + validator.UnclaimedStakedReward = 0 + + if validator.IsActive { + e.TotalStaked -= validator.StakedAmount + } + + // Mark the validator as inactive + validator.IsActive = false + + // If there are no more delegators, get the rewards and remove the validator + if len(validator.delegatorsLastClaim) == 0 { + rewardAmount += validator.UnclaimedDelegatedReward + validator.UnclaimedDelegatedReward = 0 + e.TotalStaked -= validator.DelegatedAmount + delete(e.validators, nodeID) + } + + return rewardAmount, nil +} + +// DelegateUserStake increases the delegated stake for a validator and rebalances the heap. +func (e *Manual) DelegateUserStake(nodeID ids.NodeID, delegatorAddress codec.Address, stakeStartBlock, stakeAmount uint64) error { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("delegating user stake") + + // Find the validator + validator, exists := e.validators[nodeID] + if !exists { + return ErrValidatorNotFound + } + + // Check if the delegator was already staked + if _, exists := validator.delegatorsLastClaim[delegatorAddress]; exists { + return ErrDelegatorAlreadyStaked + } + + // Update the validator's stake + validator.DelegatedAmount += stakeAmount + + // We only add to total staked amount if the validator is active + // If validator is inactive, we subtract from the total during distributeFees and mintNewNai functions + // This will prevent us from adding to the total staked amount twice + if validator.IsActive { + e.TotalStaked += stakeAmount + } + + // Update the delegator's stake + validator.delegatorsLastClaim[delegatorAddress] = stakeStartBlock + + return nil +} + +// UndelegateUserStake decreases the delegated stake for a validator and rebalances the heap. +func (e *Manual) UndelegateUserStake(nodeID ids.NodeID, actor codec.Address, stakeAmount uint64) (uint64, error) { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("undelegating user stake") + + // Find the validator + validator, exists := e.validators[nodeID] + if !exists { + return 0, ErrValidatorNotFound + } + + // Check if the delegator exists + if _, exists := validator.delegatorsLastClaim[actor]; !exists { + return 0, ErrDelegatorNotFound + } + + // Claim rewards while undelegating + currentBlockHeight := e.GetLastAcceptedBlockHeight() + rewardAmount, err := e.CalculateUserDelegationRewards(nodeID, actor, currentBlockHeight) + if err != nil { + return 0, err + } + validator.delegatorsLastClaim[actor] = currentBlockHeight + validator.UnclaimedDelegatedReward -= rewardAmount // Reset unclaimed rewards + + // Update the validator's stake + validator.DelegatedAmount -= stakeAmount + // We only subtract from total staked amount if the validator is active + // If validator is inactive, we subtract from the total during distributeFees and mintNewNai functions + // This will prevent us from adding to the total staked amount twice + if validator.IsActive { + e.TotalStaked -= stakeAmount + } + + // Remove the delegator's entry + delete(validator.delegatorsLastClaim, actor) + + // If the validator is inactive and has no more delegators, remove the validator + if !validator.IsActive && len(validator.delegatorsLastClaim) == 0 { + delete(e.validators, nodeID) + } + + return rewardAmount, nil +} + +// ClaimStakingRewards lets validators and delegators claim their rewards +func (e *Manual) ClaimStakingRewards(nodeID ids.NodeID, actor codec.Address) (uint64, error) { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("claiming staking rewards") + + // Find the validator + validator, exists := e.validators[nodeID] + if !exists { + return 0, ErrValidatorNotFound + } + + rewardAmount := uint64(0) + if actor == codec.EmptyAddress { + // Validator claiming their rewards + rewardAmount = validator.UnclaimedStakedReward + validator.UnclaimedStakedReward = 0 // Reset unclaimed rewards + + // If there are no more delegators, get the rewards + if len(validator.delegatorsLastClaim) == 0 { + rewardAmount += validator.UnclaimedDelegatedReward + validator.UnclaimedDelegatedReward = 0 + } + } else { + // Delegator claiming their rewards + currentBlockHeight := e.GetLastAcceptedBlockHeight() + reward, err := e.CalculateUserDelegationRewards(nodeID, actor, currentBlockHeight) + if err != nil { + return 0, err + } + validator.delegatorsLastClaim[actor] = currentBlockHeight + validator.UnclaimedDelegatedReward -= reward // Reset unclaimed rewards + rewardAmount = reward + } + + return rewardAmount, nil +} + +func (e *Manual) MintNewNAI() uint64 { + e.lock.Lock() + defer e.lock.Unlock() + + currentBlockHeight := e.GetLastAcceptedBlockHeight() + + // Check if the current block is the end of an epoch + if currentBlockHeight%e.EpochTracker.EpochLength == 0 { + e.c.Logger().Info("minting new NAI tokens at the end of the epoch") + + // Calculate total rewards for the epoch based on APR and staked amount + totalEpochRewards := e.GetRewardsPerEpoch() + + // Calculate rewards per unit staked to minimize iterations + rewardsPerStakeUnit := float64(0) + if e.TotalStaked > 0 { + rewardsPerStakeUnit = float64(totalEpochRewards) / float64(e.TotalStaked) + } + + actualRewards := uint64(0) + + // Distribute rewards based on stake proportion + for _, validator := range e.validators { + lastBlockHeight := e.GetLastAcceptedBlockHeight() + // Mark validator active based on if stakeStartBlock has started + if lastBlockHeight > validator.stakeStartBlock { + validator.IsActive = true + e.TotalStaked += (validator.StakedAmount + validator.DelegatedAmount) + } + if !validator.IsActive { + continue + } + // Mark validator inactive based on if stakeEndBlock has ended + if lastBlockHeight > validator.stakeEndBlock { + validator.IsActive = false + e.TotalStaked -= (validator.StakedAmount + validator.DelegatedAmount) + continue + } + + validatorStake := validator.StakedAmount + validator.DelegatedAmount + totalValidatorReward := uint64(float64(validatorStake) * rewardsPerStakeUnit) + + // Calculate the rewards for the validator and for delegation + validatorReward, delegationReward := uint64(0), uint64(0) + if len(validator.delegatorsLastClaim) > 0 { + validatorReward, delegationReward = distributeValidatorRewards(totalValidatorReward, validator.DelegationFeeRate, validator.DelegatedAmount) + } + + actualRewards += validatorReward + delegationReward + + // Update validator's and delegators' rewards + validator.UnclaimedStakedReward += validatorReward + validator.UnclaimedDelegatedReward += delegationReward + + // Track rewards per epoch for delegation + epochNumber := currentBlockHeight / e.EpochTracker.EpochLength + validator.epochRewards[epochNumber] = delegationReward + } + + // Update the total supply with the new minted rewards + e.TotalSupply += actualRewards + + // Return the total rewards distributed in this epoch + return actualRewards + } + + // No rewards are distributed until the end of the epoch + return 0 +} + +// DistributeFees allocates transaction fees between the emission account and validators, +// based on the total staked amount. +func (e *Manual) DistributeFees(fee uint64) { + e.lock.Lock() + defer e.lock.Unlock() + + e.c.Logger().Info("distributing transaction fees") + + if e.TotalSupply+fee > e.MaxSupply { + fee = e.MaxSupply - e.TotalSupply // Adjust to not exceed max supply + } + + // Give 50% fees to Emission Account + feesForEmission := fee / 2 + e.EmissionAccount.UnclaimedBalance += feesForEmission + + // Give remaining to Validators + feesForValidators := fee - feesForEmission + if e.TotalStaked == 0 || feesForValidators == 0 { + return // No validators or no fees to distribute + } + + // Calculate fees per unit staked to minimize iterations + feesPerStakeUnit := float64(feesForValidators) / float64(e.TotalStaked) + + // Distribute fees based on stake proportion + for _, validator := range e.validators { + lastBlockHeight := e.GetLastAcceptedBlockHeight() + // Mark validator active based on if stakeStartBlock has started + if lastBlockHeight > validator.stakeStartBlock { + validator.IsActive = true + e.TotalStaked += (validator.StakedAmount + validator.DelegatedAmount) + } + if !validator.IsActive { + continue + } + // Mark validator inactive based on if stakeEndBlock has ended + if lastBlockHeight > validator.stakeEndBlock { + validator.IsActive = false + e.TotalStaked -= (validator.StakedAmount + validator.DelegatedAmount) + continue + } + + validatorStake := validator.StakedAmount + validator.DelegatedAmount + totalValidatorFee := uint64(float64(validatorStake) * feesPerStakeUnit) + + validatorFee, delegationFee := uint64(0), uint64(0) + if len(validator.delegatorsLastClaim) > 0 { + validatorFee, delegationFee = distributeValidatorRewards(totalValidatorFee, validator.DelegationFeeRate, validator.DelegatedAmount) + } + validator.UnclaimedStakedReward += validatorFee + validator.UnclaimedDelegatedReward += delegationFee + } +} + +// GetStakedValidator retrieves the details of a specific validator by their NodeID. +func (e *Manual) GetStakedValidator(nodeID ids.NodeID) []*Validator { + e.c.Logger().Info("fetching staked validator") + + if nodeID == ids.EmptyNodeID { + validators := make([]*Validator, 0, len(e.validators)) + for _, validator := range e.validators { + validators = append(validators, validator) + } + return validators + } + + // Find the validator + if validator, exists := e.validators[nodeID]; exists { + return []*Validator{validator} + } + return []*Validator{} +} + +// GetAllValidators fetches the current validators from the underlying VM +func (e *Manual) GetAllValidators(_ context.Context) []*Validator { + e.c.Logger().Info("fetching all staked and unstaked validators") + + for _, v := range e.CurrentValidators { + stakedValidator := e.GetStakedValidator(v.NodeID) + if len(stakedValidator) > 0 { + v.StakedAmount = stakedValidator[0].StakedAmount + v.UnclaimedStakedReward = stakedValidator[0].UnclaimedStakedReward + v.DelegationFeeRate = stakedValidator[0].DelegationFeeRate + v.DelegatedAmount = stakedValidator[0].DelegatedAmount + v.UnclaimedDelegatedReward = stakedValidator[0].UnclaimedDelegatedReward + v.delegatorsLastClaim = stakedValidator[0].delegatorsLastClaim + } + } + return e.CurrentValidators +} + +// GetLastAcceptedBlockTimestamp retrieves the timestamp of the last accepted block from the VM. +func (e *Manual) GetLastAcceptedBlockTimestamp() time.Time { + e.c.Logger().Info("fetching last accepted block timestamp") + return e.nuklaivm.LastAcceptedBlock().Timestamp().UTC() +} + +// GetLastAcceptedBlockHeight retrieves the height of the last accepted block from the VM. +func (e *Manual) GetLastAcceptedBlockHeight() uint64 { + e.c.Logger().Info("fetching last accepted block height") + return e.nuklaivm.LastAcceptedBlock().Height() +} + +func (e *Manual) GetEmissionValidators() map[ids.NodeID]*Validator { + e.c.Logger().Info("fetching emission validators") + return e.validators +} + +func (e *Manual) GetInfo() (emissionAccount EmissionAccount, totalSupply uint64, maxSupply uint64, totalStaked uint64, epochTracker EpochTracker) { + return e.EmissionAccount, e.TotalSupply, e.MaxSupply, e.TotalStaked, e.EpochTracker +} diff --git a/emission/tracker.go b/emission/tracker.go new file mode 100644 index 0000000..0eab6c2 --- /dev/null +++ b/emission/tracker.go @@ -0,0 +1,36 @@ +package emission + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/crypto/bls" +) + +type Tracker interface { + GetStakedValidator(nodeID ids.NodeID) []*Validator + GetAllValidators(ctx context.Context) []*Validator + GetLastAcceptedBlockTimestamp() time.Time + GetLastAcceptedBlockHeight() uint64 + GetEmissionValidators() map[ids.NodeID]*Validator + DistributeFees(fee uint64) + MintNewNAI() uint64 + ClaimStakingRewards(nodeID ids.NodeID, actor codec.Address) (uint64, error) + UndelegateUserStake(nodeID ids.NodeID, actor codec.Address, stakeAmount uint64) (uint64, error) + DelegateUserStake(nodeID ids.NodeID, delegatorAddress codec.Address, stakeStartBlock, stakeAmount uint64) error + WithdrawValidatorStake(nodeID ids.NodeID) (uint64, error) + RegisterValidatorStake(nodeID ids.NodeID, nodePublicKey *bls.PublicKey, stakeStartTime, stakeEndTime, stakedAmount, delegationFeeRate uint64) error + CalculateUserDelegationRewards(nodeID ids.NodeID, actor codec.Address, currentBlockHeight uint64) (uint64, error) + GetRewardsPerEpoch() uint64 + GetAPRForValidators() float64 + GetNumDelegators(nodeID ids.NodeID) int + AddToTotalSupply(amount uint64) uint64 + GetInfo() (emissionAccount EmissionAccount, totalSupply uint64, maxSupply uint64, totalStaked uint64, epochTracker EpochTracker) +} + +// GetEmission returns the singleton instance of Emission +func GetEmission() Tracker { + return emission +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index c78d95f..0e5b89a 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -26,12 +26,14 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/crypto/bls" "github.com/ava-labs/hypersdk/crypto/ed25519" hrpc "github.com/ava-labs/hypersdk/rpc" hutils "github.com/ava-labs/hypersdk/utils" "github.com/nuklai/nuklaivm/actions" "github.com/nuklai/nuklaivm/auth" + "github.com/nuklai/nuklaivm/cmd/nuklai-cli/cmd" nconsts "github.com/nuklai/nuklaivm/consts" nrpc "github.com/nuklai/nuklaivm/rpc" ) @@ -74,7 +76,16 @@ var ( trackSubnetsOpt runner_sdk.OpOption - numValidators uint + numValidators uint + nodesAddresses []codec.Address + nodesFactories []*auth.BLSFactory + delegate string + rdelegate codec.Address + withdraw0 string + rwithdraw0 codec.Address + withdraw0Factory *auth.ED25519Factory + delegateFactory *auth.ED25519Factory + stakeEndBlock uint64 ) func init() { @@ -347,7 +358,9 @@ var _ = ginkgo.BeforeSuite(func() { nodeInfos := status.GetClusterInfo().GetNodeInfos() instancesA = []instance{} - for _, nodeName := range subnetA { + nodesAddresses = make([]codec.Address, len(subnetA)) + nodesFactories = make([]*auth.BLSFactory, len(subnetA)) + for i, nodeName := range subnetA { info := nodeInfos[nodeName] u := fmt.Sprintf("%s/ext/bc/%s", info.Uri, blockchainIDA) bid, err := ids.FromString(blockchainIDA) @@ -380,6 +393,13 @@ var _ = ginkgo.BeforeSuite(func() { destDir := fmt.Sprintf("/tmp/nuklaivm/nodes/%s/", info.GetName()) err = copyNodeInfo(info.GetDbDir(), destDir) gomega.Expect(err).Should(gomega.BeNil()) + + validatorSignerKey, err := cmd.LoadPrivateKey("bls", fmt.Sprintf("%s/signer.key", destDir)) + gomega.Expect(err).Should(gomega.BeNil()) + nodesAddresses[i] = validatorSignerKey.Address + sk, err := bls.PrivateKeyFromBytes(validatorSignerKey.Bytes) + gomega.Expect(err).Should(gomega.BeNil()) + nodesFactories[i] = auth.NewBLSFactory(sk) } if mode != modeRunSingle { @@ -530,7 +550,7 @@ var _ = ginkgo.Describe("[Test]", func() { ginkgo.It("transfer in a single node (raw)", func() { nativeBalance, err := instancesA[0].ncli.Balance(context.TODO(), sender, ids.Empty) gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(nativeBalance).Should(gomega.Equal(startAmount)) + gomega.Ω(nativeBalance).Should(gomega.BeNumerically("<=", startAmount)) other, err := ed25519.GeneratePrivateKey() gomega.Ω(err).Should(gomega.BeNil()) @@ -573,7 +593,8 @@ var _ = ginkgo.Describe("[Test]", func() { sendAmount, balance, ) - gomega.Ω(balance).Should(gomega.Equal(startAmount - fee - sendAmount)) + updatedAmount := nativeBalance - fee - sendAmount + gomega.Ω(balance).Should(gomega.Equal(updatedAmount)) hutils.Outf("{{yellow}}fetched balance{{/}}\n") }) @@ -1506,6 +1527,466 @@ var _ = ginkgo.Describe("[Test]", func() { // TODO: restart all nodes (crisis simulation) }) +var _ = ginkgo.Describe("[Nuklai staking mechanism]", func() { + ginkgo.It("Initial staked validators", func() { + validators, err := instancesA[0].ncli.StakedValidators(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(validators)).Should(gomega.Equal(0)) + }) + ginkgo.It("Funding node 0", func() { + balanceBefore, err := instancesA[0].ncli.Balance(context.TODO(), sender, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + parser, err := instancesA[0].ncli.Parser(context.TODO()) + gomega.Ω(err).Should(gomega.BeNil()) + amount := uint64(200_000_000_000) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: nodesAddresses[0], + Asset: ids.Empty, + Value: amount, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}generated transaction{{/}}\n") + + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, fee, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found transaction %s on B{{/}}\n", tx.ID()) + + balance, err := instancesA[0].ncli.Balance(context.Background(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(200_000_000_000))) + hutils.Outf("{{yellow}}fetched balance{{/}}\n") + + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + balance, err = inst.ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(200_000_000_000))) + hutils.Outf("{{yellow}}fetched balance{{/}}\n") + } + + balanceAfter, err := instancesA[0].ncli.Balance(context.TODO(), sender, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceAfter).Should(gomega.Equal(balanceBefore - fee - amount)) + hutils.Outf("{{yellow}}fetched balance{{/}}\n") + }) + ginkgo.It("Register validator stake node 0", func() { + withdraw0Priv, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + withdraw0Factory = auth.NewED25519Factory(withdraw0Priv) + rwithdraw0 = auth.NewED25519Address(withdraw0Priv.PublicKey()) + withdraw0 = codec.MustAddressBech32(nconsts.HRP, rwithdraw0) + parser, err := instancesA[0].ncli.Parser(context.TODO()) + gomega.Ω(err).Should(gomega.BeNil()) + currentBlockHeight, _, _, _, _, _, _, _ := instancesA[0].ncli.EmissionInfo(context.Background()) + stakeStartBlock := currentBlockHeight + 2 + stakeEndBlock = currentBlockHeight + 100 + delegationFeeRate := 50 + + stakeInfo := &actions.ValidatorStakeInfo{ + NodeID: instancesA[0].nodeID.Bytes(), + StakeStartBlock: stakeStartBlock, + StakeEndBlock: stakeEndBlock, + StakedAmount: 100_000_000_000, + DelegationFeeRate: uint64(delegationFeeRate), + RewardAddress: rwithdraw0, + } + + stakeInfoBytes, err := stakeInfo.Marshal() + gomega.Ω(err).Should(gomega.BeNil()) + signature, err := nodesFactories[0].Sign(stakeInfoBytes) + gomega.Ω(err).Should(gomega.BeNil()) + signaturePacker := codec.NewWriter(signature.Size(), signature.Size()) + signature.Marshal(signaturePacker) + authSignature := signaturePacker.Bytes() + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.RegisterValidatorStake{ + StakeInfo: stakeInfoBytes, + AuthSignature: authSignature, + }, + nodesFactories[0], + ) + gomega.Ω(err).Should(gomega.BeNil()) + + balance, err := instancesA[0].ncli.Balance(context.Background(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + fmt.Printf("node 3 %s balance before staking %d\n", codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), balance) + + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted register validator stake transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, _, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found register validator stake transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(2 * time.Second) + } + + balance, err = instancesA[0].ncli.Balance(context.Background(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + fmt.Printf("node 3 instances[3] %s balance after staking %d\n", codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), balance) + + // check if gossip/ new state happens + balanceOther, err := instancesA[0].ncli.Balance(context.Background(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + fmt.Printf("node 3 instances[4] %s balance after staking %d\n", codec.MustAddressBech32(nconsts.HRP, nodesAddresses[0]), balanceOther) + } + }) + ginkgo.It("Get validator staked amount after node 0 validator staking", func() { + _, _, stakedAmount, _, _, _, err := instancesA[0].ncli.ValidatorStake(context.Background(), instancesA[0].nodeID) + fmt.Printf("NODE 3 STAKED AMOUNT %d", stakedAmount) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(100_000_000_000))) + }) + ginkgo.It("Get staked validators", func() { + validators, err := instancesA[0].ncli.StakedValidators(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(validators)).Should(gomega.Equal(1)) + }) + ginkgo.It("Transfer NAI to delegate user", func() { + delegatePriv, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + rdelegate = auth.NewED25519Address(delegatePriv.PublicKey()) + delegate = codec.MustAddressBech32(nconsts.HRP, rdelegate) + delegateFactory = auth.NewED25519Factory(delegatePriv) + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rdelegate, + Asset: ids.Empty, + Value: 100_000_000_000, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, _, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + + balance, err := inst.ncli.Balance(context.Background(), delegate, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(100_000_000_000))) + } + }) + ginkgo.It("Delegate user stake to node 0", func() { + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.DelegateUserStake{ + NodeID: instancesA[0].nodeID.Bytes(), + StakedAmount: 30_000_000_000, + RewardAddress: rdelegate, + }, + delegateFactory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted delegate user stake transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, _, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found delegate user stake transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + _, stakedAmount, _, _, err := inst.ncli.UserStake(context.Background(), rdelegate, instancesA[0].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(30_000_000_000))) + } + }) + ginkgo.It("Get user stake before claim", func() { + _, stakedAmount, _, _, err := instancesA[0].ncli.UserStake(context.Background(), rdelegate, instancesA[0].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(30_000_000_000))) + }) + ginkgo.It("Claim delegation stake rewards from node 0", func() { + balanceBefore, err := instancesA[0].ncli.Balance(context.Background(), delegate, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + time.Sleep(1 * time.Minute) + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ClaimDelegationStakeRewards{ + NodeID: instancesA[0].nodeID.Bytes(), + UserStakeAddress: rdelegate, + }, + delegateFactory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted claim delegation stake transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, fee, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found claim delegation stake transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(2 * time.Second) + } + balanceAfter, err := inst.ncli.Balance(context.Background(), delegate, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceAfter).Should(gomega.BeNumerically(">", balanceBefore-fee)) + } + }) + ginkgo.It("Undelegate user stake from node 0", func() { + balanceBefore, err := instancesA[0].ncli.Balance(context.Background(), delegate, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.UndelegateUserStake{ + NodeID: instancesA[0].nodeID.Bytes(), + RewardAddress: rdelegate, + }, + delegateFactory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted undelegate user stake transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, fee, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found undelegate user stake transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + balanceAfter, err := inst.ncli.Balance(context.Background(), delegate, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceAfter).Should(gomega.BeNumerically(">", balanceBefore-fee)) + } + }) + ginkgo.It("Transfer NAI to node 0 withdraw address for fees", func() { + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rwithdraw0, + Asset: ids.Empty, + Value: 100_000_000_000, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, _, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found transaction{{/}}\n") + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + balance, err := inst.ncli.Balance(context.Background(), withdraw0, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(100_000_000_000))) + } + }) + ginkgo.It("Claim node 0 stake reward", func() { + balanceBefore, err := instancesA[0].ncli.Balance(context.Background(), withdraw0, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceBefore).Should(gomega.Equal(uint64(100_000_000_000))) + + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ClaimValidatorStakeRewards{ + NodeID: instancesA[0].nodeID.Bytes(), + }, + withdraw0Factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted claim validator stake reward transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, fee, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found claim validator stake reward transaction{{/}}\n") + + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(2 * time.Second) + } + + balanceAfter, err := inst.ncli.Balance(context.Background(), withdraw0, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceAfter).Should(gomega.BeNumerically(">", balanceBefore-fee)) + } + }) + ginkgo.It("Withdraw validator node 0 stake", func() { + for { + currentBlockHeight, _, _, _, _, _, _, _ := instancesA[0].ncli.EmissionInfo(context.Background()) + if stakeEndBlock < currentBlockHeight { + break + } + time.Sleep(2 * time.Second) + } + _, _, _, _, _, _, err := instancesA[0].ncli.ValidatorStake(context.Background(), instancesA[0].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + + parser, err := instancesA[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instancesA[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.WithdrawValidatorStake{ + NodeID: instancesA[0].nodeID.Bytes(), + RewardAddress: rwithdraw0, + }, + nodesFactories[0], + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + hutils.Outf("{{yellow}}submitted withdraw validator node stake transaction{{/}}\n") + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + success, _, err := instancesA[0].ncli.WaitForTransaction(ctx, tx.ID()) + cancel() + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(success).Should(gomega.BeTrue()) + hutils.Outf("{{yellow}}found withdraw validator node stake transaction{{/}}\n") + + for _, inst := range instancesA { + color.Blue("checking %q", inst.uri) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(2 * time.Second) + } + _, _, _, _, _, _, err := instancesA[0].ncli.ValidatorStake(context.Background(), instancesA[0].nodeID) + gomega.Ω(err).ShouldNot(gomega.BeNil()) + validators, err := instancesA[0].ncli.StakedValidators(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(validators)).Should(gomega.Equal(0)) + } + }) +}) + func awaitHealthy(cli runner_sdk.Client) { for { time.Sleep(healthPollInterval) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 963bc8e..a3e614b 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -46,6 +46,7 @@ import ( "github.com/nuklai/nuklaivm/auth" nconsts "github.com/nuklai/nuklaivm/consts" "github.com/nuklai/nuklaivm/controller" + "github.com/nuklai/nuklaivm/emission" "github.com/nuklai/nuklaivm/genesis" nrpc "github.com/nuklai/nuklaivm/rpc" ) @@ -86,7 +87,7 @@ func init() { flag.IntVar( &vms, "vms", - 3, + 5, "number of VMs to create", ) } @@ -122,6 +123,18 @@ var ( networkID uint32 gen *genesis.Genesis + app *appSender + + withdraw0 string + delegate string + rwithdraw0 codec.Address + rdelegate codec.Address + delegateFactory *auth.ED25519Factory + nodesFactories []*auth.BLSFactory + nodesAddresses []codec.Address + emissions []emission.Tracker + nodesPubKeys []*bls.PublicKey + height int ) type instance struct { @@ -175,6 +188,10 @@ var _ = ginkgo.BeforeSuite(func() { // create embedded VMs instances = make([]instance, vms) + nodesFactories = make([]*auth.BLSFactory, vms) + nodesAddresses = make([]codec.Address, vms) + emissions = make([]emission.Tracker, vms) + nodesPubKeys = make([]*bls.PublicKey, vms) gen = genesis.Default() gen.MinUnitPrice = chain.Dimensions{1, 1, 1, 1, 1} @@ -182,7 +199,7 @@ var _ = ginkgo.BeforeSuite(func() { gen.CustomAllocation = []*genesis.CustomAllocation{ { Address: sender, - Balance: 10_000_000, + Balance: 10_000_000_000_000_000, }, } gen.EmissionBalancer = genesis.EmissionBalancer{ @@ -197,7 +214,7 @@ var _ = ginkgo.BeforeSuite(func() { subnetID := ids.GenerateTestID() chainID := ids.GenerateTestID() - app := &appSender{} + app = &appSender{} for i := range instances { nodeID := ids.GenerateTestNodeID() sk, err := bls.NewSecretKey() @@ -218,6 +235,9 @@ var _ = ginkgo.BeforeSuite(func() { WarpSigner: warp.NewSigner(sk, networkID, chainID), ValidatorState: &validators.TestState{}, } + nodesFactories[i] = auth.NewBLSFactory(sk) + nodesAddresses[i] = auth.NewBLSAddress(snowCtx.PublicKey) + nodesPubKeys[i] = snowCtx.PublicKey toEngine := make(chan common.Message, 1) db := memdb.New() @@ -242,6 +262,8 @@ var _ = ginkgo.BeforeSuite(func() { hd, err = v.CreateHandlers(context.TODO()) gomega.Ω(err).Should(gomega.BeNil()) + emissions[i] = emission.GetEmission() + hjsonRPCServer := httptest.NewServer(hd[hrpc.JSONRPCEndpoint]) njsonRPCServer := httptest.NewServer(hd[nrpc.JSONRPCEndpoint]) webSocketServer := httptest.NewServer(hd[hrpc.WebSocketEndpoint]) @@ -287,6 +309,8 @@ var _ = ginkgo.BeforeSuite(func() { } blocks = []snowman.Block{} + setEmissionValidators() + app.instances = instances color.Blue("created %d VMs", vms) }) @@ -325,1227 +349,1660 @@ var _ = ginkgo.Describe("[Network]", func() { }) }) -var _ = ginkgo.Describe("[Tx Processing]", func() { - ginkgo.It("get currently accepted block ID", func() { - for _, inst := range instances { - hcli := inst.hcli - _, _, _, err := hcli.Accepted(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - } - }) - - var transferTxRoot *chain.Transaction - ginkgo.It("Gossip TransferTx to a different node", func() { - ginkgo.By("issue TransferTx", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, transferTx, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.Transfer{ - To: rsender2, - Value: 100_000, // must be more than StateLockup - }, - factory, - ) - transferTxRoot = transferTx - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - gomega.Ω(instances[0].vm.Mempool().Len(context.Background())).Should(gomega.Equal(1)) - }) - - ginkgo.By("skip duplicate", func() { - _, err := instances[0].hcli.SubmitTx( - context.Background(), - transferTxRoot.Bytes(), - ) - gomega.Ω(err).To(gomega.Not(gomega.BeNil())) - }) - - ginkgo.By("send gossip from node 0 to 1", func() { - err := instances[0].vm.Gossiper().Force(context.TODO()) - gomega.Ω(err).Should(gomega.BeNil()) - }) - - ginkgo.By("skip invalid time", func() { - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: 0, - MaxFee: 1000, - }, - nil, - &actions.Transfer{ - To: rsender2, - Value: 110, - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // 0 timestamp) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err).To(gomega.Not(gomega.BeNil())) - }) - - ginkgo.By("skip duplicate (after gossip, which shouldn't clear)", func() { - _, err := instances[0].hcli.SubmitTx( - context.Background(), - transferTxRoot.Bytes(), - ) - gomega.Ω(err).To(gomega.Not(gomega.BeNil())) - }) - - ginkgo.By("receive gossip in the node 1, and signal block build", func() { - gomega.Ω(instances[1].vm.Builder().Force(context.TODO())).To(gomega.BeNil()) - <-instances[1].toEngine - }) - - ginkgo.By("build block in the node 1", func() { - ctx := context.TODO() - blk, err := instances[1].vm.BuildBlock(ctx) - gomega.Ω(err).To(gomega.BeNil()) - - gomega.Ω(blk.Verify(ctx)).To(gomega.BeNil()) - gomega.Ω(blk.Status()).To(gomega.Equal(choices.Processing)) - - err = instances[1].vm.SetPreference(ctx, blk.ID()) - gomega.Ω(err).To(gomega.BeNil()) - - gomega.Ω(blk.Accept(ctx)).To(gomega.BeNil()) - gomega.Ω(blk.Status()).To(gomega.Equal(choices.Accepted)) - blocks = append(blocks, blk) - - lastAccepted, err := instances[1].vm.LastAccepted(ctx) - gomega.Ω(err).To(gomega.BeNil()) - gomega.Ω(lastAccepted).To(gomega.Equal(blk.ID())) - - results := blk.(*chain.StatelessBlock).Results() - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - gomega.Ω(results[0].Output).Should(gomega.BeNil()) - - // Unit explanation - // - // bandwidth: tx size - // compute: 5 for signature, 1 for base, 1 for transfer - // read: 2 keys reads, 1 had 0 chunks - // allocate: 1 key created - // write: 1 key modified, 1 key new - transferTxConsumed := chain.Dimensions{227, 7, 12, 25, 26} - gomega.Ω(results[0].Consumed).Should(gomega.Equal(transferTxConsumed)) - - // Fee explanation - // - // Multiply all unit consumption by 1 and sum - gomega.Ω(results[0].Fee).Should(gomega.Equal(uint64(297))) - }) - - ginkgo.By("ensure balance is updated", func() { - balance, err := instances[1].ncli.Balance(context.Background(), sender, ids.Empty) - gomega.Ω(err).To(gomega.BeNil()) - gomega.Ω(balance).To(gomega.Equal(uint64(9899703))) - balance2, err := instances[1].ncli.Balance(context.Background(), sender2, ids.Empty) - gomega.Ω(err).To(gomega.BeNil()) - gomega.Ω(balance2).To(gomega.Equal(uint64(100000))) - }) - }) - - ginkgo.It("ensure multiple txs work ", func() { - ginkgo.By("transfer funds again", func() { - parser, err := instances[1].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[1].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.Transfer{ - To: rsender2, - Value: 101, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - time.Sleep(2 * time.Second) // for replay test - accept := expectBlk(instances[1]) - results := accept(true) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - - balance2, err := instances[1].ncli.Balance(context.Background(), sender2, ids.Empty) - gomega.Ω(err).To(gomega.BeNil()) - gomega.Ω(balance2).To(gomega.Equal(uint64(100101))) - }) - }) - - ginkgo.It("Test processing block handling", func() { - var accept, accept2 func(bool) []*chain.Result - - ginkgo.By("create processing tip", func() { - parser, err := instances[1].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[1].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.Transfer{ - To: rsender2, - Value: 200, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - time.Sleep(2 * time.Second) // for replay test - accept = expectBlk(instances[1]) - - submit, _, _, err = instances[1].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.Transfer{ - To: rsender2, - Value: 201, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - time.Sleep(2 * time.Second) // for replay test - accept2 = expectBlk(instances[1]) - }) - - ginkgo.By("clear processing tip", func() { - results := accept(true) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - results = accept2(true) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - }) - }) - - ginkgo.It("ensure mempool works", func() { - ginkgo.By("fail Gossip TransferTx to a stale node when missing previous blocks", func() { - parser, err := instances[1].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[1].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.Transfer{ - To: rsender2, - Value: 203, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - - err = instances[1].vm.Gossiper().Force(context.TODO()) - gomega.Ω(err).Should(gomega.BeNil()) - - // mempool in 0 should be 1 (old amount), since gossip/submit failed - gomega.Ω(instances[0].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) - }) - }) - - ginkgo.It("ensure unprocessed tip and replay protection works", func() { - ginkgo.By("import accepted blocks to instance 2", func() { - ctx := context.TODO() - - gomega.Ω(blocks[0].Height()).Should(gomega.Equal(uint64(1))) - - n := instances[2] - blk1, err := n.vm.ParseBlock(ctx, blocks[0].Bytes()) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk1.Verify(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - - // Parse tip - blk2, err := n.vm.ParseBlock(ctx, blocks[1].Bytes()) - gomega.Ω(err).Should(gomega.BeNil()) - blk3, err := n.vm.ParseBlock(ctx, blocks[2].Bytes()) - gomega.Ω(err).Should(gomega.BeNil()) - - // Verify tip - err = blk2.Verify(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk3.Verify(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - - // Check if tx from old block would be considered a repeat on processing tip - tx := blk2.(*chain.StatelessBlock).Txs[0] - sblk3 := blk3.(*chain.StatelessBlock) - sblk3t := sblk3.Timestamp().UnixMilli() - ok, err := sblk3.IsRepeat(ctx, sblk3t-n.vm.Rules(sblk3t).GetValidityWindow(), []*chain.Transaction{tx}, set.NewBits(), false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(ok.Len()).Should(gomega.Equal(1)) - - // Accept tip - err = blk1.Accept(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk2.Accept(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk3.Accept(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - - // Parse another - blk4, err := n.vm.ParseBlock(ctx, blocks[3].Bytes()) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk4.Verify(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - err = blk4.Accept(ctx) - gomega.Ω(err).Should(gomega.BeNil()) - - // Check if tx from old block would be considered a repeat on accepted tip - time.Sleep(2 * time.Second) - gomega.Ω(n.vm.IsRepeat(ctx, []*chain.Transaction{tx}, set.NewBits(), false).Len()).Should(gomega.Equal(1)) - }) - }) - - ginkgo.It("processes valid index transactions (w/block listening)", func() { - // Clear previous txs on instance 0 - accept := expectBlk(instances[0]) - accept(false) // don't care about results - - // Subscribe to blocks - hcli, err := hrpc.NewWebSocketClient(instances[0].WebSocketServer.URL, hrpc.DefaultHandshakeTimeout, pubsub.MaxPendingMessages, pubsub.MaxReadMessageSize) +var _ = ginkgo.Describe("[Nuklai staking mechanism]", func() { + ginkgo.It("Setup and get initial staked validators", func() { + height = 0 + withdraw0Priv, err := ed25519.GeneratePrivateKey() gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(hcli.RegisterBlocks()).Should(gomega.BeNil()) - - // Wait for message to be sent - time.Sleep(2 * pubsub.MaxMessageWait) + rwithdraw0 = auth.NewED25519Address(withdraw0Priv.PublicKey()) + withdraw0 = codec.MustAddressBech32(nconsts.HRP, rwithdraw0) - // Fetch balances - balance, err := instances[0].ncli.Balance(context.TODO(), sender, ids.Empty) + delegatePriv, err := ed25519.GeneratePrivateKey() gomega.Ω(err).Should(gomega.BeNil()) + rdelegate = auth.NewED25519Address(delegatePriv.PublicKey()) + delegate = codec.MustAddressBech32(nconsts.HRP, rdelegate) + delegateFactory = auth.NewED25519Factory(delegatePriv) - // Send tx - other, err := ed25519.GeneratePrivateKey() + validators, err := instances[3].ncli.StakedValidators(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - transfer := &actions.Transfer{ - To: auth.NewED25519Address(other.PublicKey()), - Value: 1, - } + gomega.Ω(len(validators)).Should(gomega.Equal(0)) + }) - parser, err := instances[0].ncli.Parser(context.Background()) + ginkgo.It("Funding node 3", func() { + parser, err := instances[3].ncli.Parser(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( + submit, _, _, err := instances[3].hcli.GenerateTransaction( context.Background(), parser, nil, - transfer, + &actions.Transfer{ + To: nodesAddresses[3], + Asset: ids.Empty, + Value: 200_000_000_000, + }, factory, ) gomega.Ω(err).Should(gomega.BeNil()) gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) - gomega.Ω(err).Should(gomega.BeNil()) - accept = expectBlk(instances[0]) - results := accept(false) + accept := expectBlk(instances[3]) + results := accept(true) gomega.Ω(results).Should(gomega.HaveLen(1)) gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - // Read item from connection - blk, lresults, prices, err := hcli.ListenBlock(context.TODO(), parser) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(len(blk.Txs)).Should(gomega.Equal(1)) - tx := blk.Txs[0].Action.(*actions.Transfer) - gomega.Ω(tx.Asset).To(gomega.Equal(ids.Empty)) - gomega.Ω(tx.Value).To(gomega.Equal(uint64(1))) - gomega.Ω(lresults).Should(gomega.Equal(results)) - gomega.Ω(prices).Should(gomega.Equal(chain.Dimensions{1, 1, 1, 1, 1})) - - // Check balance modifications are correct - balancea, err := instances[0].ncli.Balance(context.TODO(), sender, ids.Empty) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(balancea + lresults[0].Fee + 1)) + gomega.Ω(len(blocks)).Should(gomega.Equal(1)) - // Close connection when done - gomega.Ω(hcli.Close()).Should(gomega.BeNil()) - }) + blk := blocks[height] + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + balance, err := instances[3].ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[3]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(200_000_000_000))) - ginkgo.It("processes valid index transactions (w/streaming verification)", func() { - // Create streaming client - hcli, err := hrpc.NewWebSocketClient(instances[0].WebSocketServer.URL, hrpc.DefaultHandshakeTimeout, pubsub.MaxPendingMessages, pubsub.MaxReadMessageSize) + // check if gossip/ new state happens + balanceOther, err := instances[4].ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[3]), ids.Empty) gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balanceOther).Should(gomega.Equal(uint64(200_000_000_000))) - // Create tx - other, err := ed25519.GeneratePrivateKey() + balance, err = instances[3].ncli.Balance(context.TODO(), sender, ids.Empty) gomega.Ω(err).Should(gomega.BeNil()) - transfer := &actions.Transfer{ - To: auth.NewED25519Address(other.PublicKey()), - Value: 1, + gomega.Ω(balance).Should(gomega.Equal(uint64(9_999_799_999_999_703))) + }) + ginkgo.It("Register validator stake node 3", func() { + parser, err := instances[3].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + currentBlockHeight := instances[3].vm.LastAcceptedBlock().Height() + stakeStartBlock := currentBlockHeight + 2 + stakeEndBlock := currentBlockHeight + 100 + delegationFeeRate := 50 + + stakeInfo := &actions.ValidatorStakeInfo{ + NodeID: instances[3].nodeID.Bytes(), + StakeStartBlock: stakeStartBlock, + StakeEndBlock: stakeEndBlock, + StakedAmount: 100_000_000_000, + DelegationFeeRate: uint64(delegationFeeRate), + RewardAddress: rwithdraw0, } - parser, err := instances[0].ncli.Parser(context.Background()) + + stakeInfoBytes, err := stakeInfo.Marshal() gomega.Ω(err).Should(gomega.BeNil()) - _, tx, _, err := instances[0].hcli.GenerateTransaction( + signature, err := nodesFactories[3].Sign(stakeInfoBytes) + gomega.Ω(err).Should(gomega.BeNil()) + signaturePacker := codec.NewWriter(signature.Size(), signature.Size()) + signature.Marshal(signaturePacker) + authSignature := signaturePacker.Bytes() + submit, _, _, err := instances[3].hcli.GenerateTransaction( context.Background(), parser, nil, - transfer, - factory, + &actions.RegisterValidatorStake{ + StakeInfo: stakeInfoBytes, + AuthSignature: authSignature, + }, + nodesFactories[3], ) gomega.Ω(err).Should(gomega.BeNil()) - // Submit tx and accept block - gomega.Ω(hcli.RegisterTx(tx)).Should(gomega.BeNil()) + _, err = instances[3].ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[3]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) - // Wait for message to be sent - time.Sleep(2 * pubsub.MaxMessageWait) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) - for instances[0].vm.Mempool().Len(context.TODO()) == 0 { - // We need to wait for mempool to be populated because issuance will - // return as soon as bytes are on the channel. - hutils.Outf("{{yellow}}waiting for mempool to return non-zero txs{{/}}\n") - time.Sleep(500 * time.Millisecond) - } - gomega.Ω(err).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) + accept := expectBlk(instances[3]) + results := accept(true) gomega.Ω(results).Should(gomega.HaveLen(1)) gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - // Read decision from connection - txID, dErr, result, err := hcli.ListenTx(context.TODO()) + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + _, err = instances[3].ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[3]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + + // check if gossip/ new state happens + _, err = instances[4].ncli.Balance(context.TODO(), codec.MustAddressBech32(nconsts.HRP, nodesAddresses[3]), ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + + emissionInstance := emissions[3] + currentValidators := emissionInstance.GetAllValidators(context.TODO()) + gomega.Ω(len(currentValidators)).To(gomega.Equal(5)) + stakedValidator := emissionInstance.GetStakedValidator(instances[3].nodeID) + gomega.Ω(len(stakedValidator)).To(gomega.Equal(1)) + + validator, exists := emissions[3].GetEmissionValidators()[instances[3].nodeID] + gomega.Ω(exists).To(gomega.Equal(true)) + + // check when it becomes active ? + gomega.Ω(validator.IsActive).To(gomega.Equal(false)) + + // test same block is accepted + lastAcceptedBlock3, err := instances[3].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + lastAcceptedBlock4, err := instances[4].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(lastAcceptedBlock3).To(gomega.Equal(lastAcceptedBlock4)) + + validators, err := instances[4].ncli.StakedValidators(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(validators)).Should(gomega.Equal(1)) + }) + + ginkgo.It("Get validator staked amount after node 3 validator staking", func() { + _, _, stakedAmount, _, _, _, err := instances[3].ncli.ValidatorStake(context.Background(), instances[3].nodeID) gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(txID).Should(gomega.Equal(tx.ID())) - gomega.Ω(dErr).Should(gomega.BeNil()) - gomega.Ω(result.Success).Should(gomega.BeTrue()) - gomega.Ω(result).Should(gomega.Equal(results[0])) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(100_000_000_000))) + }) - // Close connection when done - gomega.Ω(hcli.Close()).Should(gomega.BeNil()) + ginkgo.It("Get validator staked amount after staking using node 0 cli", func() { + _, _, stakedAmount, _, _, _, err := instances[0].ncli.ValidatorStake(context.Background(), instances[3].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(100_000_000_000))) }) - ginkgo.It("transfer an asset with a memo", func() { - other, err := ed25519.GeneratePrivateKey() + ginkgo.It("Get staked validators", func() { + validators, err := instances[4].ncli.StakedValidators(context.TODO()) gomega.Ω(err).Should(gomega.BeNil()) - parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(len(validators)).Should(gomega.Equal(1)) + }) + + ginkgo.It("Transfer NAI to delegate user", func() { + parser, err := instances[3].ncli.Parser(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( + submit, _, _, err := instances[3].hcli.GenerateTransaction( context.Background(), parser, nil, &actions.Transfer{ - To: auth.NewED25519Address(other.PublicKey()), - Value: 10, - Memo: []byte("hello"), + To: rdelegate, + Asset: ids.Empty, + Value: 100_000_000_000, }, factory, ) gomega.Ω(err).Should(gomega.BeNil()) gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + + accept := expectBlk(instances[3]) + results := accept(true) gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeTrue()) - }) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - ginkgo.It("transfer an asset with large memo", func() { - other, err := ed25519.GeneratePrivateKey() + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + balance, err := instances[0].ncli.Balance(context.TODO(), delegate, ids.Empty) gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1001, - }, - nil, - &actions.Transfer{ - To: auth.NewED25519Address(other.PublicKey()), - Value: 10, - Memo: make([]byte, 1000), - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // too large) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("size is larger than limit")) - }) + gomega.Ω(balance).Should(gomega.Equal(uint64(100_000_000_000))) - ginkgo.It("mint an asset that doesn't exist", func() { - other, err := ed25519.GeneratePrivateKey() + // test same block is accepted + lastAcceptedBlock3, err := instances[3].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + lastAcceptedBlock4, err := instances[4].vm.LastAccepted(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - assetID := ids.GenerateTestID() - parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(lastAcceptedBlock3).To(gomega.Equal(lastAcceptedBlock4)) + }) + + ginkgo.It("Delegate user stake to node 3", func() { + parser, err := instances[3].ncli.Parser(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( + submit, _, _, err := instances[3].hcli.GenerateTransaction( context.Background(), parser, nil, - &actions.MintAsset{ - To: auth.NewED25519Address(other.PublicKey()), - Asset: assetID, - Value: 10, + &actions.DelegateUserStake{ + NodeID: instances[3].nodeID.Bytes(), + StakedAmount: 30_000_000_000, + RewardAddress: rdelegate, }, - factory, + delegateFactory, ) gomega.Ω(err).Should(gomega.BeNil()) gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + + accept := expectBlk(instances[3]) + results := accept(true) gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)). - Should(gomega.ContainSubstring("asset missing")) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - exists, _, _, _, _, _, _, err := instances[0].ncli.Asset(context.TODO(), assetID, false) + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + fmt.Printf("delegate stake to node 3 %d", height) + + blk := blocks[height] + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + height++ + + // test same block is accepted + lastAcceptedBlock3, err := instances[3].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + lastAcceptedBlock4, err := instances[4].vm.LastAccepted(context.Background()) gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeFalse()) + gomega.Ω(lastAcceptedBlock3).To(gomega.Equal(lastAcceptedBlock4)) }) - ginkgo.It("create a new asset (no metadata)", func() { - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1001, - }, - nil, - &actions.CreateAsset{ - Symbol: []byte("s0"), - Decimals: 0, - Metadata: nil, - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // too large) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Bytes field is not populated")) - }) + // TODO: GetUserStakeFromState is returning an empty value + // TODO: transactions are played twice because of Verify and Accept (block is already processed) + /* + ginkgo.FIt("Get user stake before claim", func() { + for _, inst := range instances { + color.Blue("checking %q", inst.nodeID) + + // Ensure all blocks processed + for { + _, h, _, err := inst.hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + if h > 0 { + break + } + time.Sleep(1 * time.Second) + } + _, stakedAmount, _, _, err := inst.ncli.UserStake(context.Background(), rdelegate, instances[3].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(uint64(30_000_000_000))) + + } + }) + + ginkgo.It("Claim delegation stake rewards from node 3", func() { + parser, err := instances[3].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[3].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ClaimDelegationStakeRewards{ + NodeID: instances[3].nodeID.Bytes(), + UserStakeAddress: rdelegate, + }, + delegateFactory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + + accept := expectBlk(instances[3]) + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + fmt.Println(blk.ID()) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + // test same block is accepted + lastAcceptedBlock3, err := instances[3].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + lastAcceptedBlock4, err := instances[4].vm.LastAccepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(lastAcceptedBlock3).To(gomega.Equal(lastAcceptedBlock4)) + }) + + ginkgo.It("Get user stake after claim", func() { + _, stakedAmount, _, _, err := instances[3].ncli.UserStake(context.Background(), rdelegate, instances[0].nodeID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(stakedAmount).Should(gomega.Equal(0)) + }) + + ginkgo.It("Undelegate user stake from node 3", func() { + parser, err := instances[3].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[3].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.UndelegateUserStake{ + NodeID: instances[3].nodeID.Bytes(), + RewardAddress: rdelegate, + }, + delegateFactory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + + accept := expectBlk(instances[3]) + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + fmt.Println(blk.ID()) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + }) + + ginkgo.It("Claim validator node 0 stake reward", func() { + parser, err := instances[3].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[3].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ClaimValidatorStakeRewards{ + NodeID: instances[3].nodeID.Bytes(), + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[3].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + + accept := expectBlk(instances[3]) + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + fmt.Println(blk.ID()) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + gomega.Ω(instances[3].ncli.Balance(context.Background(), withdraw0, ids.Empty)).Should(gomega.BeNumerically(">", 0)) + }) + + ginkgo.It("Withdraw validator node 0 stake", func() { + parser, err := instances[3].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[3].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.WithdrawValidatorStake{ + NodeID: instances[3].nodeID.Bytes(), + RewardAddress: rwithdraw0, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + + accept := expectBlk(instances[3]) + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + gomega.Ω(len(blocks)).Should(gomega.Equal(height + 1)) + + blk := blocks[height] + fmt.Println(blk.ID()) + ImportBlockToInstance(instances[4].vm, blk) + ImportBlockToInstance(instances[0].vm, blk) + ImportBlockToInstance(instances[2].vm, blk) + ImportBlockToInstance(instances[1].vm, blk) + height++ + + }) + + ginkgo.It("Get staked validators after staking withdraw ", func() { + validators, err := instances[0].ncli.StakedValidators(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(validators)).Should(gomega.Equal(0)) + }) + + }) */ + + _ = ginkgo.Describe("[Tx Processing]", func() { + ginkgo.It("get currently accepted block ID", func() { + for _, inst := range instances { + hcli := inst.hcli + _, _, _, err := hcli.Accepted(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + } + }) - ginkgo.It("create a new asset (no symbol)", func() { - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1001, - }, - nil, - &actions.CreateAsset{ - Symbol: nil, - Decimals: 0, - Metadata: []byte("m"), - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // too large) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Bytes field is not populated")) - }) + var transferTxRoot *chain.Transaction + ginkgo.It("Gossip TransferTx to a different node", func() { + ginkgo.By("issue TransferTx", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, transferTx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rsender2, + Value: 100_000, // must be more than StateLockup + }, + factory, + ) + transferTxRoot = transferTx + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + gomega.Ω(instances[0].vm.Mempool().Len(context.Background())).Should(gomega.Equal(1)) + }) + + ginkgo.By("skip duplicate", func() { + _, err := instances[0].hcli.SubmitTx( + context.Background(), + transferTxRoot.Bytes(), + ) + gomega.Ω(err).To(gomega.Not(gomega.BeNil())) + }) + + ginkgo.By("send gossip from node 0 to 1", func() { + err := instances[0].vm.Gossiper().Force(context.TODO()) + gomega.Ω(err).Should(gomega.BeNil()) + }) + + ginkgo.By("skip invalid time", func() { + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: 0, + MaxFee: 1000, + }, + nil, + &actions.Transfer{ + To: rsender2, + Value: 110, + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // 0 timestamp) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err).To(gomega.Not(gomega.BeNil())) + }) + + ginkgo.By("skip duplicate (after gossip, which shouldn't clear)", func() { + _, err := instances[0].hcli.SubmitTx( + context.Background(), + transferTxRoot.Bytes(), + ) + gomega.Ω(err).To(gomega.Not(gomega.BeNil())) + }) + + ginkgo.By("receive gossip in the node 1, and signal block build", func() { + gomega.Ω(instances[1].vm.Builder().Force(context.TODO())).To(gomega.BeNil()) + <-instances[1].toEngine + }) + + ginkgo.By("build block in the node 1", func() { + ctx := context.TODO() + blk, err := instances[1].vm.BuildBlock(ctx) + gomega.Ω(err).To(gomega.BeNil()) + + gomega.Ω(blk.Verify(ctx)).To(gomega.BeNil()) + gomega.Ω(blk.Status()).To(gomega.Equal(choices.Processing)) + + err = instances[1].vm.SetPreference(ctx, blk.ID()) + gomega.Ω(err).To(gomega.BeNil()) + + gomega.Ω(blk.Accept(ctx)).To(gomega.BeNil()) + gomega.Ω(blk.Status()).To(gomega.Equal(choices.Accepted)) + blocks = append(blocks, blk) + + lastAccepted, err := instances[1].vm.LastAccepted(ctx) + gomega.Ω(err).To(gomega.BeNil()) + gomega.Ω(lastAccepted).To(gomega.Equal(blk.ID())) + + results := blk.(*chain.StatelessBlock).Results() + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + gomega.Ω(results[0].Output).Should(gomega.BeNil()) + + // Unit explanation + // + // bandwidth: tx size + // compute: 5 for signature, 1 for base, 1 for transfer + // read: 2 keys reads, 1 had 0 chunks + // allocate: 1 key created + // write: 1 key modified, 1 key new + transferTxConsumed := chain.Dimensions{227, 7, 12, 25, 26} + gomega.Ω(results[0].Consumed).Should(gomega.Equal(transferTxConsumed)) + + // Fee explanation + // + // Multiply all unit consumption by 1 and sum + gomega.Ω(results[0].Fee).Should(gomega.Equal(uint64(297))) + }) + + ginkgo.By("ensure balance is updated", func() { + balance, err := instances[1].ncli.Balance(context.Background(), sender, ids.Empty) + gomega.Ω(err).To(gomega.BeNil()) + gomega.Ω(balance).To(gomega.Equal(uint64(9999699999899109))) + balance2, err := instances[1].ncli.Balance(context.Background(), sender2, ids.Empty) + gomega.Ω(err).To(gomega.BeNil()) + gomega.Ω(balance2).To(gomega.Equal(uint64(100000))) + }) + }) - ginkgo.It("create asset with too long of metadata", func() { - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - nil, - &actions.CreateAsset{ - Symbol: []byte("s0"), - Decimals: 0, - Metadata: make([]byte, actions.MaxMetadataSize*2), - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // too large) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("size is larger than limit")) - }) + ginkgo.It("ensure multiple txs work ", func() { + ginkgo.By("transfer funds again", func() { + parser, err := instances[1].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[1].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rsender2, + Value: 101, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + time.Sleep(2 * time.Second) // for replay test + accept := expectBlk(instances[1]) + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + balance2, err := instances[1].ncli.Balance(context.Background(), sender2, ids.Empty) + gomega.Ω(err).To(gomega.BeNil()) + gomega.Ω(balance2).To(gomega.Equal(uint64(100101))) + }) + }) - ginkgo.It("create a new asset (simple metadata)", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, tx, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.CreateAsset{ - Symbol: asset1Symbol, - Decimals: asset1Decimals, - Metadata: asset1, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + ginkgo.It("Test processing block handling", func() { + var accept, accept2 func(bool) []*chain.Result + + ginkgo.By("create processing tip", func() { + parser, err := instances[1].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[1].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rsender2, + Value: 200, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + time.Sleep(2 * time.Second) // for replay test + accept = expectBlk(instances[1]) + + submit, _, _, err = instances[1].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rsender2, + Value: 201, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + time.Sleep(2 * time.Second) // for replay test + accept2 = expectBlk(instances[1]) + }) + + ginkgo.By("clear processing tip", func() { + results := accept(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + results = accept2(true) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + }) + }) - asset1ID = tx.ID() - balance, err := instances[0].ncli.Balance(context.TODO(), sender, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + ginkgo.It("ensure mempool works", func() { + ginkgo.By("fail Gossip TransferTx to a stale node when missing previous blocks", func() { + parser, err := instances[1].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[1].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: rsender2, + Value: 203, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + + err = instances[1].vm.Gossiper().Force(context.TODO()) + gomega.Ω(err).Should(gomega.BeNil()) + + // mempool in 0 should be 1 (old amount), since gossip/submit failed + gomega.Ω(instances[0].vm.Mempool().Len(context.TODO())).Should(gomega.Equal(1)) + }) + }) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(0))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + ginkgo.It("ensure unprocessed tip and replay protection works", func() { + ginkgo.By("import accepted blocks to instance 2", func() { + ctx := context.TODO() + + gomega.Ω(blocks[0].Height()).Should(gomega.Equal(uint64(1))) + + n := instances[2] + blk1, err := n.vm.ParseBlock(ctx, blocks[4].Bytes()) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk1.Verify(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + + // Parse tip + blk2, err := n.vm.ParseBlock(ctx, blocks[5].Bytes()) + gomega.Ω(err).Should(gomega.BeNil()) + blk3, err := n.vm.ParseBlock(ctx, blocks[6].Bytes()) + gomega.Ω(err).Should(gomega.BeNil()) + + // Verify tip + err = blk2.Verify(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk3.Verify(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + + // Check if tx from old block would be considered a repeat on processing tip + tx := blk2.(*chain.StatelessBlock).Txs[0] + sblk3 := blk3.(*chain.StatelessBlock) + sblk3t := sblk3.Timestamp().UnixMilli() + ok, err := sblk3.IsRepeat(ctx, sblk3t-n.vm.Rules(sblk3t).GetValidityWindow(), []*chain.Transaction{tx}, set.NewBits(), false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(ok.Len()).Should(gomega.Equal(1)) + + // Accept tip + err = blk1.Accept(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk2.Accept(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk3.Accept(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + + // Parse another + blk4, err := n.vm.ParseBlock(ctx, blocks[7].Bytes()) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk4.Verify(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk4.Accept(ctx) + gomega.Ω(err).Should(gomega.BeNil()) + + // Check if tx from old block would be considered a repeat on accepted tip + time.Sleep(2 * time.Second) + gomega.Ω(n.vm.IsRepeat(ctx, []*chain.Transaction{tx}, set.NewBits(), false).Len()).Should(gomega.Equal(1)) + }) + }) - ginkgo.It("mint a new asset", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.MintAsset{ - To: rsender2, - Asset: asset1ID, - Value: 15, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + ginkgo.It("processes valid index transactions (w/block listening)", func() { + // Clear previous txs on instance 0 + accept := expectBlk(instances[0]) + accept(false) // don't care about results - balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(15))) - balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + // Subscribe to blocks + hcli, err := hrpc.NewWebSocketClient(instances[0].WebSocketServer.URL, hrpc.DefaultHandshakeTimeout, pubsub.MaxPendingMessages, pubsub.MaxReadMessageSize) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(hcli.RegisterBlocks()).Should(gomega.BeNil()) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(15))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + // Wait for message to be sent + time.Sleep(2 * pubsub.MaxMessageWait) - ginkgo.It("mint asset from wrong owner", func() { - other, err := ed25519.GeneratePrivateKey() - gomega.Ω(err).Should(gomega.BeNil()) - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.MintAsset{ - To: auth.NewED25519Address(other.PublicKey()), - Asset: asset1ID, - Value: 10, - }, - factory2, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)). - Should(gomega.ContainSubstring("wrong owner")) + // Fetch balances + balance, err := instances[0].ncli.Balance(context.TODO(), sender, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(15))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + // Send tx + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + transfer := &actions.Transfer{ + To: auth.NewED25519Address(other.PublicKey()), + Value: 1, + } - ginkgo.It("burn new asset", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.BurnAsset{ - Asset: asset1ID, - Value: 5, - }, - factory2, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + transfer, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(10))) - balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + gomega.Ω(err).Should(gomega.BeNil()) + accept = expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(10))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + // Read item from connection + blk, lresults, prices, err := hcli.ListenBlock(context.TODO(), parser) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(len(blk.Txs)).Should(gomega.Equal(1)) + tx := blk.Txs[0].Action.(*actions.Transfer) + gomega.Ω(tx.Asset).To(gomega.Equal(ids.Empty)) + gomega.Ω(tx.Value).To(gomega.Equal(uint64(1))) + gomega.Ω(lresults).Should(gomega.Equal(results)) + gomega.Ω(prices).Should(gomega.Equal(chain.Dimensions{1, 1, 1, 1, 1})) + + // Check balance modifications are correct + balancea, err := instances[0].ncli.Balance(context.TODO(), sender, ids.Empty) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(balancea + lresults[0].Fee + 1)) - ginkgo.It("burn missing asset", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.BurnAsset{ - Asset: asset1ID, - Value: 10, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)). - Should(gomega.ContainSubstring("invalid balance")) + // Close connection when done + gomega.Ω(hcli.Close()).Should(gomega.BeNil()) + }) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(10))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + ginkgo.It("processes valid index transactions (w/streaming verification)", func() { + // Create streaming client + hcli, err := hrpc.NewWebSocketClient(instances[0].WebSocketServer.URL, hrpc.DefaultHandshakeTimeout, pubsub.MaxPendingMessages, pubsub.MaxReadMessageSize) + gomega.Ω(err).Should(gomega.BeNil()) - ginkgo.It("rejects empty mint", func() { - other, err := ed25519.GeneratePrivateKey() - gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - nil, - &actions.MintAsset{ + // Create tx + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + transfer := &actions.Transfer{ To: auth.NewED25519Address(other.PublicKey()), - Asset: asset1ID, - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // bad codec) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Uint64 field is not populated")) - }) + Value: 1, + } + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + _, tx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + transfer, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) - ginkgo.It("reject max mint", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.MintAsset{ - To: rsender2, - Asset: asset1ID, - Value: hconsts.MaxUint64, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)). - Should(gomega.ContainSubstring("overflow")) + // Submit tx and accept block + gomega.Ω(hcli.RegisterTx(tx)).Should(gomega.BeNil()) - balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(10))) - balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + // Wait for message to be sent + time.Sleep(2 * pubsub.MaxMessageWait) - exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(exists).Should(gomega.BeTrue()) - gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) - gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) - gomega.Ω(metadata).Should(gomega.Equal(asset1)) - gomega.Ω(supply).Should(gomega.Equal(uint64(10))) - gomega.Ω(owner).Should(gomega.Equal(sender)) - gomega.Ω(warp).Should(gomega.BeFalse()) - }) + for instances[0].vm.Mempool().Len(context.TODO()) == 0 { + // We need to wait for mempool to be populated because issuance will + // return as soon as bytes are on the channel. + hutils.Outf("{{yellow}}waiting for mempool to return non-zero txs{{/}}\n") + time.Sleep(500 * time.Millisecond) + } + gomega.Ω(err).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - ginkgo.It("rejects mint of native token", func() { - other, err := ed25519.GeneratePrivateKey() - gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - nil, - &actions.MintAsset{ - To: auth.NewED25519Address(other.PublicKey()), - Value: 10, - }, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // bad codec) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("ID field is not populated")) - }) + // Read decision from connection + txID, dErr, result, err := hcli.ListenTx(context.TODO()) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(txID).Should(gomega.Equal(tx.ID())) + gomega.Ω(dErr).Should(gomega.BeNil()) + gomega.Ω(result.Success).Should(gomega.BeTrue()) + gomega.Ω(result).Should(gomega.Equal(results[0])) - ginkgo.It("mints another new asset (to self)", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, tx, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.CreateAsset{ - Symbol: asset2Symbol, - Decimals: asset2Decimals, - Metadata: asset2, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - asset2ID = tx.ID() + // Close connection when done + gomega.Ω(hcli.Close()).Should(gomega.BeNil()) + }) - submit, _, _, err = instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.MintAsset{ - To: rsender, - Asset: asset2ID, - Value: 10, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept = expectBlk(instances[0]) - results = accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + ginkgo.It("transfer an asset with a memo", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.Transfer{ + To: auth.NewED25519Address(other.PublicKey()), + Value: 10, + Memo: []byte("hello"), + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeTrue()) + }) - balance, err := instances[0].ncli.Balance(context.TODO(), sender, asset2ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(10))) - }) + ginkgo.It("transfer an asset with large memo", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1001, + }, + nil, + &actions.Transfer{ + To: auth.NewED25519Address(other.PublicKey()), + Value: 10, + Memo: make([]byte, 1000), + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // too large) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("size is larger than limit")) + }) - ginkgo.It("mints another new asset (to self) on another account", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, tx, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.CreateAsset{ - Symbol: asset3Symbol, - Decimals: asset3Decimals, - Metadata: asset3, - }, - factory2, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - asset3ID = tx.ID() + ginkgo.It("mint an asset that doesn't exist", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + assetID := ids.GenerateTestID() + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: auth.NewED25519Address(other.PublicKey()), + Asset: assetID, + Value: 10, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)). + Should(gomega.ContainSubstring("asset missing")) + + exists, _, _, _, _, _, _, err := instances[0].ncli.Asset(context.TODO(), assetID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeFalse()) + }) + + ginkgo.It("create a new asset (no metadata)", func() { + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1001, + }, + nil, + &actions.CreateAsset{ + Symbol: []byte("s0"), + Decimals: 0, + Metadata: nil, + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // too large) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Bytes field is not populated")) + }) + + ginkgo.It("create a new asset (no symbol)", func() { + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1001, + }, + nil, + &actions.CreateAsset{ + Symbol: nil, + Decimals: 0, + Metadata: []byte("m"), + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // too large) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Bytes field is not populated")) + }) + + ginkgo.It("create asset with too long of metadata", func() { + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + nil, + &actions.CreateAsset{ + Symbol: []byte("s0"), + Decimals: 0, + Metadata: make([]byte, actions.MaxMetadataSize*2), + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // too large) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("size is larger than limit")) + }) + + ginkgo.It("create a new asset (simple metadata)", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.CreateAsset{ + Symbol: asset1Symbol, + Decimals: asset1Decimals, + Metadata: asset1, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + asset1ID = tx.ID() + balance, err := instances[0].ncli.Balance(context.TODO(), sender, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(0))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("mint a new asset", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: rsender2, + Asset: asset1ID, + Value: 15, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(15))) + balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(15))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("mint asset from wrong owner", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: auth.NewED25519Address(other.PublicKey()), + Asset: asset1ID, + Value: 10, + }, + factory2, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)). + Should(gomega.ContainSubstring("wrong owner")) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(15))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("burn new asset", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.BurnAsset{ + Asset: asset1ID, + Value: 5, + }, + factory2, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(10))) + balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(10))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("burn missing asset", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.BurnAsset{ + Asset: asset1ID, + Value: 10, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)). + Should(gomega.ContainSubstring("invalid balance")) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(10))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("rejects empty mint", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + nil, + &actions.MintAsset{ + To: auth.NewED25519Address(other.PublicKey()), + Asset: asset1ID, + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // bad codec) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("Uint64 field is not populated")) + }) + + ginkgo.It("reject max mint", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: rsender2, + Asset: asset1ID, + Value: hconsts.MaxUint64, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)). + Should(gomega.ContainSubstring("overflow")) + + balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(10))) + balance, err = instances[0].ncli.Balance(context.TODO(), sender, asset1ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(0))) + + exists, symbol, decimals, metadata, supply, owner, warp, err := instances[0].ncli.Asset(context.TODO(), asset1ID, false) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(exists).Should(gomega.BeTrue()) + gomega.Ω(symbol).Should(gomega.Equal(asset1Symbol)) + gomega.Ω(decimals).Should(gomega.Equal(asset1Decimals)) + gomega.Ω(metadata).Should(gomega.Equal(asset1)) + gomega.Ω(supply).Should(gomega.Equal(uint64(10))) + gomega.Ω(owner).Should(gomega.Equal(sender)) + gomega.Ω(warp).Should(gomega.BeFalse()) + }) + + ginkgo.It("rejects mint of native token", func() { + other, err := ed25519.GeneratePrivateKey() + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + nil, + &actions.MintAsset{ + To: auth.NewED25519Address(other.PublicKey()), + Value: 10, + }, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // bad codec) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("ID field is not populated")) + }) + + ginkgo.It("mints another new asset (to self)", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.CreateAsset{ + Symbol: asset2Symbol, + Decimals: asset2Decimals, + Metadata: asset2, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + asset2ID = tx.ID() + + submit, _, _, err = instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: rsender, + Asset: asset2ID, + Value: 10, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept = expectBlk(instances[0]) + results = accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + + balance, err := instances[0].ncli.Balance(context.TODO(), sender, asset2ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(10))) + }) - submit, _, _, err = instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.MintAsset{ - To: rsender2, - Asset: asset3ID, - Value: 10, - }, - factory2, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept = expectBlk(instances[0]) - results = accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + ginkgo.It("mints another new asset (to self) on another account", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.CreateAsset{ + Symbol: asset3Symbol, + Decimals: asset3Decimals, + Metadata: asset3, + }, + factory2, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) + asset3ID = tx.ID() - balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset3ID) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(balance).Should(gomega.Equal(uint64(10))) - }) + submit, _, _, err = instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.MintAsset{ + To: rsender2, + Asset: asset3ID, + Value: 10, + }, + factory2, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept = expectBlk(instances[0]) + results = accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + gomega.Ω(results[0].Success).Should(gomega.BeTrue()) - ginkgo.It("import warp message with nil when expected", func() { - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - nil, - &actions.ImportAsset{}, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // empty warp) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("expected warp message")) - }) + balance, err := instances[0].ncli.Balance(context.TODO(), sender2, asset3ID) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(balance).Should(gomega.Equal(uint64(10))) + }) - ginkgo.It("import warp message empty", func() { - wm, err := warp.NewMessage(&warp.UnsignedMessage{}, &warp.BitSetSignature{}) - gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - wm, - &actions.ImportAsset{}, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // empty warp) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("empty warp payload")) - }) + ginkgo.It("import warp message with nil when expected", func() { + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + nil, + &actions.ImportAsset{}, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // empty warp) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("expected warp message")) + }) - ginkgo.It("import with wrong payload", func() { - uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, []byte("hello")) - gomega.Ω(err).Should(gomega.BeNil()) - wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) - gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - wm, - &actions.ImportAsset{}, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // invalid object) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("insufficient length for input")) - }) + ginkgo.It("import warp message empty", func() { + wm, err := warp.NewMessage(&warp.UnsignedMessage{}, &warp.BitSetSignature{}) + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + wm, + &actions.ImportAsset{}, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // empty warp) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("empty warp payload")) + }) - ginkgo.It("import with invalid payload", func() { - wt := &actions.WarpTransfer{} - wtb, err := wt.Marshal() - gomega.Ω(err).Should(gomega.BeNil()) - uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, wtb) - gomega.Ω(err).Should(gomega.BeNil()) - wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) - gomega.Ω(err).Should(gomega.BeNil()) - tx := chain.NewTx( - &chain.Base{ - ChainID: instances[0].chainID, - Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), - MaxFee: 1000, - }, - wm, - &actions.ImportAsset{}, - ) - // Must do manual construction to avoid `tx.Sign` error (would fail with - // invalid object) - msg, err := tx.Digest() - gomega.Ω(err).To(gomega.BeNil()) - auth, err := factory.Sign(msg) - gomega.Ω(err).To(gomega.BeNil()) - tx.Auth = auth - p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth - gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) - gomega.Ω(p.Err()).To(gomega.BeNil()) - _, err = instances[0].hcli.SubmitTx( - context.Background(), - p.Bytes(), - ) - gomega.Ω(err.Error()).Should(gomega.ContainSubstring("field is not populated")) - }) + ginkgo.It("import with wrong payload", func() { + uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, []byte("hello")) + gomega.Ω(err).Should(gomega.BeNil()) + wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + wm, + &actions.ImportAsset{}, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // invalid object) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("insufficient length for input")) + }) - ginkgo.It("import with wrong destination", func() { - wt := &actions.WarpTransfer{ - To: rsender, - Symbol: []byte("s"), - Decimals: 2, - Asset: ids.GenerateTestID(), - Value: 100, - Return: false, - Reward: 100, - TxID: ids.GenerateTestID(), - DestinationChainID: ids.GenerateTestID(), - } - wtb, err := wt.Marshal() - gomega.Ω(err).Should(gomega.BeNil()) - uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, wtb) - gomega.Ω(err).Should(gomega.BeNil()) - wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) - gomega.Ω(err).Should(gomega.BeNil()) - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - wm, - &actions.ImportAsset{}, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + ginkgo.It("import with invalid payload", func() { + wt := &actions.WarpTransfer{} + wtb, err := wt.Marshal() + gomega.Ω(err).Should(gomega.BeNil()) + uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, wtb) + gomega.Ω(err).Should(gomega.BeNil()) + wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) + gomega.Ω(err).Should(gomega.BeNil()) + tx := chain.NewTx( + &chain.Base{ + ChainID: instances[0].chainID, + Timestamp: hutils.UnixRMilli(-1, 5*hconsts.MillisecondsPerSecond), + MaxFee: 1000, + }, + wm, + &actions.ImportAsset{}, + ) + // Must do manual construction to avoid `tx.Sign` error (would fail with + // invalid object) + msg, err := tx.Digest() + gomega.Ω(err).To(gomega.BeNil()) + auth, err := factory.Sign(msg) + gomega.Ω(err).To(gomega.BeNil()) + tx.Auth = auth + p := codec.NewWriter(0, hconsts.MaxInt) // test codec growth + gomega.Ω(tx.Marshal(p)).To(gomega.BeNil()) + gomega.Ω(p.Err()).To(gomega.BeNil()) + _, err = instances[0].hcli.SubmitTx( + context.Background(), + p.Bytes(), + ) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring("field is not populated")) + }) - // Build block with no context (should fail) - gomega.Ω(instances[0].vm.Builder().Force(context.TODO())).To(gomega.BeNil()) - <-instances[0].toEngine - blk, err := instances[0].vm.BuildBlock(context.TODO()) - gomega.Ω(err).To(gomega.Not(gomega.BeNil())) - gomega.Ω(blk).To(gomega.BeNil()) - - // Wait for mempool to be size 1 (txs are restored async) - for { - if instances[0].vm.Mempool().Len(context.Background()) > 0 { - break + ginkgo.It("import with wrong destination", func() { + wt := &actions.WarpTransfer{ + To: rsender, + Symbol: []byte("s"), + Decimals: 2, + Asset: ids.GenerateTestID(), + Value: 100, + Return: false, + Reward: 100, + TxID: ids.GenerateTestID(), + DestinationChainID: ids.GenerateTestID(), } - log.Info("waiting for txs to be restored") - time.Sleep(100 * time.Millisecond) - } + wtb, err := wt.Marshal() + gomega.Ω(err).Should(gomega.BeNil()) + uwm, err := warp.NewUnsignedMessage(networkID, ids.Empty, wtb) + gomega.Ω(err).Should(gomega.BeNil()) + wm, err := warp.NewMessage(uwm, &warp.BitSetSignature{}) + gomega.Ω(err).Should(gomega.BeNil()) + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + wm, + &actions.ImportAsset{}, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - // Build block with context - accept := expectBlkWithContext(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)).Should(gomega.ContainSubstring("warp verification failed")) - }) + // Build block with no context (should fail) + gomega.Ω(instances[0].vm.Builder().Force(context.TODO())).To(gomega.BeNil()) + <-instances[0].toEngine + blk, err := instances[0].vm.BuildBlock(context.TODO()) + gomega.Ω(err).To(gomega.Not(gomega.BeNil())) + gomega.Ω(blk).To(gomega.BeNil()) + + // Wait for mempool to be size 1 (txs are restored async) + for { + if instances[0].vm.Mempool().Len(context.Background()) > 0 { + break + } + log.Info("waiting for txs to be restored") + time.Sleep(100 * time.Millisecond) + } - ginkgo.It("export native asset", func() { - dest := ids.GenerateTestID() - loan, err := instances[0].ncli.Loan(context.TODO(), ids.Empty, dest) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(loan).Should(gomega.Equal(uint64(0))) + // Build block with context + accept := expectBlkWithContext(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)).Should(gomega.ContainSubstring("warp verification failed")) + }) - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, tx, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.ExportAsset{ - To: rsender, - Asset: ids.Empty, - Value: 100, - Return: false, - Reward: 10, - Destination: dest, - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeTrue()) - wt := &actions.WarpTransfer{ - To: rsender, - Symbol: []byte(nconsts.Symbol), - Decimals: nconsts.Decimals, - Asset: ids.Empty, - Value: 100, - Return: false, - Reward: 10, - TxID: tx.ID(), - DestinationChainID: dest, - } - wtb, err := wt.Marshal() - gomega.Ω(err).Should(gomega.BeNil()) - wm, err := warp.NewUnsignedMessage(networkID, instances[0].chainID, wtb) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(result.WarpMessage).Should(gomega.Equal(wm)) + ginkgo.It("export native asset", func() { + dest := ids.GenerateTestID() + loan, err := instances[0].ncli.Loan(context.TODO(), ids.Empty, dest) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(loan).Should(gomega.Equal(uint64(0))) - loan, err = instances[0].ncli.Loan(context.TODO(), ids.Empty, dest) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(loan).Should(gomega.Equal(uint64(110))) - }) + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, tx, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ExportAsset{ + To: rsender, + Asset: ids.Empty, + Value: 100, + Return: false, + Reward: 10, + Destination: dest, + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeTrue()) + wt := &actions.WarpTransfer{ + To: rsender, + Symbol: []byte(nconsts.Symbol), + Decimals: nconsts.Decimals, + Asset: ids.Empty, + Value: 100, + Return: false, + Reward: 10, + TxID: tx.ID(), + DestinationChainID: dest, + } + wtb, err := wt.Marshal() + gomega.Ω(err).Should(gomega.BeNil()) + wm, err := warp.NewUnsignedMessage(networkID, instances[0].chainID, wtb) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(result.WarpMessage).Should(gomega.Equal(wm)) - ginkgo.It("export native asset (invalid return)", func() { - parser, err := instances[0].ncli.Parser(context.Background()) - gomega.Ω(err).Should(gomega.BeNil()) - submit, _, _, err := instances[0].hcli.GenerateTransaction( - context.Background(), - parser, - nil, - &actions.ExportAsset{ - To: rsender, - Asset: ids.Empty, - Value: 100, - Return: true, - Reward: 10, - Destination: ids.GenerateTestID(), - }, - factory, - ) - gomega.Ω(err).Should(gomega.BeNil()) - gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) - accept := expectBlk(instances[0]) - results := accept(false) - gomega.Ω(results).Should(gomega.HaveLen(1)) - result := results[0] - gomega.Ω(result.Success).Should(gomega.BeFalse()) - gomega.Ω(string(result.Output)).Should(gomega.ContainSubstring("not warp asset")) + loan, err = instances[0].ncli.Loan(context.TODO(), ids.Empty, dest) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(loan).Should(gomega.Equal(uint64(110))) + }) + + ginkgo.It("export native asset (invalid return)", func() { + parser, err := instances[0].ncli.Parser(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + submit, _, _, err := instances[0].hcli.GenerateTransaction( + context.Background(), + parser, + nil, + &actions.ExportAsset{ + To: rsender, + Asset: ids.Empty, + Value: 100, + Return: true, + Reward: 10, + Destination: ids.GenerateTestID(), + }, + factory, + ) + gomega.Ω(err).Should(gomega.BeNil()) + gomega.Ω(submit(context.Background())).Should(gomega.BeNil()) + accept := expectBlk(instances[0]) + results := accept(false) + gomega.Ω(results).Should(gomega.HaveLen(1)) + result := results[0] + gomega.Ω(result.Success).Should(gomega.BeFalse()) + gomega.Ω(string(result.Output)).Should(gomega.ContainSubstring("not warp asset")) + }) }) }) @@ -1652,3 +2109,31 @@ func (*appSender) SendCrossChainAppRequest(context.Context, ids.ID, uint32, []by func (*appSender) SendCrossChainAppResponse(context.Context, ids.ID, uint32, []byte) error { return nil } + +func ImportBlockToInstance(vm *vm.VM, block snowman.Block) { + blk, err := vm.ParseBlock(context.Background(), block.Bytes()) + gomega.Ω(err).Should(gomega.BeNil()) + err = blk.Verify(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) + + gomega.Ω(blk.Status()).To(gomega.Equal(choices.Processing)) + err = vm.SetPreference(context.Background(), blk.ID()) + gomega.Ω(err).To(gomega.BeNil()) + + err = blk.Accept(context.Background()) + gomega.Ω(err).Should(gomega.BeNil()) +} + +func setEmissionValidators() { + currentValidators := make([]*emission.Validator, 0, len(instances)) + for i, inst := range instances { + val := emission.Validator{ + NodeID: inst.nodeID, + PublicKey: bls.PublicKeyToBytes(nodesPubKeys[i]), + } + currentValidators = append(currentValidators, &val) + } + for i := range instances { + emissions[i].(*emission.Manual).CurrentValidators = currentValidators + } +}