Skip to content

Commit

Permalink
Merge pull request #6078 from multiversx/correct-unStakedList-after-e…
Browse files Browse the repository at this point in the history
…nd-of-epoch

fixed unstaked list on delegation when nodes are unstaked from queue
  • Loading branch information
iulianpascalau committed Apr 2, 2024
2 parents f634eaa + c227dd5 commit fd7b7da
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 2 deletions.
@@ -0,0 +1,140 @@
package staking

import (
"encoding/hex"
"fmt"
"math/big"
"testing"
"time"

"github.com/multiversx/mx-chain-core-go/core"
"github.com/multiversx/mx-chain-core-go/data/transaction"
"github.com/multiversx/mx-chain-go/config"
"github.com/multiversx/mx-chain-go/node/chainSimulator"
"github.com/multiversx/mx-chain-go/node/chainSimulator/components/api"
"github.com/multiversx/mx-chain-go/node/chainSimulator/configs"
"github.com/multiversx/mx-chain-go/vm"
"github.com/stretchr/testify/require"
)

func TestStakingProviderWithNodes(t *testing.T) {
if testing.Short() {
t.Skip("this is not a short test")
}

stakingV4ActivationEpoch := uint32(2)

t.Run("staking ph 4 step 1 active", func(t *testing.T) {
testStakingProviderWithNodesReStakeUnStaked(t, stakingV4ActivationEpoch)
})

t.Run("staking ph 4 step 2 active", func(t *testing.T) {
testStakingProviderWithNodesReStakeUnStaked(t, stakingV4ActivationEpoch+1)
})

t.Run("staking ph 4 step 3 active", func(t *testing.T) {
testStakingProviderWithNodesReStakeUnStaked(t, stakingV4ActivationEpoch+2)
})
}

func testStakingProviderWithNodesReStakeUnStaked(t *testing.T, stakingV4ActivationEpoch uint32) {
roundDurationInMillis := uint64(6000)
roundsPerEpoch := core.OptionalUint64{
HasValue: true,
Value: 20,
}

cs, err := chainSimulator.NewChainSimulator(chainSimulator.ArgsChainSimulator{
BypassTxSignatureCheck: false,
TempDir: t.TempDir(),
PathToInitialConfig: defaultPathToInitialConfig,
NumOfShards: 3,
GenesisTimestamp: time.Now().Unix(),
RoundDurationInMillis: roundDurationInMillis,
RoundsPerEpoch: roundsPerEpoch,
ApiInterface: api.NewNoApiInterface(),
MinNodesPerShard: 3,
MetaChainMinNodes: 3,
NumNodesWaitingListMeta: 3,
NumNodesWaitingListShard: 3,
AlterConfigsFunction: func(cfg *config.Configs) {
configs.SetStakingV4ActivationEpochs(cfg, stakingV4ActivationEpoch)
},
})
require.Nil(t, err)
require.NotNil(t, cs)
defer cs.Close()

mintValue := big.NewInt(0).Mul(big.NewInt(5000), oneEGLD)
validatorOwner, err := cs.GenerateAndMintWalletAddress(0, mintValue)
require.Nil(t, err)
require.Nil(t, err)

err = cs.GenerateBlocksUntilEpochIsReached(1)
require.Nil(t, err)

// create delegation contract
stakeValue, _ := big.NewInt(0).SetString("4250000000000000000000", 10)
dataField := "createNewDelegationContract@00@0ea1"
txStake := generateTransaction(validatorOwner.Bytes, getNonce(t, cs, validatorOwner), vm.DelegationManagerSCAddress, stakeValue, dataField, 80_000_000)
stakeTx, err := cs.SendTxAndGenerateBlockTilTxIsExecuted(txStake, maxNumOfBlockToGenerateWhenExecutingTx)
require.Nil(t, err)
require.NotNil(t, stakeTx)

delegationAddress := stakeTx.Logs.Events[2].Address
delegationAddressBytes, _ := cs.GetNodeHandler(0).GetCoreComponents().AddressPubKeyConverter().Decode(delegationAddress)

// add nodes in queue
_, blsKeys, err := chainSimulator.GenerateBlsPrivateKeys(1)
require.Nil(t, err)

txDataFieldAddNodes := fmt.Sprintf("addNodes@%s@%s", blsKeys[0], mockBLSSignature+"02")
ownerNonce := getNonce(t, cs, validatorOwner)
txAddNodes := generateTransaction(validatorOwner.Bytes, ownerNonce, delegationAddressBytes, big.NewInt(0), txDataFieldAddNodes, gasLimitForStakeOperation)
addNodesTx, err := cs.SendTxAndGenerateBlockTilTxIsExecuted(txAddNodes, maxNumOfBlockToGenerateWhenExecutingTx)
require.Nil(t, err)
require.NotNil(t, addNodesTx)

txDataFieldStakeNodes := fmt.Sprintf("stakeNodes@%s", blsKeys[0])
ownerNonce = getNonce(t, cs, validatorOwner)
txStakeNodes := generateTransaction(validatorOwner.Bytes, ownerNonce, delegationAddressBytes, big.NewInt(0), txDataFieldStakeNodes, gasLimitForStakeOperation)

stakeNodesTxs, err := cs.SendTxsAndGenerateBlocksTilAreExecuted([]*transaction.Transaction{txStakeNodes}, maxNumOfBlockToGenerateWhenExecutingTx)
require.Nil(t, err)
require.Equal(t, 1, len(stakeNodesTxs))

metachainNode := cs.GetNodeHandler(core.MetachainShardId)
decodedBLSKey0, _ := hex.DecodeString(blsKeys[0])
status := getBLSKeyStatus(t, metachainNode, decodedBLSKey0)
require.Equal(t, "queued", status)

// activate staking v4
err = cs.GenerateBlocksUntilEpochIsReached(int32(stakingV4ActivationEpoch))
require.Nil(t, err)

status = getBLSKeyStatus(t, metachainNode, decodedBLSKey0)
require.Equal(t, "unStaked", status)

result := getAllNodeStates(t, metachainNode, delegationAddressBytes)
require.NotNil(t, result)
require.Equal(t, "unStaked", result[blsKeys[0]])

ownerNonce = getNonce(t, cs, validatorOwner)
reStakeTxData := fmt.Sprintf("reStakeUnStakedNodes@%s", blsKeys[0])
reStakeNodes := generateTransaction(validatorOwner.Bytes, ownerNonce, delegationAddressBytes, big.NewInt(0), reStakeTxData, gasLimitForStakeOperation)
reStakeTx, err := cs.SendTxAndGenerateBlockTilTxIsExecuted(reStakeNodes, maxNumOfBlockToGenerateWhenExecutingTx)
require.Nil(t, err)
require.NotNil(t, reStakeTx)

status = getBLSKeyStatus(t, metachainNode, decodedBLSKey0)
require.Equal(t, "staked", status)

result = getAllNodeStates(t, metachainNode, delegationAddressBytes)
require.NotNil(t, result)
require.Equal(t, "staked", result[blsKeys[0]])

err = cs.GenerateBlocks(20)
require.Nil(t, err)

checkValidatorStatus(t, cs, blsKeys[0], "auction")
}
3 changes: 2 additions & 1 deletion vm/systemSmartContracts/delegation.go
Expand Up @@ -2322,7 +2322,8 @@ func (d *delegation) deleteDelegatorIfNeeded(address []byte, delegator *Delegato
}

func (d *delegation) unStakeAtEndOfEpoch(args *vmcommon.ContractCallInput) vmcommon.ReturnCode {
if !bytes.Equal(args.CallerAddr, d.endOfEpochAddr) {
if !bytes.Equal(args.CallerAddr, d.endOfEpochAddr) &&
!bytes.Equal(args.CallerAddr, d.stakingSCAddr) {
d.eei.AddReturnMessage("can be called by end of epoch address only")
return vmcommon.UserError
}
Expand Down
4 changes: 4 additions & 0 deletions vm/systemSmartContracts/eei.go
Expand Up @@ -144,13 +144,17 @@ func (host *vmContext) GetStorageFromAddress(address []byte, key []byte) []byte
if value, isInMap := storageAdrMap[string(key)]; isInMap {
return value
}
} else {
storageAdrMap = make(map[string][]byte)
}

data, _, err := host.blockChainHook.GetStorageData(address, key)
if err != nil {
return nil
}

storageAdrMap[string(key)] = data

return data
}

Expand Down
45 changes: 44 additions & 1 deletion vm/systemSmartContracts/stakingWaitingList.go
Expand Up @@ -8,6 +8,7 @@ import (
"math/big"
"strconv"

"github.com/multiversx/mx-chain-core-go/core"
"github.com/multiversx/mx-chain-go/common"
"github.com/multiversx/mx-chain-go/vm"
vmcommon "github.com/multiversx/mx-chain-vm-common-go"
Expand Down Expand Up @@ -824,9 +825,10 @@ func (s *stakingSC) unStakeAllNodesFromQueue(args *vmcommon.ContractCallInput) v
return vmcommon.Ok
}

orderedListOwners := make([]string, 0)
mapOwnerKeys := make(map[string][][]byte)
for i, blsKey := range waitingListData.blsKeys {
registrationData := waitingListData.stakedDataList[i]

result := s.doUnStake(blsKey, registrationData)
if result != vmcommon.Ok {
return result
Expand All @@ -835,14 +837,55 @@ func (s *stakingSC) unStakeAllNodesFromQueue(args *vmcommon.ContractCallInput) v
// delete element from waiting list
inWaitingListKey := createWaitingListKey(blsKey)
s.eei.SetStorage(inWaitingListKey, nil)

ownerAddr := string(registrationData.OwnerAddress)
_, exists := mapOwnerKeys[ownerAddr]
if !exists {
mapOwnerKeys[ownerAddr] = make([][]byte, 0)
orderedListOwners = append(orderedListOwners, ownerAddr)
}

mapOwnerKeys[ownerAddr] = append(mapOwnerKeys[ownerAddr], blsKey)
}

// delete waiting list head element
s.eei.SetStorage([]byte(waitingListHeadKey), nil)

// call unStakeAtEndOfEpoch from the delegation contracts to compute the new unStaked list
for _, owner := range orderedListOwners {
listOfKeys := mapOwnerKeys[owner]

if s.eei.BlockChainHook().GetShardOfAddress([]byte(owner)) != core.MetachainShardId {
continue
}

unStakeCall := "unStakeAtEndOfEpoch"
for _, key := range listOfKeys {
unStakeCall += "@" + hex.EncodeToString(key)
}
returnCode := s.executeOnStakeAtEndOfEpoch([]byte(owner), listOfKeys, args.RecipientAddr)
if returnCode != vmcommon.Ok {
return returnCode
}
}

return vmcommon.Ok
}

func (s *stakingSC) executeOnStakeAtEndOfEpoch(destinationAddress []byte, listOfKeys [][]byte, senderAddress []byte) vmcommon.ReturnCode {
unStakeCall := "unStakeAtEndOfEpoch"
for _, key := range listOfKeys {
unStakeCall += "@" + hex.EncodeToString(key)
}
vmOutput, err := s.eei.ExecuteOnDestContext(destinationAddress, senderAddress, big.NewInt(0), []byte(unStakeCall))
if err != nil {
s.eei.AddReturnMessage(err.Error())
return vmcommon.UserError
}

return vmOutput.ReturnCode
}

func (s *stakingSC) cleanAdditionalQueue(args *vmcommon.ContractCallInput) vmcommon.ReturnCode {
if !s.enableEpochsHandler.IsFlagEnabled(common.CorrectLastUnJailedFlag) {
s.eei.AddReturnMessage("invalid method to call")
Expand Down
111 changes: 111 additions & 0 deletions vm/systemSmartContracts/staking_test.go
Expand Up @@ -3706,3 +3706,114 @@ func TestStakingSc_UnStakeAllFromQueue(t *testing.T) {
doGetStatus(t, stakingSmartContract, eei, []byte("thirdKey "), "staked")
doGetStatus(t, stakingSmartContract, eei, []byte("fourthKey"), "staked")
}

func TestStakingSc_UnStakeAllFromQueueWithDelegationContracts(t *testing.T) {
t.Parallel()

blockChainHook := &mock.BlockChainHookStub{}
blockChainHook.GetStorageDataCalled = func(accountsAddress []byte, index []byte) ([]byte, uint32, error) {
return nil, 0, nil
}
blockChainHook.GetShardOfAddressCalled = func(address []byte) uint32 {
return core.MetachainShardId
}

eei := createDefaultEei()
eei.blockChainHook = blockChainHook
eei.SetSCAddress([]byte("addr"))

delegationSC, _ := createDelegationContractAndEEI()
delegationSC.eei = eei

systemSCContainerStub := &mock.SystemSCContainerStub{GetCalled: func(key []byte) (vm.SystemSmartContract, error) {
return delegationSC, nil
}}
_ = eei.SetSystemSCContainer(systemSCContainerStub)

stakingAccessAddress := vm.ValidatorSCAddress
args := createMockStakingScArguments()
args.StakingAccessAddr = stakingAccessAddress
args.StakingSCConfig.MaxNumberOfNodesForStake = 1
enableEpochsHandler, _ := args.EnableEpochsHandler.(*enableEpochsHandlerMock.EnableEpochsHandlerStub)
args.Eei = eei
args.StakingSCConfig.UnBondPeriod = 100
stakingSmartContract, _ := NewStakingSmartContract(args)

stakerAddress := []byte("stakerAddr")

blockChainHook.CurrentNonceCalled = func() uint64 {
return 1
}

enableEpochsHandler.AddActiveFlags(common.StakingV2Flag)
enableEpochsHandler.AddActiveFlags(common.StakeFlag)

// do stake should work
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("firstKey "))
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("secondKey"))
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("thirdKey "))
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("fourthKey"))

waitingReturn := doGetWaitingListRegisterNonceAndRewardAddress(t, stakingSmartContract, eei)
requireSliceContains(t, waitingReturn, [][]byte{[]byte("secondKey"), []byte("thirdKey "), []byte("fourthKey")})

dStatus := &DelegationContractStatus{
StakedKeys: make([]*NodesData, 4),
NotStakedKeys: nil,
UnStakedKeys: nil,
NumUsers: 0,
}
dStatus.StakedKeys[0] = &NodesData{BLSKey: []byte("firstKey ")}
dStatus.StakedKeys[1] = &NodesData{BLSKey: []byte("secondKey")}
dStatus.StakedKeys[2] = &NodesData{BLSKey: []byte("thirdKey ")}
dStatus.StakedKeys[3] = &NodesData{BLSKey: []byte("fourthKey")}

marshaledData, _ := delegationSC.marshalizer.Marshal(dStatus)
eei.SetStorageForAddress(stakerAddress, []byte(delegationStatusKey), marshaledData)

arguments := CreateVmContractCallInput()
arguments.RecipientAddr = vm.StakingSCAddress
validatorData := &ValidatorDataV2{
TotalStakeValue: big.NewInt(400),
TotalUnstaked: big.NewInt(0),
RewardAddress: stakerAddress,
BlsPubKeys: [][]byte{[]byte("firstKey "), []byte("secondKey"), []byte("thirdKey "), []byte("fourthKey")},
}
arguments.CallerAddr = stakingSmartContract.endOfEpochAccessAddr
marshaledData, _ = stakingSmartContract.marshalizer.Marshal(validatorData)
eei.SetStorageForAddress(vm.ValidatorSCAddress, stakerAddress, marshaledData)

enableEpochsHandler.AddActiveFlags(common.StakingV4Step1Flag)
enableEpochsHandler.AddActiveFlags(common.StakingV4StartedFlag)

arguments.Function = "unStakeAllNodesFromQueue"
retCode := stakingSmartContract.Execute(arguments)
fmt.Println(eei.returnMessage)
assert.Equal(t, retCode, vmcommon.Ok)

assert.Equal(t, len(eei.GetStorage([]byte(waitingListHeadKey))), 0)
newHead, _ := stakingSmartContract.getWaitingListHead()
assert.Equal(t, uint32(0), newHead.Length) // no entries in the queue list

marshaledData = eei.GetStorageFromAddress(stakerAddress, []byte(delegationStatusKey))
_ = stakingSmartContract.marshalizer.Unmarshal(dStatus, marshaledData)
assert.Equal(t, len(dStatus.UnStakedKeys), 3)
assert.Equal(t, len(dStatus.StakedKeys), 1)

doGetStatus(t, stakingSmartContract, eei, []byte("secondKey"), "unStaked")
doGetStatus(t, stakingSmartContract, eei, []byte("thirdKey "), "unStaked")
doGetStatus(t, stakingSmartContract, eei, []byte("fourthKey"), "unStaked")

// stake them again - as they were deleted from waiting list
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("thirdKey "))
doStake(t, stakingSmartContract, stakingAccessAddress, stakerAddress, []byte("fourthKey"))

doGetStatus(t, stakingSmartContract, eei, []byte("thirdKey "), "staked")
doGetStatus(t, stakingSmartContract, eei, []byte("fourthKey"), "staked")
}

func requireSliceContains(t *testing.T, s1, s2 [][]byte) {
for _, elemInS2 := range s2 {
require.Contains(t, s1, elemInS2)
}
}

0 comments on commit fd7b7da

Please sign in to comment.