github.com/iotexproject/iotex-core@v1.14.1-rc1/action/protocol/staking/handlers.go (about) 1 // Copyright (c) 2022 IoTeX Foundation 2 // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability 3 // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. 4 // This source code is governed by Apache License 2.0 that can be found in the LICENSE file. 5 6 package staking 7 8 import ( 9 "context" 10 "math/big" 11 "time" 12 13 "github.com/iotexproject/go-pkgs/hash" 14 "github.com/iotexproject/iotex-address/address" 15 "github.com/iotexproject/iotex-proto/golang/iotextypes" 16 "github.com/pkg/errors" 17 18 "github.com/iotexproject/iotex-core/action" 19 "github.com/iotexproject/iotex-core/action/protocol" 20 accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util" 21 "github.com/iotexproject/iotex-core/pkg/util/byteutil" 22 "github.com/iotexproject/iotex-core/state" 23 ) 24 25 // constants 26 const ( 27 HandleCreateStake = "createStake" 28 HandleUnstake = "unstake" 29 HandleWithdrawStake = "withdrawStake" 30 HandleChangeCandidate = "changeCandidate" 31 HandleTransferStake = "transferStake" 32 HandleDepositToStake = "depositToStake" 33 HandleRestake = "restake" 34 HandleCandidateRegister = "candidateRegister" 35 HandleCandidateUpdate = "candidateUpdate" 36 ) 37 38 const _withdrawWaitingTime = 14 * 24 * time.Hour // to maintain backward compatibility with r0.11 code 39 40 // Errors and vars 41 var ( 42 ErrNilParameters = errors.New("parameter is nil") 43 errCandNotExist = &handleError{ 44 err: ErrInvalidOwner, 45 failureStatus: iotextypes.ReceiptStatus_ErrCandidateNotExist, 46 } 47 ) 48 49 type handleError struct { 50 err error 51 failureStatus iotextypes.ReceiptStatus 52 } 53 54 func (h *handleError) Error() string { 55 return h.err.Error() 56 } 57 58 func (h *handleError) ReceiptStatus() uint64 { 59 return uint64(h.failureStatus) 60 } 61 62 func (p *Protocol) handleCreateStake(ctx context.Context, act *action.CreateStake, csm CandidateStateManager, 63 ) (*receiptLog, []*action.TransactionLog, error) { 64 actionCtx := protocol.MustGetActionCtx(ctx) 65 blkCtx := protocol.MustGetBlockCtx(ctx) 66 featureCtx := protocol.MustGetFeatureCtx(ctx) 67 log := newReceiptLog(p.addr.String(), HandleCreateStake, featureCtx.NewStakingReceiptFormat) 68 69 staker, fetchErr := fetchCaller(ctx, csm, act.Amount()) 70 if fetchErr != nil { 71 return log, nil, fetchErr 72 } 73 74 // Create new bucket and bucket index 75 candidate := csm.GetByName(act.Candidate()) 76 if candidate == nil { 77 return log, nil, errCandNotExist 78 } 79 bucket := NewVoteBucket(candidate.Owner, actionCtx.Caller, act.Amount(), act.Duration(), blkCtx.BlockTimeStamp, act.AutoStake()) 80 bucketIdx, err := csm.putBucketAndIndex(bucket) 81 if err != nil { 82 return log, nil, err 83 } 84 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucketIdx), candidate.Owner.Bytes()) 85 86 // update candidate 87 weightedVote := p.calculateVoteWeight(bucket, false) 88 if err := candidate.AddVote(weightedVote); err != nil { 89 return log, nil, &handleError{ 90 err: errors.Wrapf(err, "failed to add vote for candidate %s", candidate.Owner.String()), 91 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketAmount, 92 } 93 } 94 if err := csm.Upsert(candidate); err != nil { 95 return log, nil, csmErrorToHandleError(candidate.Owner.String(), err) 96 } 97 98 // update bucket pool 99 if err := csm.DebitBucketPool(act.Amount(), true); err != nil { 100 return log, nil, &handleError{ 101 err: errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()), 102 failureStatus: iotextypes.ReceiptStatus_ErrWriteAccount, 103 } 104 } 105 106 // update staker balance 107 if err := staker.SubBalance(act.Amount()); err != nil { 108 return log, nil, &handleError{ 109 err: errors.Wrapf(err, "failed to update the balance of staker %s", actionCtx.Caller.String()), 110 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 111 } 112 } 113 // put updated staker's account state to trie 114 if err := accountutil.StoreAccount(csm.SM(), actionCtx.Caller, staker); err != nil { 115 return log, nil, errors.Wrapf(err, "failed to store account %s", actionCtx.Caller.String()) 116 } 117 118 log.AddAddress(candidate.Owner) 119 log.AddAddress(actionCtx.Caller) 120 log.SetData(byteutil.Uint64ToBytesBigEndian(bucketIdx)) 121 122 return log, []*action.TransactionLog{ 123 { 124 Type: iotextypes.TransactionLogType_CREATE_BUCKET, 125 Sender: actionCtx.Caller.String(), 126 Recipient: address.StakingBucketPoolAddr, 127 Amount: act.Amount(), 128 }, 129 }, nil 130 } 131 132 func (p *Protocol) handleUnstake(ctx context.Context, act *action.Unstake, csm CandidateStateManager, 133 ) (*receiptLog, error) { 134 actionCtx := protocol.MustGetActionCtx(ctx) 135 blkCtx := protocol.MustGetBlockCtx(ctx) 136 featureCtx := protocol.MustGetFeatureCtx(ctx) 137 log := newReceiptLog(p.addr.String(), HandleUnstake, featureCtx.NewStakingReceiptFormat) 138 139 _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 140 if fetchErr != nil { 141 return log, fetchErr 142 } 143 144 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), true, true) 145 if fetchErr != nil { 146 return log, fetchErr 147 } 148 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Candidate.Bytes()) 149 150 candidate := csm.GetByOwner(bucket.Candidate) 151 if candidate == nil { 152 return log, errCandNotExist 153 } 154 155 if featureCtx.CannotUnstakeAgain && bucket.isUnstaked() { 156 return log, &handleError{ 157 err: errors.New("unstake an already unstaked bucket again not allowed"), 158 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 159 } 160 } 161 162 if bucket.AutoStake { 163 return log, &handleError{ 164 err: errors.New("AutoStake should be disabled first in order to unstake"), 165 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 166 } 167 } 168 169 if blkCtx.BlockTimeStamp.Before(bucket.StakeStartTime.Add(bucket.StakedDuration)) { 170 return log, &handleError{ 171 err: errors.New("bucket is not ready to be unstaked"), 172 failureStatus: iotextypes.ReceiptStatus_ErrUnstakeBeforeMaturity, 173 } 174 } 175 if !featureCtx.DisableDelegateEndorsement { 176 if rErr := validateBucketWithoutEndorsement(NewEndorsementStateManager(csm.SM()), bucket, blkCtx.BlockHeight); rErr != nil { 177 return log, rErr 178 } 179 } 180 // TODO: cannot unstake if selected as candidates in this or next epoch 181 182 // update bucket 183 bucket.UnstakeStartTime = blkCtx.BlockTimeStamp.UTC() 184 if err := csm.updateBucket(act.BucketIndex(), bucket); err != nil { 185 return log, errors.Wrapf(err, "failed to update bucket for voter %s", bucket.Owner.String()) 186 } 187 selfStake, err := isSelfStakeBucket(featureCtx, csm, bucket) 188 if err != nil { 189 return log, &handleError{ 190 err: err, 191 failureStatus: iotextypes.ReceiptStatus_ErrUnknown, 192 } 193 } 194 weightedVote := p.calculateVoteWeight(bucket, selfStake) 195 if err := candidate.SubVote(weightedVote); err != nil { 196 return log, &handleError{ 197 err: errors.Wrapf(err, "failed to subtract vote for candidate %s", bucket.Candidate.String()), 198 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 199 } 200 } 201 // clear candidate's self stake if the bucket is self staking 202 if selfStake { 203 candidate.SelfStake = big.NewInt(0) 204 } 205 if err := csm.Upsert(candidate); err != nil { 206 return log, csmErrorToHandleError(candidate.Owner.String(), err) 207 } 208 209 log.AddAddress(actionCtx.Caller) 210 return log, nil 211 } 212 213 func (p *Protocol) handleWithdrawStake(ctx context.Context, act *action.WithdrawStake, csm CandidateStateManager, 214 ) (*receiptLog, []*action.TransactionLog, error) { 215 actionCtx := protocol.MustGetActionCtx(ctx) 216 blkCtx := protocol.MustGetBlockCtx(ctx) 217 featureCtx := protocol.MustGetFeatureCtx(ctx) 218 log := newReceiptLog(p.addr.String(), HandleWithdrawStake, featureCtx.NewStakingReceiptFormat) 219 220 withdrawer, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 221 if fetchErr != nil { 222 return log, nil, fetchErr 223 } 224 225 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), true, true) 226 if fetchErr != nil { 227 return log, nil, fetchErr 228 } 229 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Candidate.Bytes()) 230 231 // check unstake time 232 cannotWithdraw := bucket.UnstakeStartTime.Unix() == 0 233 if featureCtx.CannotUnstakeAgain { 234 cannotWithdraw = !bucket.isUnstaked() 235 } 236 if cannotWithdraw { 237 return log, nil, &handleError{ 238 err: errors.New("bucket has not been unstaked"), 239 failureStatus: iotextypes.ReceiptStatus_ErrWithdrawBeforeUnstake, 240 } 241 } 242 243 withdrawWaitTime := p.config.WithdrawWaitingPeriod 244 if !featureCtx.NewStakingReceiptFormat { 245 withdrawWaitTime = _withdrawWaitingTime 246 } 247 if blkCtx.BlockTimeStamp.Before(bucket.UnstakeStartTime.Add(withdrawWaitTime)) { 248 return log, nil, &handleError{ 249 err: errors.New("stake is not ready to withdraw"), 250 failureStatus: iotextypes.ReceiptStatus_ErrWithdrawBeforeMaturity, 251 } 252 } 253 254 // delete bucket and bucket index 255 if err := csm.delBucketAndIndex(bucket.Owner, bucket.Candidate, act.BucketIndex()); err != nil { 256 return log, nil, errors.Wrapf(err, "failed to delete bucket for candidate %s", bucket.Candidate.String()) 257 } 258 259 // update bucket pool 260 if err := csm.CreditBucketPool(bucket.StakedAmount); err != nil { 261 return log, nil, &handleError{ 262 err: errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()), 263 failureStatus: iotextypes.ReceiptStatus_ErrWriteAccount, 264 } 265 } 266 267 // update withdrawer balance 268 if err := withdrawer.AddBalance(bucket.StakedAmount); err != nil { 269 return log, nil, errors.Wrapf(err, "failed to add balance %s", bucket.StakedAmount) 270 } 271 // put updated withdrawer's account state to trie 272 if err := accountutil.StoreAccount(csm.SM(), actionCtx.Caller, withdrawer); err != nil { 273 return log, nil, errors.Wrapf(err, "failed to store account %s", actionCtx.Caller.String()) 274 } 275 276 log.AddAddress(actionCtx.Caller) 277 if featureCtx.CannotUnstakeAgain { 278 log.SetData(bucket.StakedAmount.Bytes()) 279 } 280 281 return log, []*action.TransactionLog{ 282 { 283 Type: iotextypes.TransactionLogType_WITHDRAW_BUCKET, 284 Sender: address.StakingBucketPoolAddr, 285 Recipient: actionCtx.Caller.String(), 286 Amount: bucket.StakedAmount, 287 }, 288 }, nil 289 } 290 291 func (p *Protocol) handleChangeCandidate(ctx context.Context, act *action.ChangeCandidate, csm CandidateStateManager, 292 ) (*receiptLog, error) { 293 actionCtx := protocol.MustGetActionCtx(ctx) 294 featureCtx := protocol.MustGetFeatureCtx(ctx) 295 blkCtx := protocol.MustGetBlockCtx(ctx) 296 log := newReceiptLog(p.addr.String(), HandleChangeCandidate, featureCtx.NewStakingReceiptFormat) 297 298 _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 299 if fetchErr != nil { 300 return log, fetchErr 301 } 302 303 candidate := csm.GetByName(act.Candidate()) 304 if candidate == nil { 305 return log, errCandNotExist 306 } 307 308 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), true, false) 309 if fetchErr != nil { 310 return log, fetchErr 311 } 312 if !featureCtx.DisableDelegateEndorsement { 313 if rErr := validateBucketWithoutEndorsement(NewEndorsementStateManager(csm.SM()), bucket, blkCtx.BlockHeight); rErr != nil { 314 return log, rErr 315 } 316 } 317 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Candidate.Bytes(), candidate.Owner.Bytes()) 318 319 prevCandidate := csm.GetByOwner(bucket.Candidate) 320 if prevCandidate == nil { 321 return log, errCandNotExist 322 } 323 324 if featureCtx.CannotUnstakeAgain && bucket.isUnstaked() { 325 return log, &handleError{ 326 err: errors.New("change candidate for an unstaked bucket not allowed"), 327 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 328 } 329 } 330 331 if featureCtx.CannotTranferToSelf && address.Equal(prevCandidate.Owner, candidate.Owner) { 332 // change to same candidate, do nothing 333 return log, &handleError{ 334 err: errors.New("change to same candidate"), 335 failureStatus: iotextypes.ReceiptStatus_ErrCandidateAlreadyExist, 336 } 337 } 338 339 // update bucket index 340 if err := csm.delCandBucketIndex(bucket.Candidate, act.BucketIndex()); err != nil { 341 return log, errors.Wrapf(err, "failed to delete candidate bucket index for candidate %s", bucket.Candidate.String()) 342 } 343 if err := csm.putCandBucketIndex(candidate.Owner, act.BucketIndex()); err != nil { 344 return log, errors.Wrapf(err, "failed to put candidate bucket index for candidate %s", candidate.Owner.String()) 345 } 346 // update bucket 347 bucket.Candidate = candidate.Owner 348 if err := csm.updateBucket(act.BucketIndex(), bucket); err != nil { 349 return log, errors.Wrapf(err, "failed to update bucket for voter %s", bucket.Owner.String()) 350 } 351 352 // update previous candidate 353 weightedVotes := p.calculateVoteWeight(bucket, false) 354 if err := prevCandidate.SubVote(weightedVotes); err != nil { 355 return log, &handleError{ 356 err: errors.Wrapf(err, "failed to subtract vote for previous candidate %s", prevCandidate.Owner.String()), 357 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 358 } 359 } 360 // if the bucket equals to the previous candidate's self-stake bucket, it must be expired endorse bucket 361 // so we need to clear the self-stake of the previous candidate 362 if !featureCtx.DisableDelegateEndorsement && prevCandidate.SelfStakeBucketIdx == bucket.Index { 363 prevCandidate.SelfStake.SetInt64(0) 364 prevCandidate.SelfStakeBucketIdx = candidateNoSelfStakeBucketIndex 365 } 366 if err := csm.Upsert(prevCandidate); err != nil { 367 return log, csmErrorToHandleError(prevCandidate.Owner.String(), err) 368 } 369 370 // update current candidate 371 if err := candidate.AddVote(weightedVotes); err != nil { 372 return log, &handleError{ 373 err: errors.Wrapf(err, "failed to add vote for candidate %s", candidate.Owner.String()), 374 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketAmount, 375 } 376 } 377 if err := csm.Upsert(candidate); err != nil { 378 return log, csmErrorToHandleError(candidate.Owner.String(), err) 379 } 380 381 log.AddAddress(candidate.Owner) 382 log.AddAddress(actionCtx.Caller) 383 return log, nil 384 } 385 386 func (p *Protocol) handleTransferStake(ctx context.Context, act *action.TransferStake, csm CandidateStateManager, 387 ) (*receiptLog, error) { 388 actionCtx := protocol.MustGetActionCtx(ctx) 389 featureCtx := protocol.MustGetFeatureCtx(ctx) 390 log := newReceiptLog(p.addr.String(), HandleTransferStake, featureCtx.NewStakingReceiptFormat) 391 392 _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 393 if fetchErr != nil { 394 return log, fetchErr 395 } 396 397 newOwner := act.VoterAddress() 398 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), true, false) 399 if fetchErr != nil { 400 if featureCtx.ReturnFetchError || 401 fetchErr.ReceiptStatus() != uint64(iotextypes.ReceiptStatus_ErrUnauthorizedOperator) { 402 return log, fetchErr 403 } 404 405 // check whether the payload contains a valid consignment transfer 406 if consignment, ok := p.handleConsignmentTransfer(csm, ctx, act, bucket); ok { 407 newOwner = consignment.Transferee() 408 } else { 409 return log, fetchErr 410 } 411 } 412 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), act.VoterAddress().Bytes(), bucket.Candidate.Bytes()) 413 414 if featureCtx.CannotTranferToSelf && address.Equal(newOwner, bucket.Owner) { 415 // change to same owner, do nothing 416 return log, &handleError{ 417 err: errors.New("transfer to same owner"), 418 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 419 } 420 } 421 422 // update bucket index 423 if err := csm.delVoterBucketIndex(bucket.Owner, act.BucketIndex()); err != nil { 424 return log, errors.Wrapf(err, "failed to delete voter bucket index for voter %s", bucket.Owner.String()) 425 } 426 if err := csm.putVoterBucketIndex(newOwner, act.BucketIndex()); err != nil { 427 return log, errors.Wrapf(err, "failed to put candidate bucket index for voter %s", act.VoterAddress().String()) 428 } 429 430 // update bucket 431 bucket.Owner = newOwner 432 if err := csm.updateBucket(act.BucketIndex(), bucket); err != nil { 433 return log, errors.Wrapf(err, "failed to update bucket for voter %s", bucket.Owner.String()) 434 } 435 436 log.AddAddress(actionCtx.Caller) 437 return log, nil 438 } 439 440 func (p *Protocol) handleConsignmentTransfer( 441 csm CandidateStateManager, 442 ctx context.Context, 443 act *action.TransferStake, 444 bucket *VoteBucket) (action.Consignment, bool) { 445 if len(act.Payload()) == 0 { 446 return nil, false 447 } 448 449 // self-stake cannot be transferred 450 actCtx := protocol.MustGetActionCtx(ctx) 451 featureCtx := protocol.MustGetFeatureCtx(ctx) 452 selfStake, err := isSelfStakeBucket(featureCtx, csm, bucket) 453 if err != nil || selfStake { 454 return nil, false 455 } 456 457 con, err := action.NewConsignment(act.Payload()) 458 if err != nil { 459 return nil, false 460 } 461 462 // a consignment transfer is valid if: 463 // (1) signer owns the bucket 464 // (2) designated transferee matches the action caller 465 // (3) designated asset ID matches bucket index 466 // (4) nonce matches the action caller's nonce 467 return con, address.Equal(con.Transferor(), bucket.Owner) && 468 address.Equal(con.Transferee(), actCtx.Caller) && 469 con.AssetID() == bucket.Index && 470 con.TransfereeNonce() == actCtx.Nonce 471 } 472 473 func (p *Protocol) handleDepositToStake(ctx context.Context, act *action.DepositToStake, csm CandidateStateManager, 474 ) (*receiptLog, []*action.TransactionLog, error) { 475 actionCtx := protocol.MustGetActionCtx(ctx) 476 featureCtx := protocol.MustGetFeatureCtx(ctx) 477 log := newReceiptLog(p.addr.String(), HandleDepositToStake, featureCtx.NewStakingReceiptFormat) 478 479 depositor, fetchErr := fetchCaller(ctx, csm, act.Amount()) 480 if fetchErr != nil { 481 return log, nil, fetchErr 482 } 483 484 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), false, true) 485 if fetchErr != nil { 486 return log, nil, fetchErr 487 } 488 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Owner.Bytes(), bucket.Candidate.Bytes()) 489 if !bucket.AutoStake { 490 return log, nil, &handleError{ 491 err: errors.New("deposit is only allowed on auto-stake bucket"), 492 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 493 } 494 } 495 candidate := csm.GetByOwner(bucket.Candidate) 496 if candidate == nil { 497 return log, nil, errCandNotExist 498 } 499 500 if featureCtx.CannotUnstakeAgain && bucket.isUnstaked() { 501 return log, nil, &handleError{ 502 err: errors.New("deposit to an unstaked bucket not allowed"), 503 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 504 } 505 } 506 selfStake, err := isSelfStakeBucket(featureCtx, csm, bucket) 507 if err != nil { 508 return log, nil, &handleError{ 509 err: err, 510 failureStatus: iotextypes.ReceiptStatus_ErrUnknown, 511 } 512 } 513 prevWeightedVotes := p.calculateVoteWeight(bucket, selfStake) 514 // update bucket 515 bucket.StakedAmount.Add(bucket.StakedAmount, act.Amount()) 516 if err := csm.updateBucket(act.BucketIndex(), bucket); err != nil { 517 return log, nil, errors.Wrapf(err, "failed to update bucket for voter %s", bucket.Owner.String()) 518 } 519 520 // update candidate 521 if err := candidate.SubVote(prevWeightedVotes); err != nil { 522 return log, nil, &handleError{ 523 err: errors.Wrapf(err, "failed to subtract vote for candidate %s", bucket.Candidate.String()), 524 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 525 } 526 } 527 weightedVotes := p.calculateVoteWeight(bucket, selfStake) 528 if err := candidate.AddVote(weightedVotes); err != nil { 529 return log, nil, &handleError{ 530 err: errors.Wrapf(err, "failed to add vote for candidate %s", candidate.Owner.String()), 531 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketAmount, 532 } 533 } 534 if selfStake { 535 if err := candidate.AddSelfStake(act.Amount()); err != nil { 536 return log, nil, &handleError{ 537 err: errors.Wrapf(err, "failed to add self stake for candidate %s", candidate.Owner.String()), 538 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketAmount, 539 } 540 } 541 } 542 if err := csm.Upsert(candidate); err != nil { 543 return log, nil, csmErrorToHandleError(candidate.Owner.String(), err) 544 } 545 546 // update bucket pool 547 if err := csm.DebitBucketPool(act.Amount(), false); err != nil { 548 return log, nil, &handleError{ 549 err: errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()), 550 failureStatus: iotextypes.ReceiptStatus_ErrWriteAccount, 551 } 552 } 553 554 // update depositor balance 555 if err := depositor.SubBalance(act.Amount()); err != nil { 556 return log, nil, &handleError{ 557 err: errors.Wrapf(err, "failed to update the balance of depositor %s", actionCtx.Caller.String()), 558 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 559 } 560 } 561 // put updated depositor's account state to trie 562 if err := accountutil.StoreAccount(csm.SM(), actionCtx.Caller, depositor); err != nil { 563 return log, nil, errors.Wrapf(err, "failed to store account %s", actionCtx.Caller.String()) 564 } 565 log.AddAddress(actionCtx.Caller) 566 567 return log, []*action.TransactionLog{ 568 { 569 Type: iotextypes.TransactionLogType_DEPOSIT_TO_BUCKET, 570 Sender: actionCtx.Caller.String(), 571 Recipient: address.StakingBucketPoolAddr, 572 Amount: act.Amount(), 573 }, 574 }, nil 575 } 576 577 func (p *Protocol) handleRestake(ctx context.Context, act *action.Restake, csm CandidateStateManager, 578 ) (*receiptLog, error) { 579 actionCtx := protocol.MustGetActionCtx(ctx) 580 blkCtx := protocol.MustGetBlockCtx(ctx) 581 featureCtx := protocol.MustGetFeatureCtx(ctx) 582 log := newReceiptLog(p.addr.String(), HandleRestake, featureCtx.NewStakingReceiptFormat) 583 584 _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 585 if fetchErr != nil { 586 return log, fetchErr 587 } 588 589 bucket, fetchErr := p.fetchBucketAndValidate(featureCtx, csm, actionCtx.Caller, act.BucketIndex(), true, true) 590 if fetchErr != nil { 591 return log, fetchErr 592 } 593 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Candidate.Bytes()) 594 595 candidate := csm.GetByOwner(bucket.Candidate) 596 if candidate == nil { 597 return log, errCandNotExist 598 } 599 600 if featureCtx.CannotUnstakeAgain && bucket.isUnstaked() { 601 return log, &handleError{ 602 err: errors.New("restake an unstaked bucket not allowed"), 603 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 604 } 605 } 606 selfStake, err := isSelfStakeBucket(featureCtx, csm, bucket) 607 if err != nil { 608 return log, &handleError{ 609 err: err, 610 failureStatus: iotextypes.ReceiptStatus_ErrUnknown, 611 } 612 } 613 prevWeightedVotes := p.calculateVoteWeight(bucket, selfStake) 614 // update bucket 615 actDuration := time.Duration(act.Duration()) * 24 * time.Hour 616 if bucket.StakedDuration.Hours() > actDuration.Hours() { 617 // in case of reducing the duration 618 if bucket.AutoStake { 619 // if auto-stake on, user can't reduce duration 620 return log, &handleError{ 621 err: errors.New("AutoStake should be disabled first in order to reduce duration"), 622 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 623 } 624 } else if blkCtx.BlockTimeStamp.Before(bucket.StakeStartTime.Add(bucket.StakedDuration)) { 625 // if auto-stake off and maturity is not enough 626 return log, &handleError{ 627 err: errors.New("bucket is not ready to be able to reduce duration"), 628 failureStatus: iotextypes.ReceiptStatus_ErrReduceDurationBeforeMaturity, 629 } 630 } 631 } 632 bucket.StakedDuration = actDuration 633 bucket.StakeStartTime = blkCtx.BlockTimeStamp.UTC() 634 bucket.AutoStake = act.AutoStake() 635 if err := csm.updateBucket(act.BucketIndex(), bucket); err != nil { 636 return log, errors.Wrapf(err, "failed to update bucket for voter %s", bucket.Owner.String()) 637 } 638 639 // update candidate 640 if err := candidate.SubVote(prevWeightedVotes); err != nil { 641 return log, &handleError{ 642 err: errors.Wrapf(err, "failed to subtract vote for candidate %s", bucket.Candidate.String()), 643 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 644 } 645 } 646 weightedVotes := p.calculateVoteWeight(bucket, selfStake) 647 if err := candidate.AddVote(weightedVotes); err != nil { 648 return log, &handleError{ 649 err: errors.Wrapf(err, "failed to add vote for candidate %s", candidate.Owner.String()), 650 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketAmount, 651 } 652 } 653 if err := csm.Upsert(candidate); err != nil { 654 return log, csmErrorToHandleError(candidate.Owner.String(), err) 655 } 656 657 log.AddAddress(actionCtx.Caller) 658 return log, nil 659 } 660 661 func (p *Protocol) handleCandidateRegister(ctx context.Context, act *action.CandidateRegister, csm CandidateStateManager, 662 ) (*receiptLog, []*action.TransactionLog, error) { 663 actCtx := protocol.MustGetActionCtx(ctx) 664 blkCtx := protocol.MustGetBlockCtx(ctx) 665 featureCtx := protocol.MustGetFeatureCtx(ctx) 666 log := newReceiptLog(p.addr.String(), HandleCandidateRegister, featureCtx.NewStakingReceiptFormat) 667 668 registrationFee := new(big.Int).Set(p.config.RegistrationConsts.Fee) 669 670 caller, fetchErr := fetchCaller(ctx, csm, new(big.Int).Add(act.Amount(), registrationFee)) 671 if fetchErr != nil { 672 return log, nil, fetchErr 673 } 674 675 owner := actCtx.Caller 676 if act.OwnerAddress() != nil { 677 owner = act.OwnerAddress() 678 } 679 680 c := csm.GetByOwner(owner) 681 ownerExist := c != nil 682 // cannot collide with existing owner (with selfstake != 0) 683 if ownerExist && c.SelfStake.Cmp(big.NewInt(0)) != 0 { 684 return log, nil, &handleError{ 685 err: ErrInvalidOwner, 686 failureStatus: iotextypes.ReceiptStatus_ErrCandidateAlreadyExist, 687 } 688 } 689 // cannot collide with existing name 690 if csm.ContainsName(act.Name()) && (!ownerExist || act.Name() != c.Name) { 691 return log, nil, &handleError{ 692 err: action.ErrInvalidCanName, 693 failureStatus: iotextypes.ReceiptStatus_ErrCandidateConflict, 694 } 695 } 696 // cannot collide with existing operator address 697 if csm.ContainsOperator(act.OperatorAddress()) && 698 (!ownerExist || !address.Equal(act.OperatorAddress(), c.Operator)) { 699 return log, nil, &handleError{ 700 err: ErrInvalidOperator, 701 failureStatus: iotextypes.ReceiptStatus_ErrCandidateConflict, 702 } 703 } 704 705 var ( 706 bucketIdx uint64 707 votes *big.Int 708 withSelfStake = act.Amount().Sign() > 0 709 txLogs []*action.TransactionLog 710 err error 711 ) 712 if withSelfStake { 713 // register with self-stake 714 bucket := NewVoteBucket(owner, owner, act.Amount(), act.Duration(), blkCtx.BlockTimeStamp, act.AutoStake()) 715 bucketIdx, err = csm.putBucketAndIndex(bucket) 716 if err != nil { 717 return log, nil, err 718 } 719 txLogs = append(txLogs, &action.TransactionLog{ 720 Type: iotextypes.TransactionLogType_CANDIDATE_SELF_STAKE, 721 Sender: actCtx.Caller.String(), 722 Recipient: address.StakingBucketPoolAddr, 723 Amount: act.Amount(), 724 }) 725 votes = p.calculateVoteWeight(bucket, true) 726 } else { 727 // register w/o self-stake, waiting to be endorsed 728 bucketIdx = uint64(candidateNoSelfStakeBucketIndex) 729 votes = big.NewInt(0) 730 } 731 log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucketIdx), owner.Bytes()) 732 733 c = &Candidate{ 734 Owner: owner, 735 Operator: act.OperatorAddress(), 736 Reward: act.RewardAddress(), 737 Name: act.Name(), 738 Votes: votes, 739 SelfStakeBucketIdx: bucketIdx, 740 SelfStake: act.Amount(), 741 } 742 743 if err := csm.Upsert(c); err != nil { 744 return log, nil, csmErrorToHandleError(owner.String(), err) 745 } 746 height, _ := csm.SM().Height() 747 if p.needToWriteCandsMap(ctx, height) { 748 csm.DirtyView().candCenter.base.recordOwner(c) 749 } 750 751 if withSelfStake { 752 // update bucket pool 753 if err := csm.DebitBucketPool(act.Amount(), true); err != nil { 754 return log, nil, &handleError{ 755 err: errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()), 756 failureStatus: iotextypes.ReceiptStatus_ErrWriteAccount, 757 } 758 } 759 // update caller balance 760 if err := caller.SubBalance(act.Amount()); err != nil { 761 return log, nil, &handleError{ 762 err: errors.Wrapf(err, "failed to update the balance of register %s", actCtx.Caller.String()), 763 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 764 } 765 } 766 // put updated caller's account state to trie 767 if err := accountutil.StoreAccount(csm.SM(), actCtx.Caller, caller); err != nil { 768 return log, nil, errors.Wrapf(err, "failed to store account %s", actCtx.Caller.String()) 769 } 770 } 771 772 // put registrationFee to reward pool 773 if _, err := p.depositGas(ctx, csm.SM(), registrationFee); err != nil { 774 return log, nil, errors.Wrap(err, "failed to deposit gas") 775 } 776 777 log.AddAddress(owner) 778 log.AddAddress(actCtx.Caller) 779 log.SetData(byteutil.Uint64ToBytesBigEndian(bucketIdx)) 780 781 txLogs = append(txLogs, &action.TransactionLog{ 782 Type: iotextypes.TransactionLogType_CANDIDATE_REGISTRATION_FEE, 783 Sender: actCtx.Caller.String(), 784 Recipient: address.RewardingPoolAddr, 785 Amount: registrationFee, 786 }) 787 return log, txLogs, nil 788 } 789 790 func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.CandidateUpdate, csm CandidateStateManager, 791 ) (*receiptLog, error) { 792 actCtx := protocol.MustGetActionCtx(ctx) 793 featureCtx := protocol.MustGetFeatureCtx(ctx) 794 log := newReceiptLog(p.addr.String(), HandleCandidateUpdate, featureCtx.NewStakingReceiptFormat) 795 796 _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) 797 if fetchErr != nil { 798 return log, fetchErr 799 } 800 801 // only owner can update candidate 802 c := csm.GetByOwner(actCtx.Caller) 803 if c == nil { 804 return log, errCandNotExist 805 } 806 807 if len(act.Name()) != 0 { 808 c.Name = act.Name() 809 } 810 811 if act.OperatorAddress() != nil { 812 c.Operator = act.OperatorAddress() 813 } 814 815 if act.RewardAddress() != nil { 816 c.Reward = act.RewardAddress() 817 } 818 log.AddTopics(c.Owner.Bytes()) 819 820 if err := csm.Upsert(c); err != nil { 821 return log, csmErrorToHandleError(c.Owner.String(), err) 822 } 823 height, _ := csm.SM().Height() 824 if p.needToWriteCandsMap(ctx, height) { 825 csm.DirtyView().candCenter.base.recordOwner(c) 826 } 827 828 log.AddAddress(actCtx.Caller) 829 return log, nil 830 } 831 832 func (p *Protocol) fetchBucket(csm CandidateStateManager, index uint64) (*VoteBucket, ReceiptError) { 833 bucket, err := csm.getBucket(index) 834 if err != nil { 835 fetchErr := &handleError{ 836 err: errors.Wrapf(err, "failed to fetch bucket by index %d", index), 837 failureStatus: iotextypes.ReceiptStatus_Failure, 838 } 839 if errors.Cause(err) == state.ErrStateNotExist { 840 fetchErr.failureStatus = iotextypes.ReceiptStatus_ErrInvalidBucketIndex 841 } 842 return nil, fetchErr 843 } 844 return bucket, nil 845 } 846 847 func (p *Protocol) fetchBucketAndValidate( 848 featureCtx protocol.FeatureCtx, 849 csm CandidateStateManager, 850 caller address.Address, 851 index uint64, 852 checkOwner bool, 853 allowSelfStaking bool, 854 ) (*VoteBucket, ReceiptError) { 855 bucket, err := p.fetchBucket(csm, index) 856 if err != nil { 857 return nil, err 858 } 859 860 // ReceiptStatus_ErrUnauthorizedOperator indicates action caller is not bucket owner 861 // upon return, the action will be subject to check whether it contains a valid consignment transfer 862 // do NOT return this value in case changes are added in the future 863 if checkOwner && !address.Equal(bucket.Owner, caller) { 864 return bucket, &handleError{ 865 err: errors.New("bucket owner does not match action caller"), 866 failureStatus: iotextypes.ReceiptStatus_ErrUnauthorizedOperator, 867 } 868 } 869 if !allowSelfStaking { 870 selfStaking, serr := isSelfStakeBucket(featureCtx, csm, bucket) 871 if serr != nil { 872 return bucket, &handleError{ 873 err: serr, 874 failureStatus: iotextypes.ReceiptStatus_ErrUnknown, 875 } 876 } 877 if selfStaking { 878 return bucket, &handleError{ 879 err: errors.New("self staking bucket cannot be processed"), 880 failureStatus: iotextypes.ReceiptStatus_ErrInvalidBucketType, 881 } 882 } 883 } 884 return bucket, nil 885 } 886 887 func fetchCaller(ctx context.Context, csm CandidateStateManager, amount *big.Int) (*state.Account, ReceiptError) { 888 actionCtx := protocol.MustGetActionCtx(ctx) 889 accountCreationOpts := []state.AccountCreationOption{} 890 if protocol.MustGetFeatureCtx(ctx).CreateLegacyNonceAccount { 891 accountCreationOpts = append(accountCreationOpts, state.LegacyNonceAccountTypeOption()) 892 } 893 caller, err := accountutil.LoadAccount(csm.SM(), actionCtx.Caller, accountCreationOpts...) 894 if err != nil { 895 return nil, &handleError{ 896 err: errors.Wrapf(err, "failed to load the account of caller %s", actionCtx.Caller.String()), 897 failureStatus: iotextypes.ReceiptStatus_Failure, 898 } 899 } 900 gasFee := big.NewInt(0).Mul(actionCtx.GasPrice, big.NewInt(0).SetUint64(actionCtx.IntrinsicGas)) 901 // check caller's balance 902 if !caller.HasSufficientBalance(new(big.Int).Add(amount, gasFee)) { 903 return nil, &handleError{ 904 err: errors.Wrapf(state.ErrNotEnoughBalance, "caller %s balance not enough", actionCtx.Caller.String()), 905 failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, 906 } 907 } 908 return caller, nil 909 } 910 911 func csmErrorToHandleError(caller string, err error) error { 912 hErr := &handleError{ 913 err: errors.Wrapf(err, "failed to put candidate %s", caller), 914 } 915 916 switch errors.Cause(err) { 917 case action.ErrInvalidCanName: 918 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateConflict 919 return hErr 920 case ErrInvalidOperator: 921 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateConflict 922 return hErr 923 case ErrInvalidSelfStkIndex: 924 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateConflict 925 return hErr 926 case action.ErrInvalidAmount: 927 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateNotExist 928 return hErr 929 case ErrInvalidOwner: 930 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateNotExist 931 return hErr 932 case ErrInvalidReward: 933 hErr.failureStatus = iotextypes.ReceiptStatus_ErrCandidateNotExist 934 return hErr 935 default: 936 return err 937 } 938 } 939 940 // BucketIndexFromReceiptLog extracts bucket index from log 941 func BucketIndexFromReceiptLog(log *iotextypes.Log) (uint64, bool) { 942 if log == nil || len(log.Topics) < 2 { 943 return 0, false 944 } 945 946 h := hash.Hash160b([]byte(_protocolID)) 947 addr, _ := address.FromBytes(h[:]) 948 if log.ContractAddress != addr.String() { 949 return 0, false 950 } 951 952 switch hash.BytesToHash256(log.Topics[0]) { 953 case hash.BytesToHash256([]byte(HandleCreateStake)), hash.BytesToHash256([]byte(HandleUnstake)), 954 hash.BytesToHash256([]byte(HandleWithdrawStake)), hash.BytesToHash256([]byte(HandleChangeCandidate)), 955 hash.BytesToHash256([]byte(HandleTransferStake)), hash.BytesToHash256([]byte(HandleDepositToStake)), 956 hash.BytesToHash256([]byte(HandleRestake)), hash.BytesToHash256([]byte(HandleCandidateRegister)): 957 return byteutil.BytesToUint64BigEndian(log.Topics[1][24:]), true 958 default: 959 return 0, false 960 } 961 }