diff --git a/action/protocol/context.go b/action/protocol/context.go index 9b30146b3e..68d74ac010 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -177,6 +177,7 @@ type ( CalculateProbationList CheckFunc LoadCandidatesLegacy CheckFunc CandCenterHasAlias CheckFunc + CandidateWithoutIdentity CheckFunc } ) @@ -391,6 +392,9 @@ func WithFeatureWithHeightCtx(ctx context.Context) context.Context { CandCenterHasAlias: func(height uint64) bool { return !g.IsOkhotsk(height) }, + CandidateWithoutIdentity: func(height uint64) bool { + return !g.IsToBeEnabled(height) + }, }, ) } diff --git a/action/protocol/poll/protocol.go b/action/protocol/poll/protocol.go index f152b1dee1..bd62325146 100644 --- a/action/protocol/poll/protocol.go +++ b/action/protocol/poll/protocol.go @@ -129,7 +129,7 @@ func NewProtocol( candidateIndexer *CandidateIndexer, readContract ReadContract, getCandidates GetCandidates, - getprobationList GetProbationList, + getProbationList GetProbationList, getUnproductiveDelegate GetUnproductiveDelegate, electionCommittee committee.Committee, stakingProto *staking.Protocol, @@ -155,7 +155,7 @@ func NewProtocol( slasher, err = NewSlasher( productivity, getCandidates, - getprobationList, + getProbationList, getUnproductiveDelegate, candidateIndexer, genesisConfig.NumCandidateDelegates, diff --git a/action/protocol/poll/slasher.go b/action/protocol/poll/slasher.go index 212fad6568..b2c29bb227 100644 --- a/action/protocol/poll/slasher.go +++ b/action/protocol/poll/slasher.go @@ -274,7 +274,7 @@ func (sh *Slasher) GetCandidates(ctx context.Context, sr protocol.StateReader, r return nil, uint64(0), errors.Wrapf(err, "failed to get probation list at height %d", targetEpochStartHeight) } // recalculate the voting power for probationlist delegates - filteredCandidate, err := filterCandidates(candidates, unqualifiedList, targetEpochStartHeight) + filteredCandidate, err := filterCandidates(candidates, unqualifiedList, targetEpochStartHeight, featureWithHeightCtx.CandidateWithoutIdentity(targetEpochStartHeight)) if err != nil { return nil, uint64(0), err } @@ -334,7 +334,7 @@ func (sh *Slasher) GetCandidatesFromIndexer(ctx context.Context, epochStartHeigh return nil, err } // recalculate the voting power for probationlist delegates - return filterCandidates(candidates, probationList, epochStartHeight) + return filterCandidates(candidates, probationList, epochStartHeight, featureWithHeightCtx.CandidateWithoutIdentity(epochStartHeight)) } // GetBPFromIndexer returns BP list from indexer @@ -520,11 +520,12 @@ func (sh *Slasher) calculateUnproductiveDelegates(ctx context.Context, sr protoc } else { produce[blkCtx.Producer.String()] = 1 } - + producerToOwner := make(map[string]string) for _, abp := range delegates { if _, ok := produce[abp.Address]; !ok { produce[abp.Address] = 0 } + producerToOwner[abp.Address] = abp.Identity } unqualified := make(map[string]uint64, 0) expectedNumBlks := numBlks / uint64(len(produce)) @@ -533,7 +534,18 @@ func (sh *Slasher) calculateUnproductiveDelegates(ctx context.Context, sr protoc unqualified[addr] = expectedNumBlks - actualNumBlks } } - return unqualified, nil + if protocol.MustGetFeatureWithHeightCtx(ctx).CandidateWithoutIdentity(blkCtx.BlockHeight) { + return unqualified, nil + } + retval := make(map[string]uint64, len(unqualified)) + for addr, count := range unqualified { + if owner, ok := producerToOwner[addr]; ok { + retval[owner] = count + } else { + retval[addr] = count + } + } + return retval, nil } func (sh *Slasher) updateCurrentBlockMeta(ctx context.Context, sm protocol.StateManager) error { @@ -595,13 +607,18 @@ func filterCandidates( candidates state.CandidateList, unqualifiedList *vote.ProbationList, epochStartHeight uint64, + usingOperatorAddr bool, ) (state.CandidateList, error) { candidatesMap := make(map[string]*state.Candidate) updatedVotingPower := make(map[string]*big.Int) intensityRate := float64(uint32(100)-unqualifiedList.IntensityRate) / float64(100) for _, cand := range candidates { filterCand := cand.Clone() - if _, ok := unqualifiedList.ProbationInfo[cand.Address]; ok { + id := cand.Address + if !usingOperatorAddr { + id = cand.Identity + } + if _, ok := unqualifiedList.ProbationInfo[id]; ok { // if it is an unqualified delegate, multiply the voting power with probation intensity rate votingPower := new(big.Float).SetInt(filterCand.Votes) filterCand.Votes, _ = votingPower.Mul(votingPower, big.NewFloat(intensityRate)).Int(nil) diff --git a/action/protocol/rewarding/reward.go b/action/protocol/rewarding/reward.go index 574bee10b7..be581b65b9 100644 --- a/action/protocol/rewarding/reward.go +++ b/action/protocol/rewarding/reward.go @@ -516,12 +516,29 @@ func (p *Protocol) slashDelegate( candidate *state.Candidate, amount *big.Int, ) (*action.Log, error) { - candidateAddr, err := address.FromString(candidate.Address) - if err != nil { - return nil, err - } - if err := stakingProtocol.SlashCandidate(ctx, sm, candidateAddr, amount); err != nil { - return nil, errors.Wrapf(err, "failed to slash candidate %s", candidate.Address) + var candidateAddr address.Address + var err error + switch { + case protocol.MustGetFeatureCtx(ctx).CandidateSlashByOwner || !protocol.MustGetFeatureWithHeightCtx(ctx).CandidateWithoutIdentity(blockHeight): + if candidate.Identity != "" { + candidateAddr, err = address.FromString(candidate.Identity) + if err != nil { + return nil, err + } + if err := stakingProtocol.SlashCandidateByID(ctx, sm, candidateAddr, amount); err != nil { + return nil, errors.Wrapf(err, "failed to slash candidate %s", candidate.Identity) + } + break + } + fallthrough + default: + candidateAddr, err = address.FromString(candidate.Address) + if err != nil { + return nil, err + } + if err := stakingProtocol.SlashCandidateByOperator(ctx, sm, candidateAddr, amount); err != nil { + return nil, errors.Wrapf(err, "failed to slash candidate %s", candidate.Address) + } } data, err := p.encodeRewardLog(rewardingpb.RewardLog_UNPRODUCTIVE_SLASH, candidateAddr.String(), amount) if err != nil { @@ -558,8 +575,13 @@ func (p *Protocol) slashUqd( slashLogs := make([]*action.Log, 0) snapshot := view.Snapshot() fCtx := protocol.MustGetFeatureCtx(ctx) + usingOperator := protocol.MustGetFeatureWithHeightCtx(ctx).CandidateWithoutIdentity(blockHeight) for _, candidate := range candidates { - if missed, ok := uqdMap[candidate.Address]; ok { + id := candidate.Identity + if usingOperator { + id = candidate.Address + } + if missed, ok := uqdMap[id]; ok { if missed == 0 { // hard probation, no slash continue @@ -571,10 +593,10 @@ func (p *Protocol) slashUqd( slashLogs = append(slashLogs, actLog) totalSlashAmount.Add(totalSlashAmount, amount) case staking.ErrNoSelfStakeBucket: - log.S().Errorf("Candidate %s doesn't have self-stake bucket, no slash", candidate.Address) + log.S().Errorf("Candidate %s doesn't have self-stake bucket, no slash", id) case staking.ErrCandidateNotExist: if !fCtx.CandidateSlashByOwner { - log.S().Errorf("Candidate %s doesn't exist, ignore slash", candidate.Address) + log.S().Errorf("Candidate %s doesn't exist, ignore slash", id) continue } fallthrough diff --git a/action/protocol/staking/candidate.go b/action/protocol/staking/candidate.go index 19d600f2f4..3017b0daec 100644 --- a/action/protocol/staking/candidate.go +++ b/action/protocol/staking/candidate.go @@ -341,14 +341,18 @@ func (d *Candidate) toIoTeXTypes() *iotextypes.CandidateV2 { } } -func (d *Candidate) toStateCandidate() *state.Candidate { - return &state.Candidate{ +func (d *Candidate) toStateCandidate(ignoreIdentity bool) *state.Candidate { + c := &state.Candidate{ Address: d.Operator.String(), // state need candidate operator not owner address Votes: new(big.Int).Set(d.Votes), RewardAddress: d.Reward.String(), CanName: []byte(d.Name), BLSPubKey: d.BLSPubKey, } + if !ignoreIdentity { + c.Identity = d.GetIdentifier().String() + } + return c } func (l CandidateList) Len() int { return len(l) } @@ -457,10 +461,10 @@ func (l *CandidateList) Decode(keys [][]byte, gvs []systemcontracts.GenericValue return nil } -func (l CandidateList) toStateCandidateList() (state.CandidateList, error) { +func (l CandidateList) toStateCandidateList(ignoreIdentity bool) (state.CandidateList, error) { list := make(state.CandidateList, 0, len(l)) for _, c := range l { - list = append(list, c.toStateCandidate()) + list = append(list, c.toStateCandidate(ignoreIdentity)) } sort.Sort(list) return list, nil diff --git a/action/protocol/staking/candidate_center_test.go b/action/protocol/staking/candidate_center_test.go index 4c192528d9..7a5f495eb7 100644 --- a/action/protocol/staking/candidate_center_test.go +++ b/action/protocol/staking/candidate_center_test.go @@ -55,7 +55,7 @@ func testEqualAllCommit(r *require.Assertions, m *CandidateCenter, old Candidate list := m.All() r.Equal(size+increase, len(list)) r.Equal(size+increase, m.Size()) - all, err := list.toStateCandidateList() + all, err := list.toStateCandidateList(true) r.NoError(err) // number of changed cand = change @@ -76,7 +76,7 @@ func testEqualAllCommit(r *require.Assertions, m *CandidateCenter, old Candidate list = m.All() r.Equal(size+increase, len(list)) r.Equal(size+increase, m.Size()) - all1, err := list.toStateCandidateList() + all1, err := list.toStateCandidateList(true) r.NoError(err) r.Equal(all, all1) return list, nil diff --git a/action/protocol/staking/candidate_test.go b/action/protocol/staking/candidate_test.go index 4f480d73f9..b12dba3758 100644 --- a/action/protocol/staking/candidate_test.go +++ b/action/protocol/staking/candidate_test.go @@ -62,6 +62,19 @@ func TestSer(t *testing.T) { r.NoError(l1.Deserialize(ser)) r.Equal(l, l1) + sl, err := l.toStateCandidateList(true) + r.NoError(err) + r.Equal(l.Len(), len(sl)) + for _, sc := range sl { + r.Equal("", sc.Identity) + } + sl, err = l.toStateCandidateList(false) + r.NoError(err) + r.Equal(l.Len(), len(sl)) + for _, sc := range sl { + r.NotEqual("", sc.Identity) + } + // empty CandidateList can successfully Serialize/Deserialize var m CandidateList ser, err = m.Serialize() @@ -102,7 +115,15 @@ func TestClone(t *testing.T) { d.Identifier = identityset.Address(3) r.Equal(d.Identifier, d.GetIdentifier()) - c := d.toStateCandidate() + c := d.toStateCandidate(false) + r.Equal(d.GetIdentifier().String(), c.Identity) + r.Equal(d.Owner.String(), c.Address) + r.Equal(d.Reward.String(), c.RewardAddress) + r.Equal(d.Votes, c.Votes) + r.Equal(d.Name, string(c.CanName)) + + c = d.toStateCandidate(true) + r.Equal("", c.Identity) r.Equal(d.Owner.String(), c.Address) r.Equal(d.Reward.String(), c.RewardAddress) r.Equal(d.Votes, c.Votes) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 53822f8266..252aaca70c 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -405,28 +405,43 @@ func (p *Protocol) CreateGenesisStates( return errors.Wrap(csm.Commit(ctx), "failed to commit candidate change in CreateGenesisStates") } -func (p *Protocol) SlashCandidate( +func (p *Protocol) SlashCandidateByOperator( ctx context.Context, sm protocol.StateManager, operator address.Address, amount *big.Int, ) error { - if amount == nil || amount.Sign() <= 0 { - return errors.New("nil or non-positive amount") + csm, err := NewCandidateStateManager(sm) + if err != nil { + return errors.Wrap(err, "failed to create candidate state manager") } + return p.slashCandidate(ctx, csm, csm.GetByOperator(operator), amount) +} + +func (p *Protocol) SlashCandidateByID( + ctx context.Context, + sm protocol.StateManager, + id address.Address, + amount *big.Int, +) error { csm, err := NewCandidateStateManager(sm) if err != nil { return errors.Wrap(err, "failed to create candidate state manager") } - fCtx := protocol.MustGetFeatureCtx(ctx) - var candidate *Candidate - if fCtx.CandidateSlashByOwner { - candidate = csm.GetByIdentifier(operator) - } else { - candidate = csm.GetByOperator(operator) + return p.slashCandidate(ctx, csm, csm.GetByIdentifier(id), amount) +} + +func (p *Protocol) slashCandidate( + ctx context.Context, + csm CandidateStateManager, + candidate *Candidate, + amount *big.Int, +) error { + if amount == nil || amount.Sign() <= 0 { + return errors.New("nil or non-positive amount") } if candidate == nil { - return errors.Wrapf(ErrCandidateNotExist, "candidate operator %s does not exist", operator.String()) + return ErrCandidateNotExist } if candidate.SelfStakeBucketIdx == candidateNoSelfStakeBucketIndex { return errors.Wrap(ErrNoSelfStakeBucket, "failed to slash candidate") @@ -864,7 +879,7 @@ func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader cand = append(cand, list[i]) } } - return cand.toStateCandidateList() + return cand.toStateCandidateList(protocol.MustGetFeatureWithHeightCtx(ctx).CandidateWithoutIdentity(height)) } // ReadState read the state on blockchain via protocol diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index f05a706df5..53943cbb45 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -177,9 +177,9 @@ func TestProtocol(t *testing.T) { } // csm's candidate center should be identical to all candidates in stateDB - c1, err := all.toStateCandidateList() + c1, err := all.toStateCandidateList(true) r.NoError(err) - c2, err := csm.DirtyView().candCenter.All().toStateCandidateList() + c2, err := csm.DirtyView().candCenter.All().toStateCandidateList(true) r.NoError(err) r.Equal(c1, c2) @@ -669,6 +669,7 @@ func TestSlashCandidate(t *testing.T) { BlockHeight: 100, }) ctx = protocol.WithFeatureCtx(ctx) + ctx = protocol.WithFeatureWithHeightCtx(ctx) t.Run("nil amount", func(t *testing.T) { err := p.SlashCandidate(ctx, sm, owner, nil) @@ -741,5 +742,10 @@ func TestSlashCandidate(t *testing.T) { ) require.NoError(err) require.Equal(1, len(cl)) + require.Equal(cl[0].Identity, "") + cl, err = p.ActiveCandidates(ctx, sm, genesis.Default.ToBeEnabledBlockHeight) + require.NoError(err) + require.Equal(1, len(cl)) + require.Equal(cl[0].Identity, owner.String()) }) } diff --git a/e2etest/contract_staking_v2_test.go b/e2etest/contract_staking_v2_test.go index c23dccd89c..062c079f60 100644 --- a/e2etest/contract_staking_v2_test.go +++ b/e2etest/contract_staking_v2_test.go @@ -2065,6 +2065,7 @@ func checkStakingViewInit(test *e2etest, require *require.Assertions) { BlockTimeStamp: tipHeader.Timestamp(), }) ctx = protocol.WithFeatureCtx(ctx) + ctx = protocol.WithFeatureWithHeightCtx(ctx) cands, err := stk.ActiveCandidates(ctx, test.cs.StateFactory(), 0) require.NoError(err) @@ -2097,6 +2098,7 @@ func checkStakingVoteView(test *e2etest, require *require.Assertions, candName s }) ctx = genesis.WithGenesisContext(ctx, test.cfg.Genesis) ctx = protocol.WithFeatureCtx(ctx) + ctx = protocol.WithFeatureWithHeightCtx(ctx) cands, err := stkPtl.ActiveCandidates(ctx, test.cs.StateFactory(), 0) require.NoError(err) cand1 := slices.IndexFunc(cands, func(c *state.Candidate) bool { diff --git a/go.mod b/go.mod index 56931316c9..8723c70415 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/iotexproject/iotex-address v0.2.9-0.20251203033311-6e8aa4fd43ef github.com/iotexproject/iotex-antenna-go/v2 v2.6.4 github.com/iotexproject/iotex-election v0.3.8-0.20251015031218-8df952babca1 - github.com/iotexproject/iotex-proto v0.6.5 + github.com/iotexproject/iotex-proto v0.6.6-0.20251119052518-636e321b7e33 github.com/ipfs/go-ipfs-api v0.7.0 github.com/libp2p/go-libp2p v0.39.0 github.com/mackerelio/go-osstat v0.2.4 diff --git a/go.sum b/go.sum index d47cf6ec60..73337b0a81 100644 --- a/go.sum +++ b/go.sum @@ -668,8 +668,8 @@ github.com/iotexproject/iotex-antenna-go/v2 v2.6.4 h1:7e0VyBDFT+iqwvr/BIk38yf7nC github.com/iotexproject/iotex-antenna-go/v2 v2.6.4/go.mod h1:L6AzDHo2TBFDAPA3ly+/PCS4JSX2g3zzhwV8RGQsTDI= github.com/iotexproject/iotex-election v0.3.8-0.20251015031218-8df952babca1 h1:jPLni/qKAnxv87HMCutde2tP9JmfWuLZgGpB4OArQGM= github.com/iotexproject/iotex-election v0.3.8-0.20251015031218-8df952babca1/go.mod h1:w9HriT1coMRbuknaSD2xqiOqDTnowBDzvFZv8tg1j2M= -github.com/iotexproject/iotex-proto v0.6.5 h1:EyzwDXYtGWvJ/qnYCwhqypOjpEAQPvESo+EdPcUJPE0= -github.com/iotexproject/iotex-proto v0.6.5/go.mod h1:OOXZIG6Q9tInog8Y5zzEJQsDv9IaG/xxpDtl4KzdWZs= +github.com/iotexproject/iotex-proto v0.6.6-0.20251119052518-636e321b7e33 h1:zMcBndFDAbu0eutHEP2VuaeSZ53HXvc4BwOnhn2YvYk= +github.com/iotexproject/iotex-proto v0.6.6-0.20251119052518-636e321b7e33/go.mod h1:OOXZIG6Q9tInog8Y5zzEJQsDv9IaG/xxpDtl4KzdWZs= github.com/ipfs/boxo v0.27.2 h1:sGo4KdwBaMjdBjH08lqPJyt27Z4CO6sugne3ryX513s= github.com/ipfs/boxo v0.27.2/go.mod h1:qEIRrGNr0bitDedTCzyzBHxzNWqYmyuHgK8LG9Q83EM= github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= diff --git a/state/candidate.go b/state/candidate.go index 8ba4fa1da8..af4484757c 100644 --- a/state/candidate.go +++ b/state/candidate.go @@ -35,7 +35,8 @@ var ( type ( // Candidate indicates the structure of a candidate Candidate struct { - Address string + Identity string // Candidate Identity + Address string // Operator address Votes *big.Int RewardAddress string CanName []byte // used as identifier to merge with native staking result, not part of protobuf @@ -57,7 +58,8 @@ func (c *Candidate) Equal(d *Candidate) bool { if c == nil || d == nil { return false } - return strings.Compare(c.Address, d.Address) == 0 && + return strings.Compare(c.Identity, d.Identity) == 0 && + strings.Compare(c.Address, d.Address) == 0 && c.RewardAddress == d.RewardAddress && c.Votes.Cmp(d.Votes) == 0 && bytes.Equal(c.BLSPubKey, d.BLSPubKey) @@ -76,6 +78,7 @@ func (c *Candidate) Clone() *Candidate { copy(pubkey, c.BLSPubKey) } return &Candidate{ + Identity: c.Identity, Address: c.Address, Votes: new(big.Int).Set(c.Votes), RewardAddress: c.RewardAddress, @@ -198,6 +201,7 @@ func (l *CandidateList) Decode(suffixs [][]byte, values []systemcontracts.Generi // candidateToPb converts a candidate to protobuf's candidate message func candidateToPb(cand *Candidate) *iotextypes.Candidate { candidatePb := &iotextypes.Candidate{ + Identity: cand.Identity, Address: cand.Address, Votes: cand.Votes.Bytes(), RewardAddress: cand.RewardAddress, @@ -217,6 +221,7 @@ func pbToCandidate(candPb *iotextypes.Candidate) (*Candidate, error) { return nil, errors.Wrap(ErrCandidatePb, "protobuf's candidate message cannot be nil") } candidate := &Candidate{ + Identity: candPb.Identity, Address: candPb.Address, Votes: big.NewInt(0).SetBytes(candPb.Votes), RewardAddress: candPb.RewardAddress,