From 79c7b8348c6f4e3c886b459ee26561aa55698f66 Mon Sep 17 00:00:00 2001 From: GheisMohammadi Date: Mon, 29 Dec 2025 18:15:30 +0800 Subject: [PATCH 1/5] initial version of staking v2 --- core/staking_verifier.go | 68 ++++++++++++++++++++++++++++++++------- internal/params/config.go | 17 ++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/core/staking_verifier.go b/core/staking_verifier.go index 5b644d3908..be04022486 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -279,24 +279,68 @@ func VerifyAndDelegateFromMsg( startBalance := big.NewInt(0).Set(delegateBalance) // Start from the oldest undelegated tokens curIndex := 0 - for ; curIndex < len(delegation.Undelegations); curIndex++ { - if delegation.Undelegations[curIndex].Epoch.Cmp(epoch) >= 0 { - break + isStakingV2 := chainConfig.IsStakingV2(epoch) + + if isStakingV2 { + // Staking V2: Properly handle undelegation consumption with explicit entry removal + newUndelegations := []staking.Undelegation{} + for curIndex < len(delegation.Undelegations) { + entry := &delegation.Undelegations[curIndex] + if entry.Epoch.Cmp(epoch) >= 0 { + // Keep all remaining entries (not yet eligible for redelegation) + newUndelegations = append(newUndelegations, delegation.Undelegations[curIndex:]...) + break + } + + if entry.Amount.Cmp(delegateBalance) <= 0 { + // Fully consume this entry + delegateBalance.Sub(delegateBalance, entry.Amount) + // Don't add to newUndelegations (fully consumed) + } else { + // Partially consume this entry + remainingAmount := big.NewInt(0).Sub(entry.Amount, delegateBalance) + newUndelegations = append(newUndelegations, staking.Undelegation{ + Amount: remainingAmount, + Epoch: entry.Epoch, + }) + delegateBalance = big.NewInt(0) + curIndex++ + // Keep all remaining entries + if curIndex < len(delegation.Undelegations) { + newUndelegations = append(newUndelegations, delegation.Undelegations[curIndex:]...) + } + break + } + curIndex++ } - if delegation.Undelegations[curIndex].Amount.Cmp(delegateBalance) <= 0 { - delegateBalance.Sub(delegateBalance, delegation.Undelegations[curIndex].Amount) - } else { - delegation.Undelegations[curIndex].Amount.Sub( - delegation.Undelegations[curIndex].Amount, delegateBalance, - ) - delegateBalance = big.NewInt(0) - break + // Only update undelegations if something was consumed + if startBalance.Cmp(delegateBalance) > 0 { + delegation.Undelegations = newUndelegations + } + } else { + // Original logic (for backward compatibility) + for ; curIndex < len(delegation.Undelegations); curIndex++ { + if delegation.Undelegations[curIndex].Epoch.Cmp(epoch) >= 0 { + break + } + if delegation.Undelegations[curIndex].Amount.Cmp(delegateBalance) <= 0 { + delegateBalance.Sub(delegateBalance, delegation.Undelegations[curIndex].Amount) + } else { + delegation.Undelegations[curIndex].Amount.Sub( + delegation.Undelegations[curIndex].Amount, delegateBalance, + ) + delegateBalance = big.NewInt(0) + break + } } } if startBalance.Cmp(delegateBalance) > 0 { // Used undelegated token for redelegation - delegation.Undelegations = delegation.Undelegations[curIndex:] + if !isStakingV2 { + // Original logic: slice undelegations array + delegation.Undelegations = delegation.Undelegations[curIndex:] + } if err := wrapper.SanityCheck(); err != nil { return nil, nil, nil, err } diff --git a/internal/params/config.go b/internal/params/config.go index 29935cd1a8..54030b475a 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -83,6 +83,7 @@ var ( HIP32Epoch: big.NewInt(2152), // 2024-10-31 13:02 UTC IsOneSecondEpoch: EpochTBD, EIP2537PrecompileEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // TestnetChainConfig contains the chain parameters to run a node on the harmony test network. @@ -134,6 +135,7 @@ var ( IsOneSecondEpoch: EpochTBD, EIP2537PrecompileEpoch: EpochTBD, EIP1153TransientStorageEpoch: big.NewInt(6280), + StakingV2Epoch: EpochTBD, } // PangaeaChainConfig contains the chain parameters for the Pangaea network. // All features except for CrossLink are enabled at launch. @@ -184,6 +186,7 @@ var ( TestnetExternalEpoch: EpochTBD, IsOneSecondEpoch: EpochTBD, EIP2537PrecompileEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // PartnerChainConfig contains the chain parameters for the Partner network. @@ -236,6 +239,7 @@ var ( IsOneSecondEpoch: big.NewInt(17436), EIP2537PrecompileEpoch: EpochTBD, EIP1153TransientStorageEpoch: big.NewInt(35626), + StakingV2Epoch: EpochTBD, } // StressnetChainConfig contains the chain parameters for the Stress test network. @@ -287,6 +291,7 @@ var ( TestnetExternalEpoch: EpochTBD, IsOneSecondEpoch: EpochTBD, EIP2537PrecompileEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // LocalnetChainConfig contains the chain parameters to run for local development. @@ -337,6 +342,7 @@ var ( TestnetExternalEpoch: EpochTBD, IsOneSecondEpoch: big.NewInt(4), EIP2537PrecompileEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // AllProtocolChanges ... @@ -391,6 +397,7 @@ var ( big.NewInt(0), big.NewInt(0), // EIP2537PrecompileEpoch big.NewInt(0), // 1153 transient storage + big.NewInt(0), // StakingV2Epoch } // TestChainConfig ... @@ -445,6 +452,7 @@ var ( big.NewInt(0), big.NewInt(0), // EIP2537PrecompileEpoch big.NewInt(0), // 1153 transient storage + big.NewInt(0), // StakingV2Epoch } // TestRules ... @@ -634,6 +642,10 @@ type ChainConfig struct { // EIP2537PrecompileEpoch is the first epoch to support the EIP-2537 precompiles EIP1153TransientStorageEpoch *big.Int `json:"eip1153-transient-storage-epoch,omitempty"` + + // StakingV2Epoch is the epoch when Staking V2 is activated, which fixes critical + // issues in redelegation logic and other staking operations + StakingV2Epoch *big.Int `json:"staking-v2-epoch,omitempty"` } // String implements the fmt.Stringer interface. @@ -937,6 +949,11 @@ func (c *ChainConfig) IsTopMaxRate(epoch *big.Int) bool { return isForked(c.TopMaxRateEpoch, epoch) } +// IsStakingV2 determines whether it is the epoch when Staking V2 is activated +func (c *ChainConfig) IsStakingV2(epoch *big.Int) bool { + return isForked(c.StakingV2Epoch, epoch) +} + // During this epoch, shards 2 and 3 will start sending // their balances over to shard 0 or 1. func (c *ChainConfig) IsOneEpochBeforeHIP30(epoch *big.Int) bool { From 02b5d2b8759b0837b89696a18a843e1a1eee8116 Mon Sep 17 00:00:00 2001 From: GheisMohammadi Date: Mon, 29 Dec 2025 18:16:01 +0800 Subject: [PATCH 2/5] add tests for staking v2, verify corner cases --- core/staking_verifier_test.go | 339 ++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 53d47a4788..cedc206458 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -1134,6 +1134,35 @@ func makeStateForRedelegate(t *testing.T) *state.DB { return sdb } +// makeStateForRedelegateCornerCases creates state with multiple undelegation entries for corner case testing +func makeStateForRedelegateCornerCases(t *testing.T, validatorAddr common.Address, undelegations []struct { + amount *big.Int + epoch *big.Int +}) *state.DB { + sdb := makeStateDBForStake(t) + + w, err := sdb.ValidatorWrapper(validatorAddr, false, true) + if err != nil { + t.Fatal(err) + } + + // Add delegation with multiple undelegation entries + delegation := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + for _, undel := range undelegations { + if err := delegation.Undelegate(undel.epoch, undel.amount); err != nil { + t.Fatal(err) + } + } + w.Delegations = append(w.Delegations, delegation) + + if err := sdb.UpdateValidatorWrapper(validatorAddr, w); err != nil { + t.Fatal(err) + } + + sdb.IntermediateRoot(true) + return sdb +} + func addStateUndelegationForAddr(sdb *state.DB, addr common.Address, epoch *big.Int) error { w, err := sdb.ValidatorWrapper(addr, false, true) if err != nil { @@ -1838,3 +1867,313 @@ func assertError(got, expect error) error { } return nil } + +// TestRedelegationCornerCases tests corner cases in redelegation logic that demonstrate +// bugs in the old implementation and fixes in Staking V2 +func TestRedelegationCornerCases(t *testing.T) { + epoch := big.NewInt(10) // Current epoch + epoch1 := big.NewInt(5) // Old undelegation epoch + epoch2 := big.NewInt(6) // Old undelegation epoch + epoch3 := big.NewInt(7) // Old undelegation epoch + + tests := []struct { + name string + undelegations []struct { + amount *big.Int + epoch *big.Int + } + delegateAmount *big.Int + stakingV2 bool + expectedUndelegations []struct { + amount *big.Int + epoch *big.Int + } + expectedBalanceDeducted *big.Int + description string + }{ + { + name: "CornerCase1_FullyConsumeFirst_PartiallyConsumeSecond", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch2}, // 10000 - partially consumed (need 5000, so 5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Add(fiveKOnes, fiveKOnes), // 10000 total + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic bug: keeps partially consumed entry but may have issues with slice manipulation + {amount: fiveKOnes, epoch: epoch2}, // Partially consumed (should be 5000) + {amount: fiveKOnes, epoch: epoch3}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), // All from undelegations + description: "Old logic: Fully consume first entry, partially consume second. May have slice manipulation issues.", + }, + { + name: "CornerCase1_FullyConsumeFirst_PartiallyConsumeSecond_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch2}, // 10000 - partially consumed (need 5000, so 5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Add(fiveKOnes, fiveKOnes), // 10000 total + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly removes fully consumed, keeps partially consumed with correct amount + {amount: fiveKOnes, epoch: epoch2}, // Partially consumed (5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), // All from undelegations + description: "New logic: Correctly removes fully consumed entry, keeps partially consumed with correct amount.", + }, + { + name: "CornerCase2_PartiallyConsumeFirst", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: tenKOnes, epoch: epoch1}, // 10000 - partially consumed (need 3000, so 7000 remains) + {amount: fiveKOnes, epoch: epoch2}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Mul(big.NewInt(3000), oneBig), // 3000 ONE (meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: modifies entry in place, then slices + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(3000), oneBig)), epoch: epoch1}, // 7000 + {amount: fiveKOnes, epoch: epoch2}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Partially consumes first entry. May work but uses error-prone slice manipulation.", + }, + { + name: "CornerCase2_PartiallyConsumeFirst_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: tenKOnes, epoch: epoch1}, // 10000 - partially consumed (need 3000, so 7000 remains) + {amount: fiveKOnes, epoch: epoch2}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Mul(big.NewInt(3000), oneBig), // 3000 ONE (meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: creates new entry with correct remaining amount + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(3000), oneBig)), epoch: epoch1}, // 7000 + {amount: fiveKOnes, epoch: epoch2}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Explicitly creates new entry with correct remaining amount.", + }, + { + name: "CornerCase3_FullyConsumeAll", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch3}, // 5000 - fully consumed + }, + delegateAmount: new(big.Int).Mul(big.NewInt(15000), oneBig), // 15000 ONE (all three, meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: should remove all, but may have issues + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Fully consumes all entries. Should result in empty undelegations.", + }, + { + name: "CornerCase3_FullyConsumeAll_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch3}, // 5000 - fully consumed + }, + delegateAmount: new(big.Int).Mul(big.NewInt(15000), oneBig), // 15000 ONE (all three, meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly removes all fully consumed entries + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Correctly removes all fully consumed entries, results in empty undelegations.", + }, + { + name: "CornerCase4_MixedConsumption", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch3}, // 10000 - partially consumed (need 2000, so 8000 remains) + }, + delegateAmount: new(big.Int).Mul(big.NewInt(12000), oneBig), // 12000 ONE (meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: may have issues with multiple full consumptions followed by partial + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(2000), oneBig)), epoch: epoch3}, // 8000 + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Mixed full and partial consumption. May have slice manipulation issues.", + }, + { + name: "CornerCase4_MixedConsumption_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch3}, // 10000 - partially consumed (need 2000, so 8000 remains) + }, + delegateAmount: new(big.Int).Mul(big.NewInt(12000), oneBig), // 12000 ONE (meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly handles mixed consumption + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(2000), oneBig)), epoch: epoch3}, // 8000 + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Correctly handles mixed full and partial consumption.", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create state with undelegations + sdb := makeStateForRedelegateCornerCases(t, validatorAddr, test.undelegations) + + // Create delegate message + msg := staking.Delegate{ + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Set(test.delegateAmount), + } + + // Get delegation index + w, err := sdb.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + t.Fatal(err) + } + delegationIndex := []staking.DelegationIndex{ + { + ValidatorAddress: validatorAddr, + Index: uint64(len(w.Delegations) - 1), // Last delegation (the one with undelegations) + BlockNum: big.NewInt(100), + }, + } + + // Configure chain config + config := ¶ms.ChainConfig{} + config.MinDelegation100Epoch = big.NewInt(100) + config.RedelegationEpoch = epoch // Enable redelegation + if test.stakingV2 { + config.StakingV2Epoch = epoch // Enable Staking V2 + } else { + config.StakingV2Epoch = big.NewInt(10000000) // EpochTBD - disable Staking V2 + } + + // Execute redelegation + ws, balanceDeducted, fromLockedTokens, err := VerifyAndDelegateFromMsg( + sdb, epoch, &msg, delegationIndex, config, + ) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify balance deducted + if balanceDeducted.Cmp(test.expectedBalanceDeducted) != 0 { + t.Errorf("Balance deducted mismatch: got %v, expected %v", balanceDeducted, test.expectedBalanceDeducted) + } + + // Verify fromLockedTokens + if test.expectedBalanceDeducted.Cmp(big.NewInt(0)) == 0 { + // All from locked tokens + if len(fromLockedTokens) == 0 { + t.Errorf("Expected fromLockedTokens to be non-empty") + } else if lockedAmt, ok := fromLockedTokens[validatorAddr]; !ok { + t.Errorf("Expected fromLockedTokens to contain validatorAddr") + } else if lockedAmt.Cmp(test.delegateAmount) != 0 { + t.Errorf("FromLockedTokens amount mismatch: got %v, expected %v", lockedAmt, test.delegateAmount) + } + } + + // Verify undelegations in the result + if len(ws) == 0 { + t.Fatal("Expected at least one validator wrapper") + } + + foundDelegation := false + for _, w := range ws { + if w.Address == validatorAddr { + for _, del := range w.Delegations { + if del.DelegatorAddress == delegatorAddr { + foundDelegation = true + + // Verify undelegations + if len(del.Undelegations) != len(test.expectedUndelegations) { + t.Errorf("Undelegations count mismatch: got %d, expected %d. Description: %s", + len(del.Undelegations), len(test.expectedUndelegations), test.description) + t.Logf("Got undelegations: %+v", del.Undelegations) + t.Logf("Expected undelegations: %+v", test.expectedUndelegations) + } else { + for i, expectedUndel := range test.expectedUndelegations { + if i >= len(del.Undelegations) { + t.Errorf("Missing undelegation at index %d", i) + continue + } + actualUndel := del.Undelegations[i] + if actualUndel.Amount.Cmp(expectedUndel.amount) != 0 { + t.Errorf("Undelegation[%d] amount mismatch: got %v, expected %v. Description: %s", + i, actualUndel.Amount, expectedUndel.amount, test.description) + } + if actualUndel.Epoch.Cmp(expectedUndel.epoch) != 0 { + t.Errorf("Undelegation[%d] epoch mismatch: got %v, expected %v", + i, actualUndel.Epoch, expectedUndel.epoch) + } + } + } + break + } + } + break + } + } + + if !foundDelegation { + t.Fatal("Could not find delegation in result") + } + }) + } +} From a92c530492892cf44839c4ec7fbd14e4918cf649 Mon Sep 17 00:00:00 2001 From: GheisMohammadi Date: Wed, 31 Dec 2025 23:39:29 +0800 Subject: [PATCH 3/5] Add batch delegation/undelegation operations for StakingV2 - Add BatchDelegate, BatchUndelegate, and UndelegateAll message types - Implement batch operations using DelegationIndex pattern (same as CollectRewards) - Add verification functions: VerifyAndBatchDelegateFromMsg, VerifyAndBatchUndelegateFromMsg, VerifyAndUndelegateAllFromMsg - Integrate batch operations into EVM context and transaction processing - Add StakingV2 epoch checks to gate new features - Update RLP encoding/decoding and transaction validation - UndelegateAll automatically finds and undelegates all active delegations --- core/blockchain_impl.go | 18 +++ core/evm.go | 190 +++++++++++++++++++++++++++++ core/staking_verifier.go | 229 +++++++++++++++++++++++++++++++++++ core/state_transition.go | 39 ++++++ core/tx_pool.go | 81 +++++++++++++ core/types/transaction.go | 16 ++- core/vm/evm.go | 6 + staking/types/messages.go | 159 ++++++++++++++++++++++++ staking/types/transaction.go | 6 + 9 files changed, 741 insertions(+), 3 deletions(-) diff --git a/core/blockchain_impl.go b/core/blockchain_impl.go index 0b0b084e7a..29a30dd84e 100644 --- a/core/blockchain_impl.go +++ b/core/blockchain_impl.go @@ -3190,6 +3190,24 @@ func (bc *BlockChainImpl) prepareStakingMetaData( case staking.DirectiveUndelegate: case staking.DirectiveCollectRewards: + case staking.DirectiveBatchDelegate: + batchDelegate := decodePayload.(*staking.BatchDelegate) + for _, delegationAction := range batchDelegate.Delegations { + delegate := &staking.Delegate{ + DelegatorAddress: batchDelegate.DelegatorAddress, + ValidatorAddress: delegationAction.ValidatorAddress, + Amount: delegationAction.Amount, + } + if err := processDelegateMetadata(delegate, + newDelegations, + state, + bc, + blockNum); err != nil { + return nil, nil, err + } + } + case staking.DirectiveBatchUndelegate: + case staking.DirectiveUndelegateAll: default: } } diff --git a/core/evm.go b/core/evm.go index d959bf990f..beee0c6d58 100644 --- a/core/evm.go +++ b/core/evm.go @@ -93,6 +93,9 @@ func NewEVMBlockContext(msg Message, header *block.Header, chain ChainContext, a Delegate: DelegateFn(header, chain), Undelegate: UndelegateFn(header, chain), CollectRewards: CollectRewardsFn(header, chain), + BatchDelegate: BatchDelegateFn(header, chain), + BatchUndelegate: BatchUndelegateFn(header, chain), + UndelegateAll: UndelegateAllFn(header, chain), CalculateMigrationGas: CalculateMigrationGasFn(chain), ShardID: chain.ShardID(), NumShards: shard.Schedule.InstanceForEpoch(header.Epoch()).NumShards(), @@ -329,6 +332,193 @@ func CollectRewardsFn(ref *block.Header, chain ChainContext) vm.CollectRewardsFu } } +func BatchDelegateFn(ref *block.Header, chain ChainContext) vm.BatchDelegateFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, batchDelegate *stakingTypes.BatchDelegate) error { + delegations, err := chain.ReadDelegationsByDelegatorAt(batchDelegate.DelegatorAddress, big.NewInt(0).Sub(ref.Number(), big.NewInt(1))) + if err != nil { + return err + } + updatedValidatorWrappers, balanceToBeDeducted, fromLockedTokens, err := VerifyAndBatchDelegateFromMsg( + db, ref.Epoch(), batchDelegate, delegations, chain.Config()) + if err != nil { + return err + } + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + db.SubBalance(batchDelegate.DelegatorAddress, balanceToBeDeducted) + + if rosettaTracer != nil && balanceToBeDeducted.Sign() != 0 { + for _, delegationAction := range batchDelegate.Delegations { + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + }, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &delegationAction.ValidatorAddress, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + delegationAction.Amount, + ) + } + } + + if len(fromLockedTokens) > 0 { + sortedKeys := []common.Address{} + for key := range fromLockedTokens { + sortedKeys = append(sortedKeys, key) + } + sort.SliceStable(sortedKeys, func(i, j int) bool { + return bytes.Compare(sortedKeys[i][:], sortedKeys[j][:]) < 0 + }) + for _, key := range sortedKeys { + redelegatedToken, ok := fromLockedTokens[key] + if !ok { + return errors.New("Key missing for delegation receipt") + } + encodedRedelegationData := []byte{} + addrBytes := key.Bytes() + encodedRedelegationData = append(encodedRedelegationData, addrBytes...) + encodedRedelegationData = append(encodedRedelegationData, redelegatedToken.Bytes()...) + db.AddLog(&types.Log{ + Address: batchDelegate.DelegatorAddress, + Topics: []common.Hash{staking.DelegateTopic}, + Data: encodedRedelegationData, + BlockNumber: ref.Number().Uint64(), + }) + + if rosettaTracer != nil { + fromAccount := common.BytesToAddress(key.Bytes()) + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &fromAccount, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &fromAccount, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + redelegatedToken, + ) + } + } + } + return nil + } +} + +func BatchUndelegateFn(ref *block.Header, chain ChainContext) vm.BatchUndelegateFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, batchUndelegate *stakingTypes.BatchUndelegate) error { + updatedValidatorWrappers, err := VerifyAndBatchUndelegateFromMsg(db, ref.Epoch(), batchUndelegate) + if err != nil { + return err + } + + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + if rosettaTracer != nil { + for i, delegationIndex := range batchUndelegate.DelegationIndexes { + amount := batchUndelegate.Amounts[i] + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchUndelegate.DelegatorAddress, + SubAccount: &delegationIndex.ValidatorAddress, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &batchUndelegate.DelegatorAddress, + SubAccount: &delegationIndex.ValidatorAddress, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + amount, + ) + } + } + + return nil + } +} + +func UndelegateAllFn(ref *block.Header, chain ChainContext) vm.UndelegateAllFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, undelegateAll *stakingTypes.UndelegateAll) error { + if chain == nil { + return errors.New("[UndelegateAll] No chain context provided") + } + delegations, err := chain.ReadDelegationsByDelegatorAt(undelegateAll.DelegatorAddress, big.NewInt(0).Sub(ref.Number(), big.NewInt(1))) + if err != nil { + return err + } + + // Track original amounts before undelegation for rosetta logging + originalAmounts := map[common.Address]*big.Int{} + for _, delegationIndex := range delegations { + if !db.IsValidator(delegationIndex.ValidatorAddress) { + continue + } + wrapper, err := db.ValidatorWrapper(delegationIndex.ValidatorAddress, false, false) + if err != nil { + continue + } + if uint64(len(wrapper.Delegations)) <= delegationIndex.Index { + continue + } + delegation := &wrapper.Delegations[delegationIndex.Index] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), undelegateAll.DelegatorAddress.Bytes()) { + continue + } + if delegation.Amount.Cmp(common.Big0) > 0 { + originalAmounts[delegationIndex.ValidatorAddress] = new(big.Int).Set(delegation.Amount) + } + } + + updatedValidatorWrappers, err := VerifyAndUndelegateAllFromMsg( + db, ref.Epoch(), undelegateAll, delegations, + ) + if err != nil { + return err + } + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + if rosettaTracer != nil { + for validatorAddr, amount := range originalAmounts { + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &undelegateAll.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &undelegateAll.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + amount, + ) + } + } + + return nil + } +} + //func MigrateDelegationsFn(ref *block.Header, chain ChainContext) vm.MigrateDelegationsFunc { // return func(db vm.StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) { // // get existing delegations diff --git a/core/staking_verifier.go b/core/staking_verifier.go index be04022486..a6502f332a 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -454,6 +454,235 @@ func VerifyAndUndelegateFromMsg( return nil, errNoDelegationToUndelegate } +// VerifyAndBatchDelegateFromMsg verifies batch delegation message using the stateDB +// and returns all updated validator wrappers, total balance to be deducted, and locked tokens map. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +func VerifyAndBatchDelegateFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.BatchDelegate, delegations []staking.DelegationIndex, chainConfig *params.ChainConfig, +) ([]*staking.ValidatorWrapper, *big.Int, map[common.Address]*big.Int, error) { + if stateDB == nil { + return nil, nil, nil, errStateDBIsMissing + } + if epoch == nil { + return nil, nil, nil, errEpochMissing + } + if chainConfig == nil { + return nil, nil, nil, errors.New("chain config is required") + } + if !chainConfig.IsStakingV2(epoch) { + return nil, nil, nil, errors.New("batch delegation is only available in StakingV2 epoch") + } + if len(msg.Delegations) == 0 { + return nil, nil, nil, errors.New("batch delegation must contain at least one delegation") + } + + allUpdatedWrappers := []*staking.ValidatorWrapper{} + totalBalanceToDeduct := big.NewInt(0) + allFromLockedTokens := map[common.Address]*big.Int{} + wrapperMap := map[common.Address]*staking.ValidatorWrapper{} + + for _, delegationAction := range msg.Delegations { + if !stateDB.IsValidator(delegationAction.ValidatorAddress) { + return nil, nil, nil, errValidatorNotExist + } + if delegationAction.Amount == nil || delegationAction.Amount.Sign() == -1 { + return nil, nil, nil, errNegativeAmount + } + if delegationAction.Amount.Cmp(minimumDelegation) < 0 { + if chainConfig.IsMinDelegation100(epoch) { + if delegationAction.Amount.Cmp(minimumDelegationV2) < 0 { + return nil, nil, nil, errDelegationTooSmallV2 + } + } else { + return nil, nil, nil, errDelegationTooSmall + } + } + + delegateMsg := &staking.Delegate{ + DelegatorAddress: msg.DelegatorAddress, + ValidatorAddress: delegationAction.ValidatorAddress, + Amount: delegationAction.Amount, + } + + updatedWrappers, balanceToDeduct, fromLockedTokens, err := VerifyAndDelegateFromMsg( + stateDB, epoch, delegateMsg, delegations, chainConfig, + ) + if err != nil { + return nil, nil, nil, err + } + + for _, wrapper := range updatedWrappers { + if existingWrapper, exists := wrapperMap[wrapper.Address]; exists { + if existingWrapper != wrapper { + return nil, nil, nil, errors.New("duplicate validator wrapper in batch delegation") + } + } else { + wrapperMap[wrapper.Address] = wrapper + allUpdatedWrappers = append(allUpdatedWrappers, wrapper) + } + } + + totalBalanceToDeduct.Add(totalBalanceToDeduct, balanceToDeduct) + + for validatorAddr, amount := range fromLockedTokens { + if existingAmount, exists := allFromLockedTokens[validatorAddr]; exists { + allFromLockedTokens[validatorAddr] = new(big.Int).Add(existingAmount, amount) + } else { + allFromLockedTokens[validatorAddr] = new(big.Int).Set(amount) + } + } + } + + if totalBalanceToDeduct.Cmp(big.NewInt(0)) > 0 { + if !CanTransfer(stateDB, msg.DelegatorAddress, totalBalanceToDeduct) { + return nil, nil, nil, errors.Wrapf( + errInsufficientBalanceForStake, "insufficient balance for batch delegation: %v", + totalBalanceToDeduct, + ) + } + } + + return allUpdatedWrappers, totalBalanceToDeduct, allFromLockedTokens, nil +} + +// VerifyAndBatchUndelegateFromMsg verifies batch undelegation message using the stateDB +// and returns all updated validator wrappers. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +func VerifyAndBatchUndelegateFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.BatchUndelegate, +) ([]*staking.ValidatorWrapper, error) { + if stateDB == nil { + return nil, errStateDBIsMissing + } + if epoch == nil { + return nil, errEpochMissing + } + if len(msg.DelegationIndexes) == 0 { + return nil, errors.New("batch undelegation must contain at least one delegation index") + } + if len(msg.DelegationIndexes) != len(msg.Amounts) { + return nil, errors.New("delegation indexes and amounts must have the same length") + } + + allUpdatedWrappers := []*staking.ValidatorWrapper{} + wrapperMap := map[common.Address]*staking.ValidatorWrapper{} + + for i, delegationIndex := range msg.DelegationIndexes { + amount := msg.Amounts[i] + if amount == nil || amount.Sign() == -1 { + return nil, errNegativeAmount + } + + if !stateDB.IsValidator(delegationIndex.ValidatorAddress) { + return nil, errValidatorNotExist + } + + var wrapper *staking.ValidatorWrapper + var exists bool + if wrapper, exists = wrapperMap[delegationIndex.ValidatorAddress]; !exists { + var err error + wrapper, err = stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, true) + if err != nil { + return nil, err + } + wrapperMap[delegationIndex.ValidatorAddress] = wrapper + } + + if uint64(len(wrapper.Delegations)) <= delegationIndex.Index { + utils.Logger().Warn(). + Str("validator", delegationIndex.ValidatorAddress.String()). + Uint64("delegation index", delegationIndex.Index). + Int("delegations length", len(wrapper.Delegations)). + Msg("Delegation index out of bound") + return nil, errors.New("Delegation index out of bound") + } + + delegation := &wrapper.Delegations[delegationIndex.Index] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + return nil, errors.New("delegator address mismatch") + } + + if err := delegation.Undelegate(epoch, amount); err != nil { + return nil, err + } + } + + for _, wrapper := range wrapperMap { + if err := wrapper.SanityCheck(); err != nil { + if errors.Cause(err) == staking.ErrInvalidSelfDelegation { + wrapper.Status = effective.Inactive + } else { + return nil, err + } + } + allUpdatedWrappers = append(allUpdatedWrappers, wrapper) + } + + return allUpdatedWrappers, nil +} + +// VerifyAndUndelegateAllFromMsg verifies and prepares undelegation of all delegations +// for a delegator. It reads all delegations and creates a batch undelegation. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +func VerifyAndUndelegateAllFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.UndelegateAll, delegations []staking.DelegationIndex, +) ([]*staking.ValidatorWrapper, error) { + if stateDB == nil { + return nil, errStateDBIsMissing + } + if epoch == nil { + return nil, errEpochMissing + } + if len(delegations) == 0 { + return nil, errors.New("no delegations to undelegate") + } + + delegationIndexes := []staking.DelegationIndex{} + amounts := []*big.Int{} + + for _, delegationIndex := range delegations { + if !stateDB.IsValidator(delegationIndex.ValidatorAddress) { + continue + } + + wrapper, err := stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, false) + if err != nil { + return nil, err + } + + if uint64(len(wrapper.Delegations)) <= delegationIndex.Index { + continue + } + + delegation := &wrapper.Delegations[delegationIndex.Index] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + continue + } + + if delegation.Amount.Cmp(common.Big0) <= 0 { + continue + } + + delegationIndexes = append(delegationIndexes, delegationIndex) + amounts = append(amounts, new(big.Int).Set(delegation.Amount)) + } + + if len(delegationIndexes) == 0 { + return nil, errors.New("no active delegations to undelegate") + } + + batchUndelegateMsg := &staking.BatchUndelegate{ + DelegatorAddress: msg.DelegatorAddress, + DelegationIndexes: delegationIndexes, + Amounts: amounts, + } + + return VerifyAndBatchUndelegateFromMsg(stateDB, epoch, batchUndelegateMsg) +} + // VerifyAndMigrateFromMsg verifies and transfers all delegations of // msg.From to msg.To. Returns all modified validator wrappers and delegate msgs // for metadata diff --git a/core/state_transition.go b/core/state_transition.go index 15003a487b..bf6687916e 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -413,6 +413,45 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) { return 0, errInvalidSigner } err = st.evm.Context.CollectRewards(st.evm.StateDB, nil, stkMsg) + case types.BatchDelegate: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("batch delegation is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.BatchDelegate{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.BatchDelegate(st.evm.StateDB, nil, stkMsg) + case types.BatchUndelegate: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("batch undelegation is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.BatchUndelegate{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.BatchUndelegate(st.evm.StateDB, nil, stkMsg) + case types.UndelegateAll: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("undelegate all is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.UndelegateAll{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.UndelegateAll(st.evm.StateDB, nil, stkMsg) default: return 0, stakingTypes.ErrInvalidStakingKind } diff --git a/core/tx_pool.go b/core/tx_pool.go index eaa6fffe37..817fd19547 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -903,6 +903,87 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { _, _, err = VerifyAndCollectRewardsFromDelegation(pool.currentState, delegations) return err + case staking.DirectiveBatchDelegate: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("batch delegation is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveBatchDelegate) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.BatchDelegate) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(stkMsg.DelegatorAddress) + if err != nil { + return err + } + _, delegateAmt, _, err := VerifyAndBatchDelegateFromMsg( + pool.currentState, pendingEpoch, stkMsg, delegations, pool.chainconfig) + if err != nil { + return err + } + gasAmt := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.GasLimit())) + totalAmt := new(big.Int).Add(delegateAmt, gasAmt) + if bal := pool.currentState.GetBalance(from); bal.Cmp(totalAmt) < 0 { + return fmt.Errorf("not enough balance for batch delegation: %v < %v", bal, delegateAmt) + } + return nil + case staking.DirectiveBatchUndelegate: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("batch undelegation is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveBatchUndelegate) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.BatchUndelegate) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + _, err = VerifyAndBatchUndelegateFromMsg(pool.currentState, pendingEpoch, stkMsg) + return err + case staking.DirectiveUndelegateAll: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("undelegate all is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveUndelegateAll) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.UndelegateAll) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(stkMsg.DelegatorAddress) + if err != nil { + return err + } + _, err = VerifyAndUndelegateAllFromMsg(pool.currentState, pendingEpoch, stkMsg, delegations) + return err default: return staking.ErrInvalidStakingKind } diff --git a/core/types/transaction.go b/core/types/transaction.go index 1364ccfac5..bc0a89dd3a 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -63,12 +63,22 @@ const ( Delegate Undelegate CollectRewards + BatchDelegate + BatchUndelegate + UndelegateAll ) // StakingTypeMap is the map from staking type to transactionType -var StakingTypeMap = map[staking.Directive]TransactionType{staking.DirectiveCreateValidator: StakeCreateVal, - staking.DirectiveEditValidator: StakeEditVal, staking.DirectiveDelegate: Delegate, - staking.DirectiveUndelegate: Undelegate, staking.DirectiveCollectRewards: CollectRewards} +var StakingTypeMap = map[staking.Directive]TransactionType{ + staking.DirectiveCreateValidator: StakeCreateVal, + staking.DirectiveEditValidator: StakeEditVal, + staking.DirectiveDelegate: Delegate, + staking.DirectiveUndelegate: Undelegate, + staking.DirectiveCollectRewards: CollectRewards, + staking.DirectiveBatchDelegate: BatchDelegate, + staking.DirectiveBatchUndelegate: BatchUndelegate, + staking.DirectiveUndelegateAll: UndelegateAll, +} // InternalTransaction defines the common interface for harmony and ethereum transactions. type InternalTransaction interface { diff --git a/core/vm/evm.go b/core/vm/evm.go index 764f51f4f1..d13499bb69 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -61,6 +61,9 @@ type ( DelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.Delegate) error UndelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.Undelegate) error CollectRewardsFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.CollectRewards) error + BatchDelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.BatchDelegate) error + BatchUndelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.BatchUndelegate) error + UndelegateAllFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.UndelegateAll) error // Used for migrating delegations via the staking precompile //MigrateDelegationsFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) CalculateMigrationGasFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool) (uint64, error) @@ -180,6 +183,9 @@ type BlockContext struct { Delegate DelegateFunc Undelegate UndelegateFunc CollectRewards CollectRewardsFunc + BatchDelegate BatchDelegateFunc + BatchUndelegate BatchUndelegateFunc + UndelegateAll UndelegateAllFunc CalculateMigrationGas CalculateMigrationGasFunc ShardID uint32 // Used by staking and cross shard transfer precompile diff --git a/staking/types/messages.go b/staking/types/messages.go index bddcbacf0b..1102f83868 100644 --- a/staking/types/messages.go +++ b/staking/types/messages.go @@ -27,6 +27,12 @@ const ( DirectiveUndelegate // DirectiveCollectRewards ... DirectiveCollectRewards + // DirectiveBatchDelegate ... + DirectiveBatchDelegate + // DirectiveBatchUndelegate ... + DirectiveBatchUndelegate + // DirectiveUndelegateAll ... + DirectiveUndelegateAll ) var ( @@ -36,6 +42,9 @@ var ( DirectiveDelegate: "Delegate", DirectiveUndelegate: "Undelegate", DirectiveCollectRewards: "CollectRewards", + DirectiveBatchDelegate: "BatchDelegate", + DirectiveBatchUndelegate: "BatchUndelegate", + DirectiveUndelegateAll: "UndelegateAll", } // ErrInvalidStakingKind given when caller gives bad staking message kind ErrInvalidStakingKind = errors.New("bad staking kind") @@ -263,3 +272,153 @@ func (v MigrationMsg) Copy() MigrationMsg { func (v MigrationMsg) Equals(s MigrationMsg) bool { return v.From == s.From && v.To == s.To } + +// DelegationAction represents a single delegation action in a batch operation +type DelegationAction struct { + ValidatorAddress common.Address `json:"validator_address"` + Amount *big.Int `json:"amount"` +} + +// BatchDelegate - type for delegating to multiple validators in one transaction +type BatchDelegate struct { + DelegatorAddress common.Address `json:"delegator_address"` + Delegations []DelegationAction `json:"delegations"` +} + +// Type of BatchDelegate +func (v BatchDelegate) Type() Directive { + return DirectiveBatchDelegate +} + +// Copy returns a deep copy of the BatchDelegate as a StakeMsg interface +func (v BatchDelegate) Copy() StakeMsg { + cp := BatchDelegate{ + DelegatorAddress: v.DelegatorAddress, + Delegations: make([]DelegationAction, len(v.Delegations)), + } + for i, d := range v.Delegations { + cp.Delegations[i] = DelegationAction{ + ValidatorAddress: d.ValidatorAddress, + } + if d.Amount != nil { + cp.Delegations[i].Amount = new(big.Int).Set(d.Amount) + } + } + return cp +} + +// Equals returns if v and s are equal +func (v BatchDelegate) Equals(s BatchDelegate) bool { + if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) { + return false + } + if len(v.Delegations) != len(s.Delegations) { + return false + } + for i := range v.Delegations { + if !bytes.Equal(v.Delegations[i].ValidatorAddress.Bytes(), s.Delegations[i].ValidatorAddress.Bytes()) { + return false + } + if v.Delegations[i].Amount == nil { + if s.Delegations[i].Amount != nil { + return false + } + } else if s.Delegations[i].Amount == nil { + return false + } else if v.Delegations[i].Amount.Cmp(s.Delegations[i].Amount) != 0 { + return false + } + } + return true +} + +// BatchUndelegate - type for undelegating from multiple validators in one transaction +type BatchUndelegate struct { + DelegatorAddress common.Address `json:"delegator_address"` + DelegationIndexes []DelegationIndex `json:"delegation_indexes"` + Amounts []*big.Int `json:"amounts"` +} + +// Type of BatchUndelegate +func (v BatchUndelegate) Type() Directive { + return DirectiveBatchUndelegate +} + +// Copy returns a deep copy of the BatchUndelegate as a StakeMsg interface +func (v BatchUndelegate) Copy() StakeMsg { + cp := BatchUndelegate{ + DelegatorAddress: v.DelegatorAddress, + DelegationIndexes: make([]DelegationIndex, len(v.DelegationIndexes)), + Amounts: make([]*big.Int, len(v.Amounts)), + } + for i, idx := range v.DelegationIndexes { + cp.DelegationIndexes[i] = DelegationIndex{ + ValidatorAddress: idx.ValidatorAddress, + Index: idx.Index, + } + if idx.BlockNum != nil { + cp.DelegationIndexes[i].BlockNum = new(big.Int).Set(idx.BlockNum) + } + } + for i, amt := range v.Amounts { + if amt != nil { + cp.Amounts[i] = new(big.Int).Set(amt) + } + } + return cp +} + +// Equals returns if v and s are equal +func (v BatchUndelegate) Equals(s BatchUndelegate) bool { + if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) { + return false + } + if len(v.DelegationIndexes) != len(s.DelegationIndexes) { + return false + } + if len(v.Amounts) != len(s.Amounts) { + return false + } + for i := range v.DelegationIndexes { + if !bytes.Equal(v.DelegationIndexes[i].ValidatorAddress.Bytes(), s.DelegationIndexes[i].ValidatorAddress.Bytes()) { + return false + } + if v.DelegationIndexes[i].Index != s.DelegationIndexes[i].Index { + return false + } + } + for i := range v.Amounts { + if v.Amounts[i] == nil { + if s.Amounts[i] != nil { + return false + } + } else if s.Amounts[i] == nil { + return false + } else if v.Amounts[i].Cmp(s.Amounts[i]) != 0 { + return false + } + } + return true +} + +// UndelegateAll - type for undelegating all from all validators +type UndelegateAll struct { + DelegatorAddress common.Address `json:"delegator_address"` +} + +// Type of UndelegateAll +func (v UndelegateAll) Type() Directive { + return DirectiveUndelegateAll +} + +// Copy returns a deep copy of the UndelegateAll as a StakeMsg interface +func (v UndelegateAll) Copy() StakeMsg { + return UndelegateAll{ + DelegatorAddress: v.DelegatorAddress, + } +} + +// Equals returns if v and s are equal +func (v UndelegateAll) Equals(s UndelegateAll) bool { + return bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) +} diff --git a/staking/types/transaction.go b/staking/types/transaction.go index c9923bcdfe..0d011bcd74 100644 --- a/staking/types/transaction.go +++ b/staking/types/transaction.go @@ -299,6 +299,12 @@ func RLPDecodeStakeMsg(payload []byte, d Directive) (interface{}, error) { ds = &Undelegate{} case DirectiveCollectRewards: ds = &CollectRewards{} + case DirectiveBatchDelegate: + ds = &BatchDelegate{} + case DirectiveBatchUndelegate: + ds = &BatchUndelegate{} + case DirectiveUndelegateAll: + ds = &UndelegateAll{} default: return nil, nil } From 8a55e9ee613022f35dc878e43634b33623388695 Mon Sep 17 00:00:00 2001 From: GheisMohammadi Date: Wed, 31 Dec 2025 23:54:09 +0800 Subject: [PATCH 4/5] Add tests for batch delegation/undelegation operations - Add TestVerifyAndBatchDelegateFromMsg with comprehensive test cases - Add TestVerifyAndBatchUndelegateFromMsg with error cases - Add TestVerifyAndUndelegateAllFromMsg for undelegate all functionality - Tests follow same pattern as existing delegate/undelegate tests - Include StakingV2 epoch validation tests --- core/staking_verifier_test.go | 482 ++++++++++++++++++++++++++++++++++ 1 file changed, 482 insertions(+) diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index cedc206458..91e751d90c 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -2177,3 +2177,485 @@ func TestRedelegationCornerCases(t *testing.T) { }) } } + +func TestVerifyAndBatchDelegateFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + stakingV2Epoch := big.NewInt(defaultEpoch) + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.BatchDelegate + delegations []staking.DelegationIndex + chainConfig *params.ChainConfig + + expVWrappers []staking.ValidatorWrapper + expAmt *big.Int + expRedel map[common.Address]*big.Int + expErr error + }{ + { + name: "successful batch delegate to two validators", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + {ValidatorAddress: validatorAddr2, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + config.MinDelegation100Epoch = big.NewInt(100) + return config + }(), + expVWrappers: func() []staking.ValidatorWrapper { + w1 := makeVWrapperByIndex(validatorIndex) + w1.Delegations = append(w1.Delegations, staking.NewDelegation(delegatorAddr, tenKOnes)) + w2 := makeVWrapperByIndex(validator2Index) + w2.Delegations = append(w2.Delegations, staking.NewDelegation(delegatorAddr, fiveKOnes)) + return []staking.ValidatorWrapper{w1, w2} + }(), + expAmt: new(big.Int).Add(tenKOnes, fiveKOnes), + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeStateDBForStake(t), + epoch: nil, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errEpochMissing, + }, + { + name: "not StakingV2 epoch", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = big.NewInt(10000000) // Disabled + return config + }(), + expErr: errors.New("batch delegation is only available in StakingV2 epoch"), + }, + { + name: "empty delegations", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{}, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errors.New("batch delegation must contain at least one delegation"), + }, + { + name: "invalid validator", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: makeTestAddr("not exist"), Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errValidatorNotExist, + }, + { + name: "negative amount", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: big.NewInt(-1)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errNegativeAmount, + }, + { + name: "insufficient balance", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + sdb.SetBalance(delegatorAddr, big.NewInt(100)) + return sdb + }(), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errInsufficientBalanceForStake, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, amt, amtRedel, err := VerifyAndBatchDelegateFromMsg( + test.sdb, test.epoch, &test.msg, test.delegations, test.chainConfig, + ) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if amt.Cmp(test.expAmt) != 0 { + t.Errorf("Test %v: unexpected amount %v / %v", i, amt, test.expAmt) + } + + if len(amtRedel) != len(test.expRedel) { + t.Errorf("Test %v: wrong expected redelegation length %d / %d", i, len(amtRedel), len(test.expRedel)) + } else { + for key, value := range test.expRedel { + actValue, ok := amtRedel[key] + if !ok { + t.Errorf("Test %v: missing expected redelegation key/value %v / %v", i, key, value) + } + if value.Cmp(actValue) != 0 { + t.Errorf("Test %v: unexpected redelegation value %v / %v", i, actValue, value) + } + } + } + + if len(ws) != len(test.expVWrappers) { + t.Errorf("Test %v: wrong wrapper count %d / %d", i, len(ws), len(test.expVWrappers)) + return + } + + for j := range ws { + if err := staketest.CheckValidatorWrapperEqual(*ws[j], test.expVWrappers[j]); err != nil { + t.Errorf("Test %v wrapper %v: %v", i, j, err) + } + } + }) + } +} + +func TestVerifyAndBatchUndelegateFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.BatchUndelegate + expErr error + }{ + { + name: "successful batch undelegate from two validators", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + w2 := makeVWrapperByIndex(validator2Index) + newDelegation2 := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + w2.Delegations = append(w2.Delegations, newDelegation2) + if err := sdb.UpdateValidatorWrapper(validatorAddr2, &w2); err != nil { + t.Fatal(err) + } + sdb.IntermediateRoot(true) + return sdb + }(), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + {ValidatorAddress: validatorAddr2, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{ + new(big.Int).Set(fiveKOnes), + new(big.Int).Set(fiveKOnes), + }, + }, + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeDefaultStateForUndelegate(t), + epoch: nil, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errEpochMissing, + }, + { + name: "empty delegation indexes", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{}, + Amounts: []*big.Int{}, + }, + expErr: errors.New("batch undelegation must contain at least one delegation index"), + }, + { + name: "mismatched lengths", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + {ValidatorAddress: validatorAddr2, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errors.New("delegation indexes and amounts must have the same length"), + }, + { + name: "invalid validator", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: makeTestAddr("not exist"), Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errValidatorNotExist, + }, + { + name: "negative amount", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{big.NewInt(-1)}, + }, + expErr: errNegativeAmount, + }, + { + name: "delegation index out of bound", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 999, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errors.New("Delegation index out of bound"), + }, + { + name: "delegator address mismatch", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.BatchUndelegate{ + DelegatorAddress: makeTestAddr("wrong delegator"), + DelegationIndexes: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + Amounts: []*big.Int{new(big.Int).Set(fiveKOnes)}, + }, + expErr: errors.New("delegator address mismatch"), + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, err := VerifyAndBatchUndelegateFromMsg(test.sdb, test.epoch, &test.msg) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if len(ws) == 0 { + t.Errorf("Test %v: expected at least one wrapper", i) + } + }) + } +} + +func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.UndelegateAll + delegations []staking.DelegationIndex + expErr error + }{ + { + name: "successful undelegate all", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + w2 := makeVWrapperByIndex(validator2Index) + newDelegation2 := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + w2.Delegations = append(w2.Delegations, newDelegation2) + if err := sdb.UpdateValidatorWrapper(validatorAddr2, &w2); err != nil { + t.Fatal(err) + } + sdb.IntermediateRoot(true) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: func() []staking.DelegationIndex { + return []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + {ValidatorAddress: validatorAddr2, Index: 1, BlockNum: big.NewInt(100)}, + } + }(), + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeDefaultStateForUndelegate(t), + epoch: nil, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + expErr: errEpochMissing, + }, + { + name: "no delegations", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + expErr: errors.New("no delegations to undelegate"), + }, + { + name: "no active delegations", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + w, _ := sdb.ValidatorWrapper(validatorAddr, false, true) + delegation := staking.NewDelegation(delegatorAddr, big.NewInt(0)) + w.Delegations = append(w.Delegations, delegation) + sdb.UpdateValidatorWrapper(validatorAddr, w) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + expErr: errors.New("no active delegations to undelegate"), + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, err := VerifyAndUndelegateAllFromMsg(test.sdb, test.epoch, &test.msg, test.delegations) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if len(ws) == 0 { + t.Errorf("Test %v: expected at least one wrapper", i) + } + }) + } +} From 66306cd7905e5e13a42cc577f8fdaa72e7a958f1 Mon Sep 17 00:00:00 2001 From: GheisMohammadi Date: Thu, 1 Jan 2026 00:03:31 +0800 Subject: [PATCH 5/5] Fix UndelegateAll to include delegations created in same block - Update VerifyAndUndelegateAllFromMsg to scan all validators in current state - Ensures delegations created in same block are included when calling UndelegateAll - Add ChainContext parameter to enable validator list scanning - Update all call sites (evm.go, tx_pool.go, tests) to pass chainContext - Prevents missing delegations when user delegates then immediately calls UndelegateAll --- core/evm.go | 2 +- core/staking_verifier.go | 69 ++++++++++++++++++++++++++++++++--- core/staking_verifier_test.go | 31 ++++++++++++++-- core/tx_pool.go | 2 +- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/core/evm.go b/core/evm.go index beee0c6d58..3af82242bf 100644 --- a/core/evm.go +++ b/core/evm.go @@ -485,7 +485,7 @@ func UndelegateAllFn(ref *block.Header, chain ChainContext) vm.UndelegateAllFunc } updatedValidatorWrappers, err := VerifyAndUndelegateAllFromMsg( - db, ref.Epoch(), undelegateAll, delegations, + db, ref.Epoch(), undelegateAll, delegations, chain, ) if err != nil { return err diff --git a/core/staking_verifier.go b/core/staking_verifier.go index a6502f332a..964517976d 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -624,11 +624,12 @@ func VerifyAndBatchUndelegateFromMsg( } // VerifyAndUndelegateAllFromMsg verifies and prepares undelegation of all delegations -// for a delegator. It reads all delegations and creates a batch undelegation. +// for a delegator. It reads all delegations from the current state and creates a batch undelegation. +// This ensures delegations created in the same block are included. // // Note that this function never updates the stateDB, it only reads from stateDB. func VerifyAndUndelegateAllFromMsg( - stateDB vm.StateDB, epoch *big.Int, msg *staking.UndelegateAll, delegations []staking.DelegationIndex, + stateDB vm.StateDB, epoch *big.Int, msg *staking.UndelegateAll, delegations []staking.DelegationIndex, chainContext ChainContext, ) ([]*staking.ValidatorWrapper, error) { if stateDB == nil { return nil, errStateDBIsMissing @@ -636,13 +637,12 @@ func VerifyAndUndelegateAllFromMsg( if epoch == nil { return nil, errEpochMissing } - if len(delegations) == 0 { - return nil, errors.New("no delegations to undelegate") - } delegationIndexes := []staking.DelegationIndex{} amounts := []*big.Int{} + processedValidators := map[common.Address]map[uint64]bool{} + // First, process delegations from the provided list (from previous block) for _, delegationIndex := range delegations { if !stateDB.IsValidator(delegationIndex.ValidatorAddress) { continue @@ -650,7 +650,7 @@ func VerifyAndUndelegateAllFromMsg( wrapper, err := stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, false) if err != nil { - return nil, err + continue } if uint64(len(wrapper.Delegations)) <= delegationIndex.Index { @@ -668,9 +668,66 @@ func VerifyAndUndelegateAllFromMsg( delegationIndexes = append(delegationIndexes, delegationIndex) amounts = append(amounts, new(big.Int).Set(delegation.Amount)) + + // Track processed delegations to avoid duplicates + if processedValidators[delegationIndex.ValidatorAddress] == nil { + processedValidators[delegationIndex.ValidatorAddress] = make(map[uint64]bool) + } + processedValidators[delegationIndex.ValidatorAddress][delegationIndex.Index] = true } + // Then, scan all validators in current state to find any new delegations created in this block + if chainContext != nil { + validatorList, err := chainContext.ReadValidatorList() + if err == nil { + for _, validatorAddr := range validatorList { + if !stateDB.IsValidator(validatorAddr) { + continue + } + + wrapper, err := stateDB.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + continue + } + + // Check all delegations for this delegator + for i := range wrapper.Delegations { + delegation := &wrapper.Delegations[i] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + continue + } + + if delegation.Amount.Cmp(common.Big0) <= 0 { + continue + } + + // Skip if already processed + if processedValidators[validatorAddr] != nil && processedValidators[validatorAddr][uint64(i)] { + continue + } + + // Found a new delegation (created in this block) + delegationIndexes = append(delegationIndexes, staking.DelegationIndex{ + ValidatorAddress: validatorAddr, + Index: uint64(i), + BlockNum: big.NewInt(0), + }) + amounts = append(amounts, new(big.Int).Set(delegation.Amount)) + + if processedValidators[validatorAddr] == nil { + processedValidators[validatorAddr] = make(map[uint64]bool) + } + processedValidators[validatorAddr][uint64(i)] = true + } + } + } + } + + // If no delegations found and no chain context to scan, return error if len(delegationIndexes) == 0 { + if chainContext == nil && len(delegations) == 0 { + return nil, errors.New("no delegations to undelegate") + } return nil, errors.New("no active delegations to undelegate") } diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 91e751d90c..293e26ece5 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -2565,6 +2565,7 @@ func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { epoch *big.Int msg staking.UndelegateAll delegations []staking.DelegationIndex + chain ChainContext expErr error }{ { @@ -2590,6 +2591,7 @@ func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { {ValidatorAddress: validatorAddr2, Index: 1, BlockNum: big.NewInt(100)}, } }(), + chain: makeFakeChainContextForStake(), }, { name: "nil state db", @@ -2599,6 +2601,7 @@ func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { DelegatorAddress: delegatorAddr, }, delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), expErr: errStateDBIsMissing, }, { @@ -2609,17 +2612,36 @@ func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { DelegatorAddress: delegatorAddr, }, delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), expErr: errEpochMissing, }, { - name: "no delegations", - sdb: makeDefaultStateForUndelegate(t), + name: "no delegations in list but found in state scan", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), + expErr: nil, + }, + { + name: "no delegations at all", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + return sdb + }(), epoch: epoch, msg: staking.UndelegateAll{ DelegatorAddress: delegatorAddr, }, delegations: []staking.DelegationIndex{}, - expErr: errors.New("no delegations to undelegate"), + chain: makeFakeChainContextForStake(), + expErr: errors.New("no active delegations to undelegate"), }, { name: "no active delegations", @@ -2638,13 +2660,14 @@ func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { delegations: []staking.DelegationIndex{ {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, }, + chain: makeFakeChainContextForStake(), expErr: errors.New("no active delegations to undelegate"), }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { - ws, err := VerifyAndUndelegateAllFromMsg(test.sdb, test.epoch, &test.msg, test.delegations) + ws, err := VerifyAndUndelegateAllFromMsg(test.sdb, test.epoch, &test.msg, test.delegations, test.chain) if assErr := assertError(err, test.expErr); assErr != nil { t.Errorf("Test %v: %v", i, assErr) diff --git a/core/tx_pool.go b/core/tx_pool.go index 817fd19547..dc0accd6da 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -982,7 +982,7 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { if err != nil { return err } - _, err = VerifyAndUndelegateAllFromMsg(pool.currentState, pendingEpoch, stkMsg, delegations) + _, err = VerifyAndUndelegateAllFromMsg(pool.currentState, pendingEpoch, stkMsg, delegations, chain) return err default: return staking.ErrInvalidStakingKind