From 1009b3212667dbf5fd8f35621bef4503053e84e1 Mon Sep 17 00:00:00 2001 From: Chen Chen <34592639+envestcc@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:13:10 +0800 Subject: [PATCH] [e2etest] add tests for original actions after candidate ownership transfer (#4295) --- .../protocol/staking/candidate_center_test.go | 1 - ...ndler_candidate_transfer_ownership_test.go | 3 +- action/protocol/staking/protocol.go | 6 +- e2etest/e2etest.go | 152 +++++++++ e2etest/expect.go | 115 +++++++ e2etest/native_staking_test.go | 302 ++++++++++++++++-- 6 files changed, 542 insertions(+), 37 deletions(-) create mode 100644 e2etest/e2etest.go create mode 100644 e2etest/expect.go diff --git a/action/protocol/staking/candidate_center_test.go b/action/protocol/staking/candidate_center_test.go index 026431811c..88d16cca73 100644 --- a/action/protocol/staking/candidate_center_test.go +++ b/action/protocol/staking/candidate_center_test.go @@ -578,7 +578,6 @@ func candCenterFromNewCandidateStateManager(r *require.Assertions, view protocol } func TestCandidateUpsert(t *testing.T) { - // t.Skip() r := require.New(t) m, err := NewCandidateCenter(nil) diff --git a/action/protocol/staking/handler_candidate_transfer_ownership_test.go b/action/protocol/staking/handler_candidate_transfer_ownership_test.go index f98f60a2dd..6e7a68ca5e 100644 --- a/action/protocol/staking/handler_candidate_transfer_ownership_test.go +++ b/action/protocol/staking/handler_candidate_transfer_ownership_test.go @@ -21,8 +21,6 @@ import ( ) func TestProtocol_HandleCandidateTransferOwnership(t *testing.T) { - //TODO: fix this test - t.Skip() require := require.New(t) ctrl := gomock.NewController(t) sm := testdb.NewMockStateManager(ctrl) @@ -212,6 +210,7 @@ func TestProtocol_HandleCandidateTransferOwnership(t *testing.T) { }) cfg := deepcopy.Copy(genesis.Default).(genesis.Genesis) cfg.TsunamiBlockHeight = 1 + cfg.ToBeEnabledBlockHeight = 1 // enable candidate owner transfer feature ctx = genesis.WithGenesisContext(ctx, cfg) ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) require.NoError(p.Validate(ctx, act, sm)) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index a4fca2e5e1..f550936ae5 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -562,8 +562,10 @@ func (p *Protocol) ReadState(ctx context.Context, sr protocol.StateReader, metho if err != nil { return nil, 0, err } - rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx)) - epochStartHeight := rp.GetEpochHeight(rp.GetEpochNum(inputHeight)) + epochStartHeight := inputHeight + if rp := rolldpos.FindProtocol(protocol.MustGetRegistry(ctx)); rp != nil { + epochStartHeight = rp.GetEpochHeight(rp.GetEpochNum(inputHeight)) + } nativeSR, err := ConstructBaseView(sr) if err != nil { return nil, 0, err diff --git a/e2etest/e2etest.go b/e2etest/e2etest.go new file mode 100644 index 0000000000..04838a292a --- /dev/null +++ b/e2etest/e2etest.go @@ -0,0 +1,152 @@ +package e2etest + +import ( + "context" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/actpool" + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/chainservice" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/server/itx" + "github.com/iotexproject/iotex-core/testutil" +) + +type ( + actionWithTime struct { + act *action.SealedEnvelope + t time.Time + } + testcase struct { + name string + preActs []*actionWithTime + act *actionWithTime + expect []actionExpect + } + accountNonceManager map[string]uint64 + e2etest struct { + cfg config.Config + svr *itx.Server + cs *chainservice.ChainService + t *testing.T + nonceMgr accountNonceManager + } +) + +func (m accountNonceManager) pop(addr string) uint64 { + nonce := m[addr] + m[addr] = nonce + 1 + return nonce +} + +func newE2ETest(t *testing.T, cfg config.Config) *e2etest { + require := require.New(t) + // Create a new blockchain + svr, err := itx.NewServer(cfg) + require.NoError(err) + ctx := context.Background() + require.NoError(svr.Start(ctx)) + return &e2etest{ + cfg: cfg, + svr: svr, + cs: svr.ChainService(cfg.Chain.ID), + t: t, + nonceMgr: make(accountNonceManager), + } +} + +func (e *e2etest) run(cases []*testcase) { + ctx := context.Background() + // run subcases + for _, sub := range cases { + e.t.Run(sub.name, func(t *testing.T) { + e.withTest(t).runCase(ctx, sub) + }) + } +} + +func (e *e2etest) runCase(ctx context.Context, c *testcase) { + require := require.New(e.t) + bc := e.cs.Blockchain() + ap := e.cs.ActionPool() + // run pre-actions + for _, act := range c.preActs { + _, _, err := addOneTx(ctx, ap, bc, act) + require.NoError(err) + } + // run action + act, receipt, err := addOneTx(ctx, ap, bc, c.act) + for _, exp := range c.expect { + exp.expect(e, act, receipt, err) + } +} + +func (e *e2etest) teardown() { + require := require.New(e.t) + // clean up + testutil.CleanupPath(e.cfg.Chain.ChainDBPath) + testutil.CleanupPath(e.cfg.Chain.TrieDBPath) + testutil.CleanupPath(e.cfg.Chain.BloomfilterIndexDBPath) + testutil.CleanupPath(e.cfg.Chain.CandidateIndexDBPath) + testutil.CleanupPath(e.cfg.Chain.StakingIndexDBPath) + testutil.CleanupPath(e.cfg.Chain.ContractStakingIndexDBPath) + testutil.CleanupPath(e.cfg.DB.DbPath) + testutil.CleanupPath(e.cfg.Chain.IndexDBPath) + testutil.CleanupPath(e.cfg.System.SystemLogDBPath) + testutil.CleanupPath(e.cfg.Chain.SGDIndexDBPath) + require.NoError(e.svr.Stop(context.Background())) +} + +func (e *e2etest) withTest(t *testing.T) *e2etest { + return &e2etest{ + cfg: e.cfg, + svr: e.svr, + cs: e.cs, + t: t, + } +} + +func addOneTx(ctx context.Context, ap actpool.ActPool, bc blockchain.Blockchain, tx *actionWithTime) (*action.SealedEnvelope, *action.Receipt, error) { + if err := ap.Add(ctx, tx.act); err != nil { + return tx.act, nil, err + } + blk, err := createAndCommitBlock(bc, ap, tx.t) + if err != nil { + return tx.act, nil, err + } + h, err := tx.act.Hash() + if err != nil { + return tx.act, nil, err + } + for _, r := range blk.Receipts { + if r.ActionHash == h { + return tx.act, r, nil + } + } + return tx.act, nil, errors.Errorf("failed to find receipt for %x", h) +} + +func createAndCommitBlock(bc blockchain.Blockchain, ap actpool.ActPool, blkTime time.Time) (*block.Block, error) { + blk, err := bc.MintNewBlock(blkTime) + if err != nil { + return nil, err + } + if err := bc.CommitBlock(blk); err != nil { + return nil, err + } + ap.Reset() + return blk, nil +} + +func mustNoErr[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/e2etest/expect.go b/e2etest/expect.go new file mode 100644 index 0000000000..bc6fc652eb --- /dev/null +++ b/e2etest/expect.go @@ -0,0 +1,115 @@ +package e2etest + +import ( + "context" + "slices" + + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/action/protocol" + "github.com/iotexproject/iotex-core/action/protocol/staking" + "github.com/iotexproject/iotex-core/blockchain/genesis" +) + +type ( + actionExpect interface { + expect(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) + } + basicActionExpect struct { + err error + status uint64 + executionRevertMsg string + } + candidateExpect struct { + candName string + cand *iotextypes.CandidateV2 + } + bucketExpect struct { + bucket *iotextypes.VoteBucket + } +) + +func (be *basicActionExpect) expect(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + require := require.New(test.t) + require.ErrorIs(err, be.err) + require.Equal(be.status, receipt.Status) + require.Equal(be.executionRevertMsg, receipt.ExecutionRevertMsg()) +} + +func (ce *candidateExpect) expect(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + require := require.New(test.t) + method := &iotexapi.ReadStakingDataMethod{ + Method: iotexapi.ReadStakingDataMethod_CANDIDATE_BY_NAME, + } + methodBytes, err := proto.Marshal(method) + require.NoError(err) + r := &iotexapi.ReadStakingDataRequest{ + Request: &iotexapi.ReadStakingDataRequest_CandidateByName_{ + CandidateByName: &iotexapi.ReadStakingDataRequest_CandidateByName{ + CandName: ce.candName, + }, + }, + } + cs := test.svr.ChainService(test.cfg.Chain.ID) + sr := cs.StateFactory() + bc := cs.Blockchain() + prtcl, ok := cs.Registry().Find("staking") + require.True(ok) + stkPrtcl := prtcl.(*staking.Protocol) + reqBytes, err := proto.Marshal(r) + require.NoError(err) + ctx := protocol.WithRegistry(context.Background(), cs.Registry()) + ctx = genesis.WithGenesisContext(ctx, test.cfg.Genesis) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: bc.TipHeight(), + }) + ctx = protocol.WithFeatureCtx(ctx) + respData, _, err := stkPrtcl.ReadState(ctx, sr, methodBytes, reqBytes) + require.NoError(err) + candidate := &iotextypes.CandidateV2{} + require.NoError(proto.Unmarshal(respData, candidate)) + require.EqualValues(ce.cand.String(), candidate.String()) +} + +func (be *bucketExpect) expect(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + require := require.New(test.t) + method := &iotexapi.ReadStakingDataMethod{ + Method: iotexapi.ReadStakingDataMethod_BUCKETS_BY_INDEXES, + } + methodBytes, err := proto.Marshal(method) + require.NoError(err) + r := &iotexapi.ReadStakingDataRequest{ + Request: &iotexapi.ReadStakingDataRequest_BucketsByIndexes{ + BucketsByIndexes: &iotexapi.ReadStakingDataRequest_VoteBucketsByIndexes{ + Index: []uint64{be.bucket.Index}, + }, + }, + } + cs := test.svr.ChainService(test.cfg.Chain.ID) + sr := cs.StateFactory() + bc := cs.Blockchain() + prtcl, ok := cs.Registry().Find("staking") + require.True(ok) + stkPrtcl := prtcl.(*staking.Protocol) + reqBytes, err := proto.Marshal(r) + require.NoError(err) + ctx := protocol.WithRegistry(context.Background(), cs.Registry()) + ctx = genesis.WithGenesisContext(ctx, test.cfg.Genesis) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: bc.TipHeight(), + }) + ctx = protocol.WithFeatureCtx(ctx) + respData, _, err := stkPrtcl.ReadState(ctx, sr, methodBytes, reqBytes) + require.NoError(err) + vbs := &iotextypes.VoteBucketList{} + require.NoError(proto.Unmarshal(respData, vbs)) + idx := slices.IndexFunc(vbs.Buckets, func(vb *iotextypes.VoteBucket) bool { + return vb.ContractAddress == be.bucket.ContractAddress + }) + require.True(idx != -1) + require.EqualValues(be.bucket.String(), vbs.Buckets[idx].String()) +} diff --git a/e2etest/native_staking_test.go b/e2etest/native_staking_test.go index aeaaa7cf83..77572cad0c 100644 --- a/e2etest/native_staking_test.go +++ b/e2etest/native_staking_test.go @@ -3,24 +3,23 @@ package e2etest import ( "context" "encoding/hex" + "math" "math/big" "testing" "time" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/mohae/deepcopy" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/iotexproject/iotex-core/action" "github.com/iotexproject/iotex-core/action/protocol" accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util" "github.com/iotexproject/iotex-core/action/protocol/staking" - "github.com/iotexproject/iotex-core/actpool" - "github.com/iotexproject/iotex-core/blockchain" - "github.com/iotexproject/iotex-core/blockchain/block" "github.com/iotexproject/iotex-core/blockchain/genesis" "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/pkg/util/byteutil" @@ -135,10 +134,10 @@ func TestNativeStaking(t *testing.T) { } register1, r1, err := addOneTx(action.SignedCandidateRegister(1, candidate1Name, cand1Addr.String(), cand1Addr.String(), - cand1Addr.String(), selfStake.String(), 91, true, nil, gasLimit, gasPrice, cand1PriKey)) + cand1Addr.String(), selfStake.String(), 91, true, nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) require.NoError(err) register2, _, err := addOneTx(action.SignedCandidateRegister(1, candidate2Name, cand2Addr.String(), cand2Addr.String(), - cand2Addr.String(), selfStake.String(), 1, false, nil, gasLimit, gasPrice, cand2PriKey)) + cand2Addr.String(), selfStake.String(), 1, false, nil, gasLimit, gasPrice, cand2PriKey, action.WithChainID(chainID))) require.NoError(err) // check candidate state require.NoError(checkCandidateState(sf, candidate1Name, cand1Addr.String(), selfStake, cand1Votes, cand1Addr)) @@ -164,10 +163,10 @@ func TestNativeStaking(t *testing.T) { voter2PriKey := identityset.PrivateKey(3) cs1, r1, err := addOneTx(action.SignedCreateStake(1, candidate1Name, vote.String(), 1, false, - nil, gasLimit, gasPrice, voter1PriKey)) + nil, gasLimit, gasPrice, voter1PriKey, action.WithChainID(chainID))) require.NoError(err) cs2, r2, err := addOneTx(action.SignedCreateStake(1, candidate1Name, vote.String(), 1, false, - nil, gasLimit, gasPrice, voter2PriKey)) + nil, gasLimit, gasPrice, voter2PriKey, action.WithChainID(chainID))) require.NoError(err) // check candidate state @@ -375,7 +374,7 @@ func TestNativeStaking(t *testing.T) { // register without stake register3, r3, err := addOneTx(action.SignedCandidateRegister(1, candidate3Name, cand3Addr.String(), cand3Addr.String(), - cand3Addr.String(), "0", 1, false, nil, gasLimit, gasPrice, cand3PriKey)) + cand3Addr.String(), "0", 1, false, nil, gasLimit, gasPrice, cand3PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_Success, r3.Status) require.NoError(checkCandidateState(sf, candidate3Name, cand3Addr.String(), big.NewInt(0), big.NewInt(0), cand3Addr)) @@ -395,7 +394,7 @@ func TestNativeStaking(t *testing.T) { } // stake bucket _, cr3, err := addOneTx(action.SignedCreateStake(3, candidate3Name, selfStake.String(), 1, false, - nil, gasLimit, gasPrice, voter1PriKey)) + nil, gasLimit, gasPrice, voter1PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_Success, cr3.Status) logs = cr3.Logs() @@ -404,12 +403,12 @@ func TestNativeStaking(t *testing.T) { endorseBucketIndex := byteutil.BytesToUint64BigEndian(logs[0].Topics[1][24:]) t.Logf("endorseBucketIndex=%+v", endorseBucketIndex) // endorse bucket - _, esr, err := addOneTx(action.SignedCandidateEndorsement(4, endorseBucketIndex, true, gasLimit, gasPrice, voter1PriKey)) + _, esr, err := addOneTx(action.SignedCandidateEndorsement(4, endorseBucketIndex, true, gasLimit, gasPrice, voter1PriKey, action.WithChainID(chainID))) require.NoError(err) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_Success, esr.Status) // candidate self stake - _, cssr, err := addOneTx(action.SignedCandidateActivate(2, endorseBucketIndex, gasLimit, gasPrice, cand3PriKey)) + _, cssr, err := addOneTx(action.SignedCandidateActivate(2, endorseBucketIndex, gasLimit, gasPrice, cand3PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_Success, cssr.Status) ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ @@ -423,7 +422,7 @@ func TestNativeStaking(t *testing.T) { t.Logf("\ncandidate=%+v, %+v\n", string(cand.CanName), cand.Votes.String()) } // unendorse bucket - _, esr, err = addOneTx(action.SignedCandidateEndorsement(5, endorseBucketIndex, false, gasLimit, gasPrice, voter1PriKey)) + _, esr, err = addOneTx(action.SignedCandidateEndorsement(5, endorseBucketIndex, false, gasLimit, gasPrice, voter1PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_Success, esr.Status) ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ @@ -456,7 +455,7 @@ func TestNativeStaking(t *testing.T) { 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(5, cand1Addr.String(), nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) require.NoError(checkCandidateState(sf, candidate1Name, cand1Addr.String(), selfStake, cand1Votes, cand1Addr)) @@ -464,27 +463,27 @@ func TestNativeStaking(t *testing.T) { 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(6, newOwner1.String(), nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(7, cand2Addr.String(), nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(8, newOwner2.String(), nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(9, newOwner2.String(), nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) require.NoError(err) require.EqualValues(iotextypes.ReceiptStatus_ErrUnauthorizedOperator, ccto.Status) require.NoError(checkCandidateState(sf, candidate1Name, newOwner2.String(), selfStake, cand1Votes, cand1Addr)) @@ -494,18 +493,18 @@ func TestNativeStaking(t *testing.T) { _, 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)) + _, ccto, err := addOneTx(action.SignedCandidateTransferOwnership(11, se.ContractAddress, nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) 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)) + _, _, err := addOneTx(action.SignedCandidateTransferOwnership(12, "123", nil, gasLimit, gasPrice, cand1PriKey, action.WithChainID(chainID))) 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))) + _, _, err := addOneTx(action.SignedCandidateTransferOwnership(12, newOwner.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(12), action.WithChainID(chainID))) require.ErrorContains(err, "failed to find receipt") }) } @@ -608,14 +607,253 @@ func checkAccountState( return nil } -func createAndCommitBlock(bc blockchain.Blockchain, ap actpool.ActPool, blkTime time.Time) (*block.Block, error) { - blk, err := bc.MintNewBlock(blkTime) - if err != nil { - return nil, err - } - if err := bc.CommitBlock(blk); err != nil { - return nil, err +func TestCandidateTransferOwnership(t *testing.T) { + require := require.New(t) + initCfg := func() config.Config { + cfg := deepcopy.Copy(config.Default).(config.Config) + testTriePath, err := testutil.PathOfTempFile("trie") + require.NoError(err) + testDBPath, err := testutil.PathOfTempFile("db") + require.NoError(err) + testIndexPath, err := testutil.PathOfTempFile("index") + require.NoError(err) + testSystemLogPath, err := testutil.PathOfTempFile("systemlog") + require.NoError(err) + testSGDIndexPath, err := testutil.PathOfTempFile("sgdindex") + require.NoError(err) + + cfg.ActPool.MinGasPriceStr = "0" + cfg.Chain.TrieDBPatchFile = "" + cfg.Chain.TrieDBPath = testTriePath + cfg.Chain.ChainDBPath = testDBPath + cfg.Chain.IndexDBPath = testIndexPath + cfg.Chain.ContractStakingIndexDBPath = testIndexPath + cfg.Chain.SGDIndexDBPath = testSGDIndexPath + cfg.System.SystemLogDBPath = testSystemLogPath + cfg.Consensus.Scheme = config.NOOPScheme + cfg.Chain.EnableAsyncIndexWrite = false + cfg.Genesis.InitBalanceMap[identityset.Address(1).String()] = "100000000000000000000000000" + cfg.Genesis.InitBalanceMap[identityset.Address(2).String()] = "100000000000000000000000000" + cfg.Genesis.EndorsementWithdrawWaitingBlocks = 10 + cfg.Genesis.PacificBlockHeight = 1 + cfg.Genesis.AleutianBlockHeight = 1 + cfg.Genesis.BeringBlockHeight = 1 + cfg.Genesis.CookBlockHeight = 1 + cfg.Genesis.DardanellesBlockHeight = 1 + cfg.Genesis.DaytonaBlockHeight = 1 + cfg.Genesis.EasterBlockHeight = 1 + cfg.Genesis.FbkMigrationBlockHeight = 1 + cfg.Genesis.FairbankBlockHeight = 1 + cfg.Genesis.GreenlandBlockHeight = 1 + cfg.Genesis.HawaiiBlockHeight = 1 + cfg.Genesis.IcelandBlockHeight = 1 + cfg.Genesis.JutlandBlockHeight = 1 + cfg.Genesis.KamchatkaBlockHeight = 1 + cfg.Genesis.LordHoweBlockHeight = 1 + cfg.Genesis.MidwayBlockHeight = 1 + cfg.Genesis.NewfoundlandBlockHeight = 1 + cfg.Genesis.OkhotskBlockHeight = 1 + cfg.Genesis.PalauBlockHeight = 1 + cfg.Genesis.QuebecBlockHeight = 1 + cfg.Genesis.RedseaBlockHeight = 1 + cfg.Genesis.SumatraBlockHeight = 1 + cfg.Genesis.TsunamiBlockHeight = 1 + cfg.Genesis.UpernavikBlockHeight = 1 + cfg.Genesis.ToBeEnabledBlockHeight = 1 // enable CandidateIdentifiedByOwner feature + return cfg } - ap.Reset() - return blk, nil + registerAmount, _ := big.NewInt(0).SetString("1200000000000000000000000", 10) + gasLimit = uint64(1000000) + gasPrice = big.NewInt(10) + successExpect := &basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_Success), ""} + + t.Run("transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + oldOwnerID := 1 + newOwnerID := 2 + chainID := test.cfg.Chain.ID + test.run([]*testcase{ + { + name: "success to transfer candidate ownership", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &candidateExpect{"cand1", &iotextypes.CandidateV2{Name: "cand1", OperatorAddress: identityset.Address(1).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "1245621408203087110422302", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID).String(), SelfStakeBucketIdx: 0}}}, + }, + { + name: "cannot transfer to old owner", + act: &actionWithTime{mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), identityset.Address(oldOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{&basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_ErrUnauthorizedOperator), ""}}, + }, + }) + }) + t.Run("candidate activate after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + chainID := test.cfg.Chain.ID + + test.run([]*testcase{ + { + name: "old owner cannot invoke candidate activate", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateActivate(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), 1, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{&basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_ErrCandidateNotExist), ""}}, + }, + { + name: "new owner can invoke candidate activate", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), "cand1", registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateActivate(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), 2, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &candidateExpect{"cand1", &iotextypes.CandidateV2{Name: "cand1", OperatorAddress: identityset.Address(1).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "3736864224609261331266906", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID).String(), SelfStakeBucketIdx: 2}}}, + }, + }) + }) + t.Run("candidate endorsement after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + stakerID := 3 + chainID := test.cfg.Chain.ID + stakeTime := time.Now() + + test.run([]*testcase{ + { + name: "endorse same candidate after transfer ownership", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand1", registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), stakeTime}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateEndorsement(test.nonceMgr.pop(identityset.Address(stakerID).String()), 1, true, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &bucketExpect{&iotextypes.VoteBucket{Index: 1, CandidateAddress: identityset.Address(oldOwnerID).String(), StakedAmount: registerAmount.String(), AutoStake: true, StakedDuration: 1, CreateTime: timestamppb.New(stakeTime), StakeStartTime: timestamppb.New(stakeTime), UnstakeStartTime: ×tamppb.Timestamp{}, Owner: identityset.Address(stakerID).String(), ContractAddress: "", EndorsementExpireBlockHeight: math.MaxUint64}}}, + }, + }) + }) + t.Run("candidate register after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + newOwnerID2 := 3 + chainID := test.cfg.Chain.ID + + test.run([]*testcase{ + { + name: "old owner cannot register again", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{&basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_ErrCandidateAlreadyExist), ""}}, + }, + { + name: "new owner cannot register again", + act: &actionWithTime{mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), identityset.Address(newOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{&basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_ErrCandidateAlreadyExist), ""}}, + }, + { + name: "transfer ownership to another new owner", + act: &actionWithTime{mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), identityset.Address(newOwnerID2).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &candidateExpect{"cand1", &iotextypes.CandidateV2{Name: "cand1", OperatorAddress: identityset.Address(1).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "1245621408203087110422302", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID2).String(), SelfStakeBucketIdx: 0}}}, + }, + { + name: "new owner can register again", + act: &actionWithTime{mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), identityset.Address(newOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &candidateExpect{"cand2", &iotextypes.CandidateV2{Name: "cand2", OperatorAddress: identityset.Address(2).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "1245621408203087110422302", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID).String(), SelfStakeBucketIdx: 1}}}, + }, + }) + }) + t.Run("candidate update after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + chainID := test.cfg.Chain.ID + + test.run([]*testcase{ + { + name: "old owner cannot update", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCandidateUpdate(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{&basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_ErrCandidateNotExist), ""}}, + }, + { + name: "new owner can update", + act: &actionWithTime{mustNoErr(action.SignedCandidateUpdate(test.nonceMgr.pop(identityset.Address(newOwnerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), gasLimit, gasPrice, identityset.PrivateKey(newOwnerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &candidateExpect{"cand2", &iotextypes.CandidateV2{Name: "cand2", OperatorAddress: identityset.Address(2).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "1245621408203087110422302", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID).String(), SelfStakeBucketIdx: 0}}}, + }, + }) + }) + t.Run("stake change candidate after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + stakerID := 3 + chainID := test.cfg.Chain.ID + stakeTime := time.Now() + + test.run([]*testcase{ + { + name: "change candidate after transfer ownership", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand2", identityset.Address(2).String(), identityset.Address(1).String(), identityset.Address(stakerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand2", registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), stakeTime}, + }, + act: &actionWithTime{mustNoErr(action.SignedChangeCandidate(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand1", 2, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{ + successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 2, CandidateAddress: identityset.Address(oldOwnerID).String(), StakedAmount: registerAmount.String(), AutoStake: true, StakedDuration: 1, CreateTime: timestamppb.New(stakeTime), StakeStartTime: timestamppb.New(stakeTime), UnstakeStartTime: ×tamppb.Timestamp{}, Owner: identityset.Address(stakerID).String(), ContractAddress: "", EndorsementExpireBlockHeight: 0}}, + &candidateExpect{"cand1", &iotextypes.CandidateV2{Name: "cand1", OperatorAddress: identityset.Address(1).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "2491242816406174220844604", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(newOwnerID).String(), SelfStakeBucketIdx: 0}}, + &candidateExpect{"cand2", &iotextypes.CandidateV2{Name: "cand2", OperatorAddress: identityset.Address(2).String(), RewardAddress: identityset.Address(1).String(), TotalWeightedVotes: "1245621408203087110422302", SelfStakingTokens: registerAmount.String(), OwnerAddress: identityset.Address(stakerID).String(), SelfStakeBucketIdx: 1}}, + }, + }, + }) + }) + t.Run("stake create after transfer candidate ownership", func(t *testing.T) { + test := newE2ETest(t, initCfg()) + defer test.teardown() + + oldOwnerID := 1 + newOwnerID := 2 + stakerID := 3 + chainID := test.cfg.Chain.ID + stakeTime := time.Now() + + test.run([]*testcase{ + { + name: "create stake after transfer ownership", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(oldOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateTransferOwnership(test.nonceMgr.pop(identityset.Address(oldOwnerID).String()), identityset.Address(newOwnerID).String(), nil, gasLimit, gasPrice, identityset.PrivateKey(oldOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand1", registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{ + successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 1, CandidateAddress: identityset.Address(oldOwnerID).String(), StakedAmount: registerAmount.String(), AutoStake: true, StakedDuration: 1, CreateTime: timestamppb.New(stakeTime), StakeStartTime: timestamppb.New(stakeTime), UnstakeStartTime: ×tamppb.Timestamp{}, Owner: identityset.Address(stakerID).String(), ContractAddress: "", EndorsementExpireBlockHeight: 0}}, + }, + }, + }) + }) }