From 56ddea7c83179e65f5a57ac741bc24e24ef87207 Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 1 Dec 2025 22:11:28 +0800 Subject: [PATCH] operator update blspubkey --- action/protocol/context.go | 2 ++ action/protocol/staking/handlers.go | 49 ++++++++++++++++++++++++++++- action/signedaction.go | 30 ++++++++++++++++++ e2etest/native_staking_test.go | 41 ++++++++++++++++++++---- 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/action/protocol/context.go b/action/protocol/context.go index 9b30146b3e..b89b969649 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -165,6 +165,7 @@ type ( StoreVoteOfNFTBucketIntoView bool CandidateSlashByOwner bool CandidateBLSPublicKeyNotCopied bool + OnlyOwnerCanUpdateBLSPublicKey bool } // FeatureWithHeightCtx provides feature check functions. @@ -333,6 +334,7 @@ func WithFeatureCtx(ctx context.Context) context.Context { StoreVoteOfNFTBucketIntoView: !g.IsXingu(height), CandidateSlashByOwner: !g.IsXinguBeta(height), CandidateBLSPublicKeyNotCopied: !g.IsXinguBeta(height), + OnlyOwnerCanUpdateBLSPublicKey: !g.IsToBeEnabled(height), }, ) } diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index 10ee230b67..5945b431e7 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -856,7 +856,14 @@ func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.Candid // only owner can update candidate c := csm.GetByOwner(actCtx.Caller) if c == nil { - return log, errCandNotExist + if featureCtx.OnlyOwnerCanUpdateBLSPublicKey { + return log, errCandNotExist + } + c = csm.GetByOperator(actCtx.Caller) + if c == nil { + return log, errCandNotExist + } + return p.handleCandidateUpdateByOperator(ctx, act, csm, c, log) } if len(act.Name()) != 0 { @@ -893,6 +900,46 @@ func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.Candid return log, nil } +func (p *Protocol) handleCandidateUpdateByOperator(ctx context.Context, act *action.CandidateUpdate, csm CandidateStateManager, c *Candidate, log *receiptLog) (*receiptLog, error) { + // operator can only update BLS public key + if !act.WithBLS() { + return log, &handleError{ + err: errors.New("BLS public key must be provided when updating by operator"), + failureStatus: iotextypes.ReceiptStatus_ErrUnauthorizedOperator, + } + } + if len(act.Name()) > 0 && act.Name() != c.Name { + return log, &handleError{ + err: errors.New("candidate name cannot be updated by operator"), + failureStatus: iotextypes.ReceiptStatus_ErrUnauthorizedOperator, + } + } + if act.OperatorAddress() != nil && !address.Equal(act.OperatorAddress(), c.Operator) { + return log, &handleError{ + err: errors.New("candidate operator cannot be updated by operator"), + failureStatus: iotextypes.ReceiptStatus_ErrUnauthorizedOperator, + } + } + if act.RewardAddress() != nil && !address.Equal(act.RewardAddress(), c.Reward) { + return log, &handleError{ + err: errors.New("candidate reward address cannot be updated by operator"), + failureStatus: iotextypes.ReceiptStatus_ErrUnauthorizedOperator, + } + } + // update BLS public key + c.BLSPubKey = act.BLSPubKey() + topics, eventData, err := action.PackCandidateUpdatedEvent(c.GetIdentifier(), c.Operator, c.Owner, c.Name, c.Reward, act.BLSPubKey()) + if err != nil { + return log, errors.Wrap(err, "failed to pack candidate register with BLS event") + } + log.AddEvent(topics, eventData) + log.AddTopics(c.GetIdentifier().Bytes()) + if err := csm.Upsert(c); err != nil { + return log, csmErrorToHandleError(c.GetIdentifier().String(), err) + } + return log, nil +} + func (p *Protocol) fetchBucket(csm NativeBucketGetByIndex, index uint64) (*VoteBucket, ReceiptError) { bucket, err := csm.NativeBucket(index) if err != nil { diff --git a/action/signedaction.go b/action/signedaction.go index 1cb6236a44..71eb58ffa6 100644 --- a/action/signedaction.go +++ b/action/signedaction.go @@ -161,6 +161,36 @@ func SignedCandidateUpdate( return selp, nil } +// SignedCandidateUpdateWithBLS returns a signed candidate update with BLS public key +func SignedCandidateUpdateWithBLS( + nonce uint64, + name, operatorAddrStr, rewardAddrStr string, + blsPubKey []byte, + gasLimit uint64, + gasPrice *big.Int, + registererPriKey crypto.PrivateKey, + options ...SignedActionOption, +) (*SealedEnvelope, error) { + cu, err := NewCandidateUpdateWithBLS(name, operatorAddrStr, rewardAddrStr, blsPubKey) + if err != nil { + return nil, err + } + bd := &EnvelopeBuilder{} + bd = bd.SetNonce(nonce). + SetGasPrice(gasPrice). + SetGasLimit(gasLimit). + SetAction(cu) + for _, opt := range options { + opt(bd) + } + elp := bd.Build() + selp, err := Sign(elp, registererPriKey) + if err != nil { + return nil, errors.Wrapf(err, "failed to sign candidate update %v", elp) + } + return selp, nil +} + // SignedCandidateActivate returns a signed candidate selfstake func SignedCandidateActivate( nonce uint64, diff --git a/e2etest/native_staking_test.go b/e2etest/native_staking_test.go index d0134f41dc..122fb5bd00 100644 --- a/e2etest/native_staking_test.go +++ b/e2etest/native_staking_test.go @@ -1581,6 +1581,8 @@ func TestCandidateBLSPublicKey(t *testing.T) { cfg := initCfg(require) cfg.Genesis.WakeBlockHeight = 1 cfg.Genesis.XinguBlockHeight = 10 // enable CandidateBLSPublicKey feature + cfg.Genesis.XinguBetaBlockHeight = 11 + cfg.Genesis.ToBeEnabledBlockHeight = 20 // enable candidate BLS key update by operator feature cfg.Genesis.SystemStakingContractAddress = "" cfg.Genesis.SystemStakingContractV2Address = "" cfg.Genesis.SystemStakingContractV3Address = "" @@ -1592,10 +1594,12 @@ func TestCandidateBLSPublicKey(t *testing.T) { test := newE2ETest(t, cfg) var ( - chainID = test.cfg.Chain.ID - registerAmount = unit.ConvertIotxToRau(1200000) - candOwnerID = 3 - candOwnerID2 = 4 + chainID = test.cfg.Chain.ID + registerAmount = unit.ConvertIotxToRau(1200000) + candOwnerID = 3 + candOperatorID = 1 + candOwnerID2 = 4 + candOperatorID2 = 2 ) genTransferActionsWithPrice := func(n int, price *big.Int) []*actionWithTime { acts := make([]*actionWithTime, n) @@ -1611,7 +1615,7 @@ func TestCandidateBLSPublicKey(t *testing.T) { { name: "register without bls key", acts: []*actionWithTime{ - {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(candOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(candOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice1559, identityset.PrivateKey(candOwnerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(candOwnerID).String()), "cand1", identityset.Address(candOperatorID).String(), identityset.Address(1).String(), identityset.Address(candOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice1559, identityset.PrivateKey(candOwnerID), action.WithChainID(chainID))), time.Now()}, }, blockExpect: func(test *e2etest, blk *block.Block, err error) { require.NoError(err) @@ -1627,7 +1631,7 @@ func TestCandidateBLSPublicKey(t *testing.T) { name: "register with bls key", preActs: genTransferActionsWithPrice(int(cfg.Genesis.XinguBlockHeight), gasPrice1559), acts: []*actionWithTime{ - {mustNoErr(action.SignedCandidateRegisterWithBLS(test.nonceMgr.pop(identityset.Address(candOwnerID2).String()), "cand2", identityset.Address(2).String(), identityset.Address(2).String(), identityset.Address(candOwnerID2).String(), registerAmount.String(), 1, true, blsPubKey, []byte{1, 2, 3}, gasLimit, gasPrice, identityset.PrivateKey(candOwnerID2), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedCandidateRegisterWithBLS(test.nonceMgr.pop(identityset.Address(candOwnerID2).String()), "cand2", identityset.Address(candOperatorID2).String(), identityset.Address(2).String(), identityset.Address(candOwnerID2).String(), registerAmount.String(), 1, true, blsPubKey, []byte{1, 2, 3}, gasLimit, gasPrice, identityset.PrivateKey(candOwnerID2), action.WithChainID(chainID))), time.Now()}, }, blockExpect: func(test *e2etest, blk *block.Block, err error) { require.NoError(err) @@ -1640,6 +1644,31 @@ func TestCandidateBLSPublicKey(t *testing.T) { }, }, }) + height, err := test.cs.BlockDAO().Height() + require.NoError(err) + jumps := int(cfg.Genesis.ToBeEnabledBlockHeight - height) + if jumps <= 0 { + jumps = 1 + } + blsPrivKey2, err := crypto.GenerateBLS12381PrivateKey(identityset.PrivateKey(candOperatorID).Bytes()) + require.NoError(err) + test.run([]*testcase{ + { + name: "update bls key by operator", + preActs: genTransferActionsWithPrice(jumps, gasPrice1559), + acts: []*actionWithTime{ + {mustNoErr(action.SignedCandidateUpdateWithBLS(test.nonceMgr.pop(identityset.Address(candOperatorID).String()), "cand1", identityset.Address(candOperatorID).String(), "", blsPrivKey2.PublicKey().Bytes(), gasLimit, gasPrice, identityset.PrivateKey(candOperatorID), action.WithChainID(chainID))), time.Now()}, + }, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + require.EqualValues(2, len(blk.Receipts)) + require.EqualValues(iotextypes.ReceiptStatus_Success, blk.Receipts[0].Status) + cand, err := test.getCandidateByName("cand1") + require.NoError(err) + require.EqualValues(blsPrivKey2.PublicKey().Bytes(), cand.BlsPubKey) + }, + }, + }) } func parseNativeStakedBucketIndex(receipt *action.Receipt) []uint64 {