diff --git a/action/protocol/context.go b/action/protocol/context.go index 8cedeec6dd..bd58b8f761 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -121,6 +121,7 @@ type ( RefactorFreshAccountConversion bool SuicideTxLogMismatchPanic bool PanicUnrecoverableError bool + CandidateIdentifiedByOwner bool } // FeatureWithHeightCtx provides feature check functions. @@ -267,6 +268,7 @@ func WithFeatureCtx(ctx context.Context) context.Context { RefactorFreshAccountConversion: g.IsTsunami(height), SuicideTxLogMismatchPanic: g.IsToBeEnabled(height), PanicUnrecoverableError: g.IsToBeEnabled(height), + CandidateIdentifiedByOwner: !g.IsToBeEnabled(height), }, ) } diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index e46a3de835..0a4feecefb 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -686,7 +686,7 @@ func (p *Protocol) handleCandidateRegister(ctx context.Context, act *action.Cand failureStatus: iotextypes.ReceiptStatus_ErrCandidateAlreadyExist, } } - // TODO: should be hard-fork + // cannot collide with existing identifier c = csm.GetByIdentifier(owner) if c != nil { return log, nil, &handleError{ diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 75c9f2769e..759cc857ae 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -425,6 +425,8 @@ func (p *Protocol) handle(ctx context.Context, act action.Action, csm CandidateS rLog, tLogs, err = p.handleCandidateActivate(ctx, act, csm) case *action.CandidateEndorsement: rLog, tLogs, err = p.handleCandidateEndorsement(ctx, act, csm) + case *action.CandidateTransferOwnership: + rLog, tLogs, err = p.handleCandidateTransferOwnership(ctx, act, csm) default: return nil, nil } @@ -473,6 +475,8 @@ func (p *Protocol) Validate(ctx context.Context, act action.Action, sr protocol. return p.validateCandidateActivate(ctx, act) case *action.CandidateEndorsement: return p.validateCandidateEndorsement(ctx, act) + case *action.CandidateTransferOwnership: + return p.validateCandidateTransferOwnershipAction(ctx, act) } return nil } diff --git a/action/protocol/staking/validations.go b/action/protocol/staking/validations.go index 0947dc337f..2008f4b058 100644 --- a/action/protocol/staking/validations.go +++ b/action/protocol/staking/validations.go @@ -98,3 +98,11 @@ func (p *Protocol) validateCandidateActivate(ctx context.Context, act *action.Ca } return nil } + +func (p *Protocol) validateCandidateTransferOwnershipAction(ctx context.Context, act *action.CandidateTransferOwnership) error { + // TODO: remove this check after candidate transfer ownership is enabled + if protocol.MustGetFeatureCtx(ctx).CandidateIdentifiedByOwner { + return errors.Wrap(action.ErrInvalidAct, "candidate transfer ownership is disabled") + } + return nil +} diff --git a/action/signedaction.go b/action/signedaction.go index ffecc58f5d..e375ea5f7a 100644 --- a/action/signedaction.go +++ b/action/signedaction.go @@ -317,3 +317,28 @@ func SignedRestake( } return selp, nil } + +// SignedCandidateTransferOwnership returns a signed candidate transfer ownership +func SignedCandidateTransferOwnership( + nonce uint64, + ownerAddrStr string, + payload []byte, + gasLimit uint64, + gasPrice *big.Int, + senderPriKey crypto.PrivateKey, +) (*SealedEnvelope, error) { + cto, err := NewCandidateTransferOwnership(nonce, gasLimit, gasPrice, ownerAddrStr, payload) + if err != nil { + return nil, err + } + bd := &EnvelopeBuilder{} + elp := bd.SetNonce(nonce). + SetGasPrice(gasPrice). + SetGasLimit(gasLimit). + SetAction(cto).Build() + selp, err := Sign(elp, senderPriKey) + if err != nil { + return nil, errors.Wrapf(err, "failed to sign candidate transfer ownership %v", elp) + } + return selp, nil +} diff --git a/action/signedaction_test.go b/action/signedaction_test.go index 1cb1ef156c..277c9353c0 100644 --- a/action/signedaction_test.go +++ b/action/signedaction_test.go @@ -201,3 +201,17 @@ func TestSignedRestake(t *testing.T) { require.Equal([]byte{}, exec.payload) require.NotNil(selp.Signature()) } + +func TestSignedCandidateTransferOwnership(t *testing.T) { + require := require.New(t) + selp, err := SignedCandidateTransferOwnership(1, _cand1Addr, []byte{}, _gasLimit, _gasPrice, _cand1PriKey) + require.NoError(err) + + exec := selp.Action().(*CandidateTransferOwnership) + require.Equal(uint64(1), exec.Nonce()) + require.Equal(_cand1Addr, exec.newOwner.String()) + require.Equal(_gasLimit, exec.GasLimit()) + require.Equal(_gasPrice, exec.GasPrice()) + require.Equal([]byte{}, exec.payload) + require.NotNil(selp.Signature()) +} diff --git a/e2etest/native_staking_test.go b/e2etest/native_staking_test.go index 4dfdeca6b1..aeaaa7cf83 100644 --- a/e2etest/native_staking_test.go +++ b/e2etest/native_staking_test.go @@ -2,6 +2,7 @@ package e2etest import ( "context" + "encoding/hex" "math/big" "testing" "time" @@ -454,6 +455,59 @@ func TestNativeStaking(t *testing.T) { require.NoError(err) require.Equal(4, len(cands)) }) + t.Run("candidate transfer ownership to self", func(t *testing.T) { + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(5, cand1Addr.String(), nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, cand1Addr.String(), selfStake, cand1Votes, cand1Addr)) + }) + + t.Run("candidate transfer ownership to a normal new address", func(t *testing.T) { + newOwner1 := identityset.Address(33) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(6, newOwner1.String(), nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_Success, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, newOwner1.String(), selfStake, cand1Votes, cand1Addr)) + }) + t.Run("candidate transfer ownership to a exist candidate", func(t *testing.T) { + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(7, cand2Addr.String(), nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, identityset.Address(33).String(), selfStake, cand1Votes, cand1Addr)) + }) + t.Run("candidate transfer ownership to a normal new address again", func(t *testing.T) { + newOwner2 := identityset.Address(34) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(8, newOwner2.String(), nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_Success, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, newOwner2.String(), selfStake, cand1Votes, cand1Addr)) + }) + t.Run("candidate transfer ownership to a transfered candidate", func(t *testing.T) { + newOwner2 := identityset.Address(34) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(9, newOwner2.String(), nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, newOwner2.String(), selfStake, cand1Votes, cand1Addr)) + }) + t.Run("candidate transfer ownership to a contract address", func(t *testing.T) { + data, _ := hex.DecodeString("608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582002faabbefbbda99b20217cf33cb8ab8100caf1542bf1f48117d72e2c59139aea0029") + _, se, err := addOneTx(action.SignedExecution(action.EmptyAddress, cand1PriKey, 10, big.NewInt(0), uint64(100000), big.NewInt(0), data)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_Success, se.Status) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(11, se.ContractAddress, nil, gasLimit, gasPrice, cand1PriKey)) + require.NoError(err) + require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) + require.NoError(checkCandidateState(sf, candidate1Name, identityset.Address(34).String(), selfStake, cand1Votes, cand1Addr)) + }) + t.Run("candidate transfer ownership to a invalid address", func(t *testing.T) { + _, _, err := addOneTx(action.SignedCandidateTransferOwnership(12, "123", nil, gasLimit, gasPrice, cand1PriKey)) + require.ErrorContains(err, action.ErrAddress.Error()) + }) + t.Run("candidate transfer ownership with none candidate", func(t *testing.T) { + newOwner := identityset.Address(34) + _, _, err := addOneTx(action.SignedCandidateTransferOwnership(12, newOwner.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(12))) + require.ErrorContains(err, "failed to find receipt") + }) } cfg := config.Default @@ -489,8 +543,9 @@ func TestNativeStaking(t *testing.T) { cfg.Chain.EnableAsyncIndexWrite = false cfg.Genesis.BootstrapCandidates = testInitCands cfg.Genesis.FbkMigrationBlockHeight = 1 - cfg.Genesis.TsunamiBlockHeight = 0 + cfg.Genesis.TsunamiBlockHeight = 2 cfg.Genesis.EndorsementWithdrawWaitingBlocks = 10 + cfg.Genesis.ToBeEnabledBlockHeight = 3 // enable CandidateIdentifiedByOwner feature t.Run("test native staking", func(t *testing.T) { testNativeStaking(cfg, t)