diff --git a/action/protocol/context.go b/action/protocol/context.go index 342ba3eae6..82e7c6e5bc 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -164,6 +164,7 @@ type ( NotUseMinSelfStakeToBeActive bool StoreVoteOfNFTBucketIntoView bool CandidateSlashByOwner bool + AllowUpdateDelegate bool } // FeatureWithHeightCtx provides feature check functions. @@ -331,6 +332,7 @@ func WithFeatureCtx(ctx context.Context) context.Context { NotUseMinSelfStakeToBeActive: !g.IsXingu(height), StoreVoteOfNFTBucketIntoView: !g.IsXingu(height), CandidateSlashByOwner: !g.IsXinguBeta(height), + AllowUpdateDelegate: !g.IsXingu(height), }, ) } diff --git a/action/protocol/staking/contractstaking/statereader_test.go b/action/protocol/staking/contractstaking/statereader_test.go index 8f607c844e..dbd0a5cd0a 100644 --- a/action/protocol/staking/contractstaking/statereader_test.go +++ b/action/protocol/staking/contractstaking/statereader_test.go @@ -8,7 +8,6 @@ import ( "go.uber.org/mock/gomock" "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/iotexproject/iotex-core/v2/state" "github.com/iotexproject/iotex-core/v2/test/identityset" "github.com/iotexproject/iotex-core/v2/test/mock/mock_chainmanager" @@ -101,7 +100,7 @@ func (d *dummyIter) Next(s interface{}) ([]byte, error) { if d.idx >= d.size { return nil, state.ErrNilValue } - *(s.(*stakingpb.SystemStakingBucket)) = stakingpb.SystemStakingBucket{} + *(s.(*Bucket)) = Bucket{} key := d.keys[d.idx] d.idx++ return key, nil diff --git a/action/protocol/staking/handler_candidate_transfer_ownership.go b/action/protocol/staking/handler_candidate_transfer_ownership.go index 247ca8e874..594b553c84 100644 --- a/action/protocol/staking/handler_candidate_transfer_ownership.go +++ b/action/protocol/staking/handler_candidate_transfer_ownership.go @@ -24,7 +24,7 @@ func (p *Protocol) handleCandidateTransferOwnership(ctx context.Context, act *ac featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), handleCandidateTransferOwnership, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, nil, fetchErr } diff --git a/action/protocol/staking/handler_stake_migrate.go b/action/protocol/staking/handler_stake_migrate.go index 84de4074c6..6709202af2 100644 --- a/action/protocol/staking/handler_stake_migrate.go +++ b/action/protocol/staking/handler_stake_migrate.go @@ -43,7 +43,7 @@ func (p *Protocol) handleStakeMigrate(ctx context.Context, elp action.Envelope, if rErr != nil { return nil, nil, gasConsumed, gasToBeDeducted, rErr } - staker, rerr := fetchCaller(ctx, csm, big.NewInt(0)) + staker, rerr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if rerr != nil { return nil, nil, gasConsumed, gasToBeDeducted, errors.Wrap(rerr, "failed to fetch caller") } diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index 10ee230b67..aaea5f2e65 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -66,7 +66,7 @@ func (p *Protocol) handleCreateStake(ctx context.Context, act *action.CreateStak featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleCreateStake, featureCtx.NewStakingReceiptFormat) - staker, fetchErr := fetchCaller(ctx, csm, act.Amount()) + staker, fetchErr := fetchCaller(ctx, csm.SM(), act.Amount()) if fetchErr != nil { return log, nil, fetchErr } @@ -136,7 +136,7 @@ func (p *Protocol) handleUnstake(ctx context.Context, act *action.Unstake, csm C featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleUnstake, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, fetchErr } @@ -229,7 +229,7 @@ func (p *Protocol) handleWithdrawStake(ctx context.Context, act *action.Withdraw featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleWithdrawStake, featureCtx.NewStakingReceiptFormat) - withdrawer, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + withdrawer, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, nil, fetchErr } @@ -307,7 +307,7 @@ func (p *Protocol) handleChangeCandidate(ctx context.Context, act *action.Change blkCtx := protocol.MustGetBlockCtx(ctx) log := newReceiptLog(p.addr.String(), HandleChangeCandidate, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, fetchErr } @@ -401,7 +401,7 @@ func (p *Protocol) handleTransferStake(ctx context.Context, act *action.Transfer featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleTransferStake, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, fetchErr } @@ -488,7 +488,7 @@ func (p *Protocol) handleDepositToStake(ctx context.Context, act *action.Deposit featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleDepositToStake, featureCtx.NewStakingReceiptFormat) - depositor, fetchErr := fetchCaller(ctx, csm, act.Amount()) + depositor, fetchErr := fetchCaller(ctx, csm.SM(), act.Amount()) if fetchErr != nil { return log, nil, fetchErr } @@ -593,7 +593,7 @@ func (p *Protocol) handleRestake(ctx context.Context, act *action.Restake, csm C featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleRestake, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, fetchErr } @@ -679,7 +679,7 @@ func (p *Protocol) handleCandidateRegister(ctx context.Context, act *action.Cand registrationFee := new(big.Int).Set(p.config.RegistrationConsts.Fee) - caller, fetchErr := fetchCaller(ctx, csm, new(big.Int).Add(act.Amount(), registrationFee)) + caller, fetchErr := fetchCaller(ctx, csm.SM(), new(big.Int).Add(act.Amount(), registrationFee)) if fetchErr != nil { return log, nil, fetchErr } @@ -842,13 +842,27 @@ func (p *Protocol) handleCandidateRegister(ctx context.Context, act *action.Cand return log, txLogs, nil } +func (p *Protocol) assertNotActiveDelegate(ctx context.Context, sr protocol.StateReader, name string) error { + if p.isDelegate == nil { + return nil + } + exists, err := p.isDelegate(ctx, sr, name) + if err != nil { + return errors.Wrap(err, "failed to check whether the candidate is a delegate") + } + if exists { + return errors.New("delegate is an active delegate") + } + return nil +} + func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.CandidateUpdate, csm CandidateStateManager, ) (*receiptLog, error) { actCtx := protocol.MustGetActionCtx(ctx) featureCtx := protocol.MustGetFeatureCtx(ctx) log := newReceiptLog(p.addr.String(), HandleCandidateUpdate, featureCtx.NewStakingReceiptFormat) - _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) + _, fetchErr := fetchCaller(ctx, csm.SM(), big.NewInt(0)) if fetchErr != nil { return log, fetchErr } @@ -858,6 +872,14 @@ func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.Candid if c == nil { return log, errCandNotExist } + if !featureCtx.AllowUpdateDelegate { + if err := p.assertNotActiveDelegate(ctx, csm.SM(), c.Name); err != nil { + return log, &handleError{ + err: err, + failureStatus: iotextypes.ReceiptStatus_ErrWriteCandidate, + } + } + } if len(act.Name()) != 0 { c.Name = act.Name() @@ -969,13 +991,13 @@ func (p *Protocol) generateCandidateID(owner address.Address, height uint64, csm return nil, errors.New("failed to generate candidate ID after max attempts") } -func fetchCaller(ctx context.Context, csm CandidateStateManager, amount *big.Int) (*state.Account, ReceiptError) { +func fetchCaller(ctx context.Context, sm protocol.StateReader, amount *big.Int) (*state.Account, ReceiptError) { actionCtx := protocol.MustGetActionCtx(ctx) accountCreationOpts := []state.AccountCreationOption{} if protocol.MustGetFeatureCtx(ctx).CreateLegacyNonceAccount { accountCreationOpts = append(accountCreationOpts, state.LegacyNonceAccountTypeOption()) } - caller, err := accountutil.LoadAccount(csm.SM(), actionCtx.Caller, accountCreationOpts...) + caller, err := accountutil.LoadAccount(sm, actionCtx.Caller, accountCreationOpts...) if err != nil { return nil, &handleError{ err: errors.Wrapf(err, "failed to load the account of caller %s", actionCtx.Caller.String()), diff --git a/action/protocol/staking/handlers_test.go b/action/protocol/staking/handlers_test.go index 7dae3aea12..f5f46edce1 100644 --- a/action/protocol/staking/handlers_test.go +++ b/action/protocol/staking/handlers_test.go @@ -699,8 +699,9 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { gasLimit uint64 blkGasLimit uint64 gasPrice *big.Int - newProtocol bool // candidate update + allowUpdate bool + isActive bool updateName string updateOperator string updateReward string @@ -725,6 +726,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "update", identityset.Address(31).String(), identityset.Address(32).String(), @@ -749,6 +751,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "update", identityset.Address(31).String(), identityset.Address(32).String(), @@ -773,6 +776,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "", "", "", @@ -797,6 +801,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "test2", "", "", @@ -821,12 +826,63 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "!invalidname", identityset.Address(31).String(), identityset.Address(32).String(), action.ErrInvalidCanName, iotextypes.ReceiptStatus_Failure, }, + // invalid update of active delegate + { + 1201000, + identityset.Address(27), + 1, + "test", + identityset.Address(27).String(), + identityset.Address(29).String(), + identityset.Address(27).String(), + "1200000000000000000000000", + "1806204150552640363969204", + uint32(10000), + false, + []byte("payload"), + uint64(1000000), + uint64(1000000), + big.NewInt(1000), + false, + true, + "update", + identityset.Address(31).String(), + identityset.Address(32).String(), + nil, + iotextypes.ReceiptStatus_ErrWriteCandidate, + }, + // success, update non-active delegate, name, operator and reward address + { + 1201000, + identityset.Address(27), + 1, + "test", + identityset.Address(27).String(), + identityset.Address(29).String(), + identityset.Address(27).String(), + "1200000000000000000000000", + "1806204150552640363969204", + uint32(10000), + false, + []byte("payload"), + uint64(1000000), + uint64(1000000), + big.NewInt(1000), + true, + false, + "update", + identityset.Address(31).String(), + identityset.Address(32).String(), + nil, + iotextypes.ReceiptStatus_Success, + }, // success,update name, operator and reward address { 1201000, @@ -845,6 +901,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), big.NewInt(1000), true, + false, "update", identityset.Address(31).String(), identityset.Address(32).String(), @@ -868,6 +925,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), uint64(1000000), big.NewInt(1000), + true, false, "test1", "", @@ -892,6 +950,7 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { uint64(1000000), uint64(1000000), big.NewInt(1000), + true, false, "test", identityset.Address(7).String(), @@ -912,25 +971,41 @@ func TestProtocol_HandleCandidateUpdate(t *testing.T) { SetGasPrice(test.gasPrice).SetAction(act).Build() registerCost, err := elp.Cost() require.NoError(err) + g := genesis.TestDefault() ctx := protocol.WithActionCtx(context.Background(), protocol.ActionCtx{ Caller: test.caller, GasPrice: test.gasPrice, IntrinsicGas: intrinsic, Nonce: test.nonce, }) + blockHeight := uint64(1) + var cu *action.CandidateUpdate + if test.allowUpdate { + cu, err = action.NewCandidateUpdate(test.updateName, test.updateOperator, test.updateReward) + require.NoError(err) + p.SetIsDelegateFunc(nil) + } else { + blockHeight = g.XinguBlockHeight + blsPrivKey, err := crypto.GenerateBLS12381PrivateKey(identityset.PrivateKey(0).Bytes()) + require.NoError(err) + cu, err = action.NewCandidateUpdateWithBLS(test.updateName, test.updateOperator, test.updateReward, blsPrivKey.PublicKey().Bytes()) + require.NoError(err) + p.SetIsDelegateFunc(func(ctx context.Context, sr protocol.StateReader, candidate string) (bool, error) { + return test.isActive, nil + }) + } + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ - BlockHeight: 1, + BlockHeight: blockHeight, BlockTimeStamp: time.Now(), GasLimit: test.blkGasLimit, }) ctx = protocol.WithBlockchainCtx(ctx, protocol.BlockchainCtx{Tip: protocol.TipInfo{}}) - ctx = genesis.WithGenesisContext(ctx, genesis.TestDefault()) + ctx = genesis.WithGenesisContext(ctx, g) ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) _, err = p.Handle(ctx, elp, sm) require.NoError(err) - cu, err := action.NewCandidateUpdate(test.updateName, test.updateOperator, test.updateReward) - require.NoError(err) intrinsic, _ = cu.IntrinsicGas() elp = builder.SetNonce(test.nonce + 1).SetGasLimit(test.gasLimit). SetGasPrice(test.gasPrice).SetAction(cu).Build() diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 92eb19f0df..b0ed03e777 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -77,6 +77,8 @@ var ( ) type ( + // IsDelegateFunc defines the function to check if a candidate is a delegate + IsDelegateFunc func(ctx context.Context, sr protocol.StateReader, candidate string) (bool, error) // ReceiptError indicates a non-critical error with corresponding receipt status ReceiptError interface { Error() string @@ -100,6 +102,7 @@ type ( helperCtx HelperCtx blockStore BlockStore blocksToDurationFn func(startHeight, endHeight, currentHeight uint64) time.Duration + isDelegate IsDelegateFunc } // Configuration is the staking protocol configuration. @@ -342,6 +345,11 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (protocol return c, nil } +// SetIsDelegateFunc sets the function to check if an address is a delegate +func (p *Protocol) SetIsDelegateFunc(f IsDelegateFunc) { + p.isDelegate = f +} + // CreateGenesisStates is used to setup BootstrapCandidates from genesis config. func (p *Protocol) CreateGenesisStates( ctx context.Context, diff --git a/chainservice/builder.go b/chainservice/builder.go index 9bd18f1dc8..20667585b4 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -747,6 +747,7 @@ func (builder *Builder) registerRollDPoSProtocol() error { } factory := builder.cs.factory chain := builder.cs.chain + stakingProtocol := staking.FindProtocol(builder.cs.registry) pollProtocol, err := poll.NewProtocol( builder.cfg.Consensus.Scheme, builder.cfg.Chain, @@ -781,7 +782,7 @@ func (builder *Builder) registerRollDPoSProtocol() error { candidatesutil.ProbationListFromDB, candidatesutil.UnproductiveDelegateFromDB, builder.cs.electionCommittee, - staking.FindProtocol(builder.cs.registry), + stakingProtocol, nil, func(start, end uint64) (map[string]uint64, error) { return blockchain.Productivity(chain, start, end) @@ -792,6 +793,20 @@ func (builder *Builder) registerRollDPoSProtocol() error { if err != nil { return errors.Wrap(err, "failed to generate poll protocol") } + if stakingProtocol != nil { + stakingProtocol.SetIsDelegateFunc(func(ctx context.Context, sr protocol.StateReader, candname string) (bool, error) { + delegates, err := pollProtocol.Delegates(ctx, sr) + if err != nil { + return false, err + } + for _, d := range delegates { + if string(d.CanName) == candname { + return true, nil + } + } + return false, nil + }) + } return pollProtocol.Register(builder.cs.registry) }