Fantom-foundation/go-lachesis

View on GitHub
gossip/sfc_index.go

Summary

Maintainability
F
4 days
Test Coverage
package gossip

import (
    "fmt"
    "math/big"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/math"
    "github.com/ethereum/go-ethereum/core/state"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/log"

    "github.com/Fantom-foundation/go-lachesis/app"
    "github.com/Fantom-foundation/go-lachesis/inter"
    "github.com/Fantom-foundation/go-lachesis/inter/idx"
    "github.com/Fantom-foundation/go-lachesis/inter/sfctype"
    "github.com/Fantom-foundation/go-lachesis/lachesis"
    "github.com/Fantom-foundation/go-lachesis/lachesis/genesis/sfc"
    "github.com/Fantom-foundation/go-lachesis/lachesis/genesis/sfc/sfcpos"
    "github.com/Fantom-foundation/go-lachesis/utils"
    "github.com/Fantom-foundation/go-lachesis/utils/errlock"
    "github.com/Fantom-foundation/go-lachesis/version"
)

// GetActiveSfcStakers returns stakers which will become validators in next epoch
func (s *Service) GetActiveSfcStakers() []sfctype.SfcStakerAndID {
    stakers := make([]sfctype.SfcStakerAndID, 0, 200)
    s.app.ForEachSfcStaker(func(it sfctype.SfcStakerAndID) {
        if it.Staker.Ok() {
            stakers = append(stakers, it)
        }
    })
    return stakers
}

func (s *Service) delAllStakerData(stakerID idx.StakerID) {
    s.app.DelSfcStaker(stakerID)
    s.app.ResetBlocksMissed(stakerID)
    s.app.DelActiveValidationScore(stakerID)
    s.app.DelDirtyValidationScore(stakerID)
    s.app.DelActiveOriginationScore(stakerID)
    s.app.DelDirtyOriginationScore(stakerID)
    s.app.DelWeightedDelegationsFee(stakerID)
    s.app.DelStakerPOI(stakerID)
    s.app.DelStakerClaimedRewards(stakerID)
    s.app.DelStakerDelegationsClaimedRewards(stakerID)
}

func (s *Service) delAllDelegationData(id sfctype.DelegationID) {
    s.app.DelSfcDelegation(id)
    s.app.DelDelegationClaimedRewards(id)
}

var (
    max128 = new(big.Int).Sub(math.BigPow(2, 128), common.Big1)
)

func (s *Service) calcRewardWeights(stakers []sfctype.SfcStakerAndID, _epochDuration inter.Timestamp) (baseRewardWeights []*big.Int, txRewardWeights []*big.Int) {
    validationScores := make([]*big.Int, 0, len(stakers))
    originationScores := make([]*big.Int, 0, len(stakers))
    pois := make([]*big.Int, 0, len(stakers))
    stakes := make([]*big.Int, 0, len(stakers))

    if _epochDuration == 0 {
        _epochDuration = 1
    }
    epochDuration := new(big.Int).SetUint64(uint64(_epochDuration))

    for _, it := range stakers {
        stake := it.Staker.CalcTotalStake()
        poi := s.app.GetStakerPOI(it.StakerID)
        validationScore := s.app.GetActiveValidationScore(it.StakerID)
        originationScore := s.app.GetActiveOriginationScore(it.StakerID)

        stakes = append(stakes, stake)
        validationScores = append(validationScores, validationScore)
        originationScores = append(originationScores, originationScore)
        pois = append(pois, poi)
    }

    txRewardWeights = make([]*big.Int, 0, len(stakers))
    for i := range stakers {
        // txRewardWeight = ({origination score} + {CONST} * {PoI}) * {validation score}
        // origination score is roughly proportional to {validation score} * {stake}, so the whole formula is roughly
        // {stake} * {validation score} ^ 2
        poiWithRatio := new(big.Int).Mul(pois[i], s.config.Net.Economy.TxRewardPoiImpact)
        poiWithRatio.Div(poiWithRatio, lachesis.PercentUnit)

        txRewardWeight := new(big.Int).Add(originationScores[i], poiWithRatio)
        txRewardWeight.Mul(txRewardWeight, validationScores[i])
        txRewardWeight.Div(txRewardWeight, epochDuration)
        if txRewardWeight.Cmp(max128) > 0 {
            txRewardWeight = new(big.Int).Set(max128) // never going to get here
        }

        txRewardWeights = append(txRewardWeights, txRewardWeight)
    }

    baseRewardWeights = make([]*big.Int, 0, len(stakers))
    for i := range stakers {
        // baseRewardWeight = {stake} * {validationScore ^ 2}
        baseRewardWeight := new(big.Int).Set(stakes[i])
        for pow := 0; pow < 2; pow++ {
            baseRewardWeight.Mul(baseRewardWeight, validationScores[i])
            baseRewardWeight.Div(baseRewardWeight, epochDuration)
        }
        if baseRewardWeight.Cmp(max128) > 0 {
            baseRewardWeight = new(big.Int).Set(max128) // never going to get here
        }

        baseRewardWeights = append(baseRewardWeights, baseRewardWeight)
    }

    return baseRewardWeights, txRewardWeights
}

// getRewardPerSec returns current rewardPerSec, depending on config and value provided by SFC
func (s *Service) getRewardPerSec() *big.Int {
    rewardPerSecond := s.app.GetSfcConstants(s.engine.GetEpoch() - 1).BaseRewardPerSec
    if rewardPerSecond == nil || rewardPerSecond.Sign() == 0 {
        rewardPerSecond = s.config.Net.Economy.InitialRewardPerSecond
    }
    if rewardPerSecond.Cmp(s.config.Net.Economy.MaxRewardPerSecond) > 0 {
        rewardPerSecond = s.config.Net.Economy.MaxRewardPerSecond
    }
    return new(big.Int).Set(rewardPerSecond)
}

// getOfflinePenaltyThreshold returns current offlinePenaltyThreshold, depending on config and value provided by SFC
func (s *Service) getOfflinePenaltyThreshold() app.BlocksMissed {
    v := s.app.GetSfcConstants(s.engine.GetEpoch() - 1).OfflinePenaltyThreshold
    if v.Num == 0 {
        v.Num = s.config.Net.Economy.InitialOfflinePenaltyThreshold.BlocksNum
    }
    if v.Period == 0 {
        v.Period = inter.Timestamp(s.config.Net.Economy.InitialOfflinePenaltyThreshold.Period)
    }
    return v
}

// processSfc applies the new SFC state
func (s *Service) processSfc(block *inter.Block, receipts types.Receipts, blockFee *big.Int, sealEpoch bool, cheaters inter.Cheaters, statedb *state.StateDB) {
    // s.engineMu is locked here

    // process SFC contract logs
    epoch := s.engine.GetEpoch()
    for _, receipt := range receipts {
        for _, l := range receipt.Logs {
            if l.Address != sfc.ContractAddress {
                continue
            }
            // Add new stakers
            if l.Topics[0] == sfcpos.Topics.CreatedStake && len(l.Topics) > 2 && len(l.Data) >= 32 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())
                address := common.BytesToAddress(l.Topics[2][12:])
                amount := new(big.Int).SetBytes(l.Data[0:32])

                s.app.SetSfcStaker(stakerID, &sfctype.SfcStaker{
                    Address:      address,
                    CreatedEpoch: epoch,
                    CreatedTime:  block.Time,
                    StakeAmount:  amount,
                    DelegatedMe:  big.NewInt(0),
                })
            }

            // Increase stakes
            if l.Topics[0] == sfcpos.Topics.IncreasedStake && len(l.Topics) > 1 && len(l.Data) >= 32 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())
                newAmount := new(big.Int).SetBytes(l.Data[0:32])

                staker := s.app.GetSfcStaker(stakerID)
                if staker == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                staker.StakeAmount = newAmount
                s.app.SetSfcStaker(stakerID, staker)
            }

            // Add new delegations
            if l.Topics[0] == sfcpos.Topics.CreatedDelegation && len(l.Topics) > 1 && len(l.Data) >= 32 {
                address := common.BytesToAddress(l.Topics[1][12:])
                toStakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[2][:]).Uint64())
                amount := new(big.Int).SetBytes(l.Data[0:32])

                staker := s.app.GetSfcStaker(toStakerID)
                if staker == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                staker.DelegatedMe.Add(staker.DelegatedMe, amount)

                s.app.SetSfcDelegation(sfctype.DelegationID{address, toStakerID}, &sfctype.SfcDelegation{
                    CreatedEpoch: epoch,
                    CreatedTime:  block.Time,
                    Amount:       amount,
                })
                s.app.SetSfcStaker(toStakerID, staker)
            }

            // Deactivate stakes
            if (l.Topics[0] == sfcpos.Topics.DeactivatedStake || l.Topics[0] == sfcpos.Topics.PreparedToWithdrawStake) && len(l.Topics) > 1 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())

                staker := s.app.GetSfcStaker(stakerID)
                if staker == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                staker.DeactivatedEpoch = epoch
                staker.DeactivatedTime = block.Time
                s.app.SetSfcStaker(stakerID, staker)
            }

            // Deactivate delegations
            if (l.Topics[0] == sfcpos.Topics.DeactivatedDelegation || l.Topics[0] == sfcpos.Topics.PreparedToWithdrawDelegation) && len(l.Topics) > 2 {
                address := common.BytesToAddress(l.Topics[1][12:])
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[2][:]).Uint64())
                id := sfctype.DelegationID{address, stakerID}

                delegation := s.app.GetSfcDelegation(id)
                if delegation == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                staker := s.app.GetSfcStaker(stakerID)
                if staker != nil {
                    staker.DelegatedMe.Sub(staker.DelegatedMe, delegation.Amount)
                    if staker.DelegatedMe.Sign() < 0 {
                        staker.DelegatedMe = big.NewInt(0)
                    }
                    s.app.SetSfcStaker(stakerID, staker)
                }
                delegation.DeactivatedEpoch = epoch
                delegation.DeactivatedTime = block.Time
                s.app.SetSfcDelegation(id, delegation)
            }

            // Update stake
            if l.Topics[0] == sfcpos.Topics.UpdatedStake && len(l.Topics) > 1 && len(l.Data) >= 64 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())
                newAmount := new(big.Int).SetBytes(l.Data[0:32])
                newDelegatedMe := new(big.Int).SetBytes(l.Data[32:64])

                staker := s.app.GetSfcStaker(stakerID)
                if staker == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                staker.StakeAmount = newAmount
                staker.DelegatedMe = newDelegatedMe
                s.app.SetSfcStaker(stakerID, staker)
            }

            // Update delegation
            if l.Topics[0] == sfcpos.Topics.UpdatedDelegation && len(l.Topics) > 3 && len(l.Data) >= 32 {
                address := common.BytesToAddress(l.Topics[1][12:])
                oldStakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[3][:]).Uint64())
                newStakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[3][:]).Uint64())
                newAmount := new(big.Int).SetBytes(l.Data[0:32])
                oldId := sfctype.DelegationID{address, oldStakerID}
                newId := sfctype.DelegationID{address, newStakerID}

                delegation := s.app.GetSfcDelegation(oldId)
                if delegation == nil {
                    s.Log.Warn("Internal SFC index isn't synced with SFC contract")
                    continue
                }
                delegation.Amount = newAmount
                s.app.DelSfcDelegation(oldId)
                s.app.SetSfcDelegation(newId, delegation)
            }

            // Delete stakes
            if l.Topics[0] == sfcpos.Topics.WithdrawnStake && len(l.Topics) > 1 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())
                s.delAllStakerData(stakerID)
            }

            // Delete delegations
            if l.Topics[0] == sfcpos.Topics.WithdrawnDelegation && len(l.Topics) > 2 {
                address := common.BytesToAddress(l.Topics[1][12:])
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[2][:]).Uint64())
                s.delAllDelegationData(sfctype.DelegationID{address, stakerID})
            }

            // Track changes of constants by SFC
            if l.Topics[0] == sfcpos.Topics.UpdatedBaseRewardPerSec && len(l.Data) >= 32 {
                baseRewardPerSec := new(big.Int).SetBytes(l.Data[0:32])
                constants := s.app.GetSfcConstants(epoch)
                constants.BaseRewardPerSec = baseRewardPerSec
                s.app.SetSfcConstants(epoch, constants)
            }
            if l.Topics[0] == sfcpos.Topics.UpdatedGasPowerAllocationRate && len(l.Data) >= 64 {
                shortAllocationRate := new(big.Int).SetBytes(l.Data[0:32])
                longAllocationRate := new(big.Int).SetBytes(l.Data[32:64])
                constants := s.app.GetSfcConstants(epoch)
                constants.ShortGasPowerAllocPerSec = shortAllocationRate.Uint64()
                constants.LongGasPowerAllocPerSec = longAllocationRate.Uint64()
                s.app.SetSfcConstants(epoch, constants)
            }
            if l.Topics[0] == sfcpos.Topics.UpdatedOfflinePenaltyThreshold && len(l.Data) >= 64 {
                blocksNum := new(big.Int).SetBytes(l.Data[0:32])
                period := new(big.Int).SetBytes(l.Data[32:64])
                constants := s.app.GetSfcConstants(epoch)
                constants.OfflinePenaltyThreshold.Num = idx.Block(blocksNum.Uint64())
                constants.OfflinePenaltyThreshold.Period = inter.Timestamp(period.Uint64())
                s.app.SetSfcConstants(epoch, constants)
            }
            if l.Topics[0] == sfcpos.Topics.UpdatedMinGasPrice && len(l.Data) >= 32 {
                minGasPrice := new(big.Int).SetBytes(l.Data[0:32])
                constants := s.app.GetSfcConstants(epoch)
                constants.MinGasPrice = minGasPrice
                s.app.SetSfcConstants(epoch, constants)
            }

            if l.Topics[0] == sfcpos.Topics.NetworkUpgradeActivated && len(l.Data) >= 32 {
                netVersion := new(big.Int).SetBytes(l.Data[0:32])
                s.store.SetNetworkVersion(netVersion)
                if netVersion.Cmp(version.AsBigInt()) > 0 {
                    if s.config.Upgrade.ShutDownIfNotUpgraded {
                        errlock.Permanent(fmt.Errorf("The network's supported version of %s was activated at block %d.\n"+
                            "Node's current version is %s and a shutdown is required as ShutDownIfNotUpgraded flag was set.\n"+
                            "Please upgrade the node to continue.", version.BigToString(netVersion), block.Index, version.AsString()))
                        panic("unreachable")
                    } else {
                        s.store.SetNonSupportedUpgrade(netVersion)
                    }
                }
            }

            if l.Topics[0] == sfcpos.Topics.NetworkMigration && len(l.Data) >= 32 {
                netVersion := new(big.Int).SetBytes(l.Data[0:32])
                log.Info("Network migration is initiated", "version", version.BigToString(netVersion))
                s.migration = true
            }

            // Track rewards (API-only)
            if l.Topics[0] == sfcpos.Topics.ClaimedValidatorReward && len(l.Topics) > 1 && len(l.Data) >= 32 {
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[1][:]).Uint64())
                reward := new(big.Int).SetBytes(l.Data[0:32])

                s.app.IncStakerClaimedRewards(stakerID, reward)
            }
            if l.Topics[0] == sfcpos.Topics.ClaimedDelegationReward && len(l.Topics) > 2 && len(l.Data) >= 32 {
                address := common.BytesToAddress(l.Topics[1][12:])
                stakerID := idx.StakerID(new(big.Int).SetBytes(l.Topics[2][:]).Uint64())
                reward := new(big.Int).SetBytes(l.Data[0:32])

                s.app.IncDelegationClaimedRewards(sfctype.DelegationID{address, stakerID}, reward)
                s.app.IncStakerDelegationsClaimedRewards(stakerID, reward)
            }
        }
    }

    // Update EpochStats
    stats := s.store.GetDirtyEpochStats()
    stats.TotalFee = new(big.Int).Add(stats.TotalFee, blockFee)
    if sealEpoch {
        // dirty EpochStats becomes active
        stats.End = block.Time
        s.store.SetEpochStats(epoch, stats)

        // new dirty EpochStats
        s.store.SetDirtyEpochStats(&sfctype.EpochStats{
            Start:    block.Time,
            TotalFee: new(big.Int),
        })
    } else {
        s.store.SetDirtyEpochStats(stats)
    }

    // Write cheaters
    for _, stakerID := range cheaters {
        staker := s.app.GetSfcStaker(stakerID)
        if staker.HasFork() {
            continue
        }
        // write into DB
        staker.Status |= sfctype.ForkBit
        s.app.SetSfcStaker(stakerID, staker)
        // write into SFC contract
        position := sfcpos.Staker(stakerID)
        statedb.SetState(sfc.ContractAddress, position.Status(), utils.U64to256(staker.Status))
    }

    if sealEpoch {
        if s.app.HasSfcConstants(epoch) {
            s.app.SetSfcConstants(epoch+1, s.app.GetSfcConstants(epoch))
        }

        // Write offline validators
        for _, it := range s.app.GetSfcStakers() {
            if it.Staker.Offline() {
                continue
            }

            gotMissed := s.app.GetBlocksMissed(it.StakerID)
            badMissed := s.getOfflinePenaltyThreshold()
            if gotMissed.Num >= badMissed.Num && gotMissed.Period >= badMissed.Period {
                // write into DB
                it.Staker.Status |= sfctype.OfflineBit
                s.app.SetSfcStaker(it.StakerID, it.Staker)
                // write into SFC contract
                position := sfcpos.Staker(it.StakerID)
                statedb.SetState(sfc.ContractAddress, position.Status(), utils.U64to256(it.Staker.Status))
            }
        }

        // Write epoch snapshot (for reward)
        cheatersSet := cheaters.Set()
        epochPos := sfcpos.EpochSnapshot(epoch)
        epochValidators := s.app.GetEpochValidators(epoch)
        baseRewardWeights, txRewardWeights := s.calcRewardWeights(epochValidators, stats.Duration())

        totalBaseRewardWeight := new(big.Int)
        totalTxRewardWeight := new(big.Int)
        totalStake := new(big.Int)
        totalDelegated := new(big.Int)
        for i, it := range epochValidators {
            baseRewardWeight := baseRewardWeights[i]
            txRewardWeight := txRewardWeights[i]
            totalStake.Add(totalStake, it.Staker.StakeAmount)
            totalDelegated.Add(totalDelegated, it.Staker.DelegatedMe)

            if _, ok := cheatersSet[it.StakerID]; ok {
                continue // don't give reward to cheaters
            }
            if baseRewardWeight.Sign() == 0 && txRewardWeight.Sign() == 0 {
                continue // don't give reward to offline validators
            }

            meritPos := epochPos.ValidatorMerit(it.StakerID)

            statedb.SetState(sfc.ContractAddress, meritPos.StakeAmount(), utils.BigTo256(it.Staker.StakeAmount))
            statedb.SetState(sfc.ContractAddress, meritPos.DelegatedMe(), utils.BigTo256(it.Staker.DelegatedMe))
            statedb.SetState(sfc.ContractAddress, meritPos.BaseRewardWeight(), utils.BigTo256(baseRewardWeight))
            statedb.SetState(sfc.ContractAddress, meritPos.TxRewardWeight(), utils.BigTo256(txRewardWeight))

            totalBaseRewardWeight.Add(totalBaseRewardWeight, baseRewardWeight)
            totalTxRewardWeight.Add(totalTxRewardWeight, txRewardWeight)
        }
        baseRewardPerSec := s.getRewardPerSec()

        // set total supply
        baseRewards := new(big.Int).Mul(big.NewInt(stats.Duration().Unix()), baseRewardPerSec)
        rewards := new(big.Int).Add(baseRewards, stats.TotalFee)
        totalSupply := new(big.Int).Add(s.app.GetTotalSupply(), rewards)
        statedb.SetState(sfc.ContractAddress, sfcpos.CurrentSealedEpoch(), utils.U64to256(uint64(epoch)))
        s.app.SetTotalSupply(totalSupply)

        statedb.SetState(sfc.ContractAddress, epochPos.TotalBaseRewardWeight(), utils.BigTo256(totalBaseRewardWeight))
        statedb.SetState(sfc.ContractAddress, epochPos.TotalTxRewardWeight(), utils.BigTo256(totalTxRewardWeight))
        statedb.SetState(sfc.ContractAddress, epochPos.EpochFee(), utils.BigTo256(stats.TotalFee))
        statedb.SetState(sfc.ContractAddress, epochPos.EndTime(), utils.U64to256(uint64(stats.End.Unix())))
        statedb.SetState(sfc.ContractAddress, epochPos.Duration(), utils.U64to256(uint64(stats.Duration().Unix())))
        statedb.SetState(sfc.ContractAddress, epochPos.BaseRewardPerSecond(), utils.BigTo256(baseRewardPerSec))
        statedb.SetState(sfc.ContractAddress, epochPos.StakeTotalAmount(), utils.BigTo256(totalStake))
        statedb.SetState(sfc.ContractAddress, epochPos.DelegationsTotalAmount(), utils.BigTo256(totalDelegated))
        statedb.SetState(sfc.ContractAddress, epochPos.TotalSupply(), utils.BigTo256(totalSupply))
        statedb.SetState(sfc.ContractAddress, sfcpos.CurrentSealedEpoch(), utils.U64to256(uint64(epoch)))

        // Add balance for SFC to pay rewards
        statedb.AddBalance(sfc.ContractAddress, rewards)

        // Select new validators
        s.app.SetEpochValidators(epoch+1, s.GetActiveSfcStakers())
    }
}