diff --git a/integrationTests/chainSimulator/staking/stakingProviderWithNodesinQueue_test.go b/integrationTests/chainSimulator/staking/stakingProviderWithNodesinQueue_test.go new file mode 100644 index 00000000000..af50d56c821 --- /dev/null +++ b/integrationTests/chainSimulator/staking/stakingProviderWithNodesinQueue_test.go @@ -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") +} diff --git a/vm/systemSmartContracts/delegation.go b/vm/systemSmartContracts/delegation.go index ac33ba81da2..ab5c97cfce0 100644 --- a/vm/systemSmartContracts/delegation.go +++ b/vm/systemSmartContracts/delegation.go @@ -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 } diff --git a/vm/systemSmartContracts/eei.go b/vm/systemSmartContracts/eei.go index 55f554d11b0..3f251a6cca4 100644 --- a/vm/systemSmartContracts/eei.go +++ b/vm/systemSmartContracts/eei.go @@ -144,6 +144,8 @@ 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) @@ -151,6 +153,8 @@ func (host *vmContext) GetStorageFromAddress(address []byte, key []byte) []byte return nil } + storageAdrMap[string(key)] = data + return data } diff --git a/vm/systemSmartContracts/stakingWaitingList.go b/vm/systemSmartContracts/stakingWaitingList.go index e1d0ff00cb4..e08b16b3cde 100644 --- a/vm/systemSmartContracts/stakingWaitingList.go +++ b/vm/systemSmartContracts/stakingWaitingList.go @@ -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" @@ -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 @@ -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") diff --git a/vm/systemSmartContracts/staking_test.go b/vm/systemSmartContracts/staking_test.go index 53d78208cf1..fb92a574945 100644 --- a/vm/systemSmartContracts/staking_test.go +++ b/vm/systemSmartContracts/staking_test.go @@ -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) + } +}