mirror of https://github.com/poanetwork/gecko.git
263 lines
9.7 KiB
Go
263 lines
9.7 KiB
Go
// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package platformvm
|
|
|
|
import (
|
|
"container/heap"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/ava-labs/gecko/database"
|
|
"github.com/ava-labs/gecko/database/versiondb"
|
|
"github.com/ava-labs/gecko/ids"
|
|
"github.com/ava-labs/gecko/utils/math"
|
|
)
|
|
|
|
var (
|
|
errShouldBeDSValidator = errors.New("expected validator to be in the default subnet")
|
|
)
|
|
|
|
// rewardValidatorTx is a transaction that represents a proposal to remove a
|
|
// validator that is currently validating from the validator set.
|
|
//
|
|
// If this transaction is accepted and the next block accepted is a *Commit
|
|
// block, the validator is removed and the account that the validator specified
|
|
// receives the staked $AVA as well as a validating reward.
|
|
//
|
|
// If this transaction is accepted and the next block accepted is an *Abort
|
|
// block, the validator is removed and the account that the validator specified
|
|
// receives the staked $AVA but no reward.
|
|
type rewardValidatorTx struct {
|
|
// ID of the tx that created the delegator/validator being removed/rewarded
|
|
TxID ids.ID `serialize:"true"`
|
|
|
|
vm *VM
|
|
}
|
|
|
|
func (tx *rewardValidatorTx) initialize(vm *VM) error {
|
|
tx.vm = vm
|
|
return nil
|
|
}
|
|
|
|
// SyntacticVerify that this transaction is well formed
|
|
func (tx *rewardValidatorTx) SyntacticVerify() error {
|
|
switch {
|
|
case tx == nil:
|
|
return errNilTx
|
|
case tx.TxID.IsZero():
|
|
return errInvalidID
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// SemanticVerify this transaction performs a valid state transition.
|
|
//
|
|
// The current validating set must have at least one member.
|
|
// The next validator to be removed must be the validator specified in this block.
|
|
// The next validator to be removed must be have an end time equal to the current
|
|
// chain timestamp.
|
|
func (tx *rewardValidatorTx) SemanticVerify(db database.Database) (*versiondb.Database, *versiondb.Database, func(), func(), error) {
|
|
if err := tx.SyntacticVerify(); err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
if db == nil {
|
|
return nil, nil, nil, nil, errDbNil
|
|
}
|
|
|
|
currentEvents, err := tx.vm.getCurrentValidators(db, DefaultSubnetID)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, errDBCurrentValidators
|
|
}
|
|
if currentEvents.Len() == 0 { // there is no validator to remove
|
|
return nil, nil, nil, nil, errEmptyValidatingSet
|
|
}
|
|
|
|
vdrTx := currentEvents.Peek()
|
|
|
|
if txID := vdrTx.ID(); !txID.Equals(tx.TxID) {
|
|
return nil, nil, nil, nil, fmt.Errorf("attempting to remove TxID: %s. Should be removing %s",
|
|
tx.TxID,
|
|
txID)
|
|
}
|
|
|
|
// Verify that the chain's timestamp is the validator's end time
|
|
currentTime, err := tx.vm.getTimestamp(db)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
if endTime := vdrTx.EndTime(); !endTime.Equal(currentTime) {
|
|
return nil, nil, nil, nil, fmt.Errorf("attempting to remove TxID: %s before their end time %s",
|
|
tx.TxID,
|
|
endTime)
|
|
}
|
|
|
|
heap.Pop(currentEvents) // Remove validator from the validator set
|
|
|
|
onCommitDB := versiondb.New(db)
|
|
// If this tx's proposal is committed, remove the validator from the validator set and update the
|
|
// account balance to reflect the return of staked $AVA and their reward.
|
|
if err := tx.vm.putCurrentValidators(onCommitDB, currentEvents, DefaultSubnetID); err != nil {
|
|
return nil, nil, nil, nil, errDBPutCurrentValidators
|
|
}
|
|
|
|
onAbortDB := versiondb.New(db)
|
|
// If this tx's proposal is aborted, remove the validator from the validator set and update the
|
|
// account balance to reflect the return of staked $AVA. The validator receives no reward.
|
|
if err := tx.vm.putCurrentValidators(onAbortDB, currentEvents, DefaultSubnetID); err != nil {
|
|
return nil, nil, nil, nil, errDBPutCurrentValidators
|
|
}
|
|
|
|
switch vdrTx := vdrTx.(type) {
|
|
case *addDefaultSubnetValidatorTx:
|
|
duration := vdrTx.Duration()
|
|
amount := vdrTx.Wght
|
|
reward := reward(duration, amount, InflationRate)
|
|
amountWithReward, err := math.Add64(amount, reward)
|
|
if err != nil {
|
|
amountWithReward = amount
|
|
tx.vm.Ctx.Log.Error("error while calculating balance with reward: %s", err)
|
|
}
|
|
|
|
accountID := vdrTx.Destination
|
|
account, err := tx.vm.getAccount(db, accountID) // account receiving staked $AVA (and, if applicable, reward)
|
|
// Error is likely because the staked $AVA is being sent to a new
|
|
// account that isn't in the platform chain's state yet.
|
|
// Create the account
|
|
// TODO: We should have a keyNotFound error to distinguish this case from others
|
|
if err != nil {
|
|
account = newAccount(accountID, 0, 0)
|
|
}
|
|
|
|
accountWithReward := account // The state of the account if the validator earned a validating reward
|
|
accountNoReward := account // The state of the account if the validator didn't earn a validating reward
|
|
if newAccount, err := account.Add(amountWithReward); err == nil {
|
|
accountWithReward = newAccount
|
|
} else {
|
|
tx.vm.Ctx.Log.Error("error while calculating account balance: %v", err)
|
|
}
|
|
if newAccount, err := account.Add(amount); err == nil {
|
|
accountNoReward = newAccount
|
|
} else {
|
|
tx.vm.Ctx.Log.Error("error while calculating account balance: %v", err)
|
|
}
|
|
|
|
if err := tx.vm.putAccount(onCommitDB, accountWithReward); err != nil {
|
|
return nil, nil, nil, nil, errDBPutAccount
|
|
}
|
|
if err := tx.vm.putAccount(onAbortDB, accountNoReward); err != nil {
|
|
return nil, nil, nil, nil, errDBPutAccount
|
|
}
|
|
case *addDefaultSubnetDelegatorTx:
|
|
parentTx, err := currentEvents.getDefaultSubnetStaker(vdrTx.NodeID)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
duration := vdrTx.Duration()
|
|
amount := vdrTx.Wght
|
|
reward := reward(duration, amount, InflationRate)
|
|
|
|
// Because parentTx.Shares <= NumberOfShares this will never underflow
|
|
delegatorShares := NumberOfShares - uint64(parentTx.Shares)
|
|
// Because delegatorShares <= NumberOfShares this will never overflow
|
|
delegatorReward := delegatorShares * (reward / NumberOfShares)
|
|
// Delay rounding as long as possible for small numbers
|
|
if optimisticReward, err := math.Mul64(delegatorShares, reward); err == nil {
|
|
delegatorReward = optimisticReward / NumberOfShares
|
|
}
|
|
|
|
// Because delegatorReward <= reward this will never underflow
|
|
validatorReward := reward - delegatorReward
|
|
|
|
delegatorAmountWithReward, err := math.Add64(amount, delegatorReward)
|
|
if err != nil {
|
|
delegatorAmountWithReward = amount
|
|
tx.vm.Ctx.Log.Error("error while calculating balance with reward: %s", err)
|
|
}
|
|
|
|
delegatorAccountID := vdrTx.Destination
|
|
delegatorAccount, err := tx.vm.getAccount(db, delegatorAccountID) // account receiving staked $AVA (and, if applicable, reward)
|
|
// Error is likely because the staked $AVA is being sent to a new
|
|
// account that isn't in the platform chain's state yet.
|
|
// Create the account
|
|
// TODO: We should have a keyNotFound error to distinguish this case from others
|
|
if err != nil {
|
|
delegatorAccount = newAccount(delegatorAccountID, 0, 0)
|
|
}
|
|
|
|
delegatorAccountWithReward := delegatorAccount // The state of the account if the validator earned a validating reward
|
|
delegatorAccountNoReward := delegatorAccount // The state of the account if the validator didn't earn a validating reward
|
|
if newAccount, err := delegatorAccount.Add(delegatorAmountWithReward); err == nil {
|
|
delegatorAccountWithReward = newAccount
|
|
} else {
|
|
tx.vm.Ctx.Log.Error("error while calculating account balance: %v", err)
|
|
}
|
|
if newAccount, err := delegatorAccount.Add(amount); err == nil {
|
|
delegatorAccountNoReward = newAccount
|
|
} else {
|
|
tx.vm.Ctx.Log.Error("error while calculating account balance: %v", err)
|
|
}
|
|
|
|
if err := tx.vm.putAccount(onCommitDB, delegatorAccountWithReward); err != nil {
|
|
return nil, nil, nil, nil, errDBPutAccount
|
|
}
|
|
if err := tx.vm.putAccount(onAbortDB, delegatorAccountNoReward); err != nil {
|
|
return nil, nil, nil, nil, errDBPutAccount
|
|
}
|
|
|
|
validatorAccountID := parentTx.Destination
|
|
validatorAccount, err := tx.vm.getAccount(onCommitDB, validatorAccountID) // account receiving staked $AVA (and, if applicable, reward)
|
|
// Error is likely because the staked $AVA is being sent to a new
|
|
// account that isn't in the platform chain's state yet.
|
|
// Create the account
|
|
// TODO: We should have a keyNotFound error to distinguish this case from others
|
|
if err != nil {
|
|
validatorAccount = newAccount(validatorAccountID, 0, 0)
|
|
}
|
|
|
|
validatorAccountWithReward := validatorAccount // The state of the account if the validator earned a validating reward
|
|
if newAccount, err := validatorAccount.Add(validatorReward); err == nil {
|
|
validatorAccountWithReward = newAccount
|
|
} else {
|
|
tx.vm.Ctx.Log.Error("error while calculating account balance: %v", err)
|
|
}
|
|
|
|
if err := tx.vm.putAccount(onCommitDB, validatorAccountWithReward); err != nil {
|
|
return nil, nil, nil, nil, errDBPutAccount
|
|
}
|
|
default:
|
|
return nil, nil, nil, nil, errShouldBeDSValidator
|
|
}
|
|
|
|
// Regardless of whether this tx is committed or aborted, update the
|
|
// validator set to remove the staker. onAbortDB or onCommitDB should commit
|
|
// (flush to vm.DB) before this is called
|
|
updateValidators := func() {
|
|
if err := tx.vm.updateValidators(DefaultSubnetID); err != nil {
|
|
tx.vm.Ctx.Log.Fatal("failed to update validators on the default subnet: %s", err)
|
|
}
|
|
}
|
|
|
|
return onCommitDB, onAbortDB, updateValidators, updateValidators, nil
|
|
}
|
|
|
|
// InitiallyPrefersCommit returns true.
|
|
//
|
|
// Right now, *Commit (that is, remove the validator and reward them) is always
|
|
// preferred over *Abort (remove the validator but don't reward them.)
|
|
//
|
|
// TODO: A validator should receive a reward only if they are sufficiently
|
|
// responsive and correct during the time they are validating.
|
|
func (tx *rewardValidatorTx) InitiallyPrefersCommit() bool { return true }
|
|
|
|
// RewardStakerTx creates a new transaction that proposes to remove the staker
|
|
// [validatorID] from the default validator set.
|
|
func (vm *VM) newRewardValidatorTx(txID ids.ID) (*rewardValidatorTx, error) {
|
|
tx := &rewardValidatorTx{
|
|
TxID: txID,
|
|
}
|
|
return tx, tx.initialize(vm)
|
|
}
|