package wiki import ( "context" "fmt" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/woop-chain/woop/block" "github.com/woop-chain/woop/consensus/quorum" "github.com/woop-chain/woop/core/rawdb" "github.com/woop-chain/woop/core/state" "github.com/woop-chain/woop/core/types" "github.com/woop-chain/woop/eth/rpc" "github.com/woop-chain/woop/internal/chain" internalCommon "github.com/woop-chain/woop/internal/common" "github.com/woop-chain/woop/numeric" commonRPC "github.com/woop-chain/woop/rpc/common" "github.com/woop-chain/woop/shard" "github.com/woop-chain/woop/shard/committee" "github.com/woop-chain/woop/staking/availability" "github.com/woop-chain/woop/staking/effective" staking "github.com/woop-chain/woop/staking/types" "github.com/pkg/errors" ) var ( zero = numeric.ZeroDec() bigZero = big.NewInt(0) ) func (wiki *Woop) readAndUpdateRawStakes( epoch *big.Int, decider quorum.Decider, comm shard.Committee, rawStakes []effective.SlotPurchase, validatorSpreads map[common.Address]numeric.Dec, ) []effective.SlotPurchase { for i := range comm.Slots { slot := comm.Slots[i] slotAddr := slot.EcdsaAddress slotKey := slot.BLSPublicKey spread, ok := validatorSpreads[slotAddr] if !ok { snapshot, err := wiki.BlockChain.ReadValidatorSnapshotAtEpoch(epoch, slotAddr) if err != nil { continue } spread = snapshot.RawStakePerSlot() validatorSpreads[slotAddr] = spread } commonRPC.SetRawStake(decider, slotKey, spread) // add entry to array for median calculation rawStakes = append(rawStakes, effective.SlotPurchase{ Addr: slotAddr, Key: slotKey, RawStake: spread, EPoSStake: spread, }) } return rawStakes } func (wiki *Woop) getSuperCommittees() (*quorum.Transition, error) { nowE := wiki.BlockChain.CurrentHeader().Epoch() if wiki.BlockChain.CurrentHeader().IsLastBlockInEpoch() { // current epoch is current header epoch + 1 if the header was last block of prev epoch nowE = new(big.Int).Add(nowE, common.Big1) } thenE := new(big.Int).Sub(nowE, common.Big1) var ( nowCommittee, prevCommittee *shard.State err error ) nowCommittee, err = wiki.BlockChain.ReadShardState(nowE) if err != nil { return nil, err } prevCommittee, err = wiki.BlockChain.ReadShardState(thenE) if err != nil { return nil, err } stakedSlotsNow, stakedSlotsThen := shard.ExternalSlotsAvailableForEpoch(nowE), shard.ExternalSlotsAvailableForEpoch(thenE) then, now := quorum.NewRegistry(stakedSlotsThen, int(thenE.Int64())), quorum.NewRegistry(stakedSlotsNow, int(nowE.Int64())) rawStakes := []effective.SlotPurchase{} validatorSpreads := map[common.Address]numeric.Dec{} for _, comm := range prevCommittee.Shards { decider := quorum.NewDecider(quorum.SuperMajorityStake, comm.ShardID) // before staking skip computing if wiki.BlockChain.Config().IsStaking(prevCommittee.Epoch) { if _, err := decider.SetVoters(&comm, prevCommittee.Epoch); err != nil { return nil, err } } rawStakes = wiki.readAndUpdateRawStakes(thenE, decider, comm, rawStakes, validatorSpreads) then.Deciders[fmt.Sprintf("shard-%d", comm.ShardID)] = decider } then.MedianStake = effective.Median(rawStakes) rawStakes = []effective.SlotPurchase{} validatorSpreads = map[common.Address]numeric.Dec{} for _, comm := range nowCommittee.Shards { decider := quorum.NewDecider(quorum.SuperMajorityStake, comm.ShardID) if _, err := decider.SetVoters(&comm, nowCommittee.Epoch); err != nil { return nil, errors.Wrapf( err, "committee is only available from staking epoch: %v, current epoch: %v", wiki.BlockChain.Config().StakingEpoch, wiki.BlockChain.CurrentHeader().Epoch(), ) } rawStakes = wiki.readAndUpdateRawStakes(nowE, decider, comm, rawStakes, validatorSpreads) now.Deciders[fmt.Sprintf("shard-%d", comm.ShardID)] = decider } now.MedianStake = effective.Median(rawStakes) return &quorum.Transition{Previous: then, Current: now}, nil } // IsStakingEpoch ... func (wiki *Woop) IsStakingEpoch(epoch *big.Int) bool { return wiki.BlockChain.Config().IsStaking(epoch) } // IsPreStakingEpoch ... func (wiki *Woop) IsPreStakingEpoch(epoch *big.Int) bool { return wiki.BlockChain.Config().IsPreStaking(epoch) } // IsNoEarlyUnlockEpoch ... func (wiki *Woop) IsNoEarlyUnlockEpoch(epoch *big.Int) bool { return wiki.BlockChain.Config().IsNoEarlyUnlock(epoch) } // IsMaxRate ... func (wiki *Woop) IsMaxRate(epoch *big.Int) bool { return wiki.BlockChain.Config().IsMaxRate(epoch) } // IsCommitteeSelectionBlock checks if the given block is the committee selection block func (wiki *Woop) IsCommitteeSelectionBlock(header *block.Header) bool { return chain.IsCommitteeSelectionBlock(wiki.BlockChain, header) } // GetDelegationLockingPeriodInEpoch ... func (wiki *Woop) GetDelegationLockingPeriodInEpoch(epoch *big.Int) int { return chain.GetLockPeriodInEpoch(wiki.BlockChain, epoch) } // SendStakingTx adds a staking transaction func (wiki *Woop) SendStakingTx(ctx context.Context, signedStakingTx *staking.StakingTransaction) error { stx, _, _, _ := rawdb.ReadStakingTransaction(wiki.chainDb, signedStakingTx.Hash()) if stx == nil { return wiki.NodeAPI.AddPendingStakingTransaction(signedStakingTx) } return ErrFinalizedTransaction } // GetStakingTransactionsHistory returns list of staking transactions hashes of address. func (wiki *Woop) GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) { return wiki.NodeAPI.GetStakingTransactionsHistory(address, txType, order) } // GetStakingTransactionsCount returns the number of staking transactions of address. func (wiki *Woop) GetStakingTransactionsCount(address, txType string) (uint64, error) { return wiki.NodeAPI.GetStakingTransactionsCount(address, txType) } // GetSuperCommittees .. func (wiki *Woop) GetSuperCommittees() (*quorum.Transition, error) { nowE := wiki.BlockChain.CurrentHeader().Epoch() key := fmt.Sprintf("sc-%s", nowE.String()) res, err := wiki.SingleFlightRequest( key, func() (interface{}, error) { thenE := new(big.Int).Sub(nowE, common.Big1) thenKey := fmt.Sprintf("sc-%s", thenE.String()) wiki.group.Forget(thenKey) return wiki.getSuperCommittees() }) if err != nil { return nil, err } return res.(*quorum.Transition), err } // GetValidators returns validators for a particular epoch. func (wiki *Woop) GetValidators(epoch *big.Int) (*shard.Committee, error) { state, err := wiki.BlockChain.ReadShardState(epoch) if err != nil { return nil, err } for _, cmt := range state.Shards { if cmt.ShardID == wiki.ShardID { return &cmt, nil } } return nil, nil } // GetValidatorSelfDelegation returns the amount of staking after applying all delegated stakes func (wiki *Woop) GetValidatorSelfDelegation(addr common.Address) *big.Int { wrapper, err := wiki.BlockChain.ReadValidatorInformation(addr) if err != nil || wrapper == nil { return nil } if len(wrapper.Delegations) == 0 { return nil } return wrapper.Delegations[0].Amount } // GetElectedValidatorAddresses returns the address of elected validators for current epoch func (wiki *Woop) GetElectedValidatorAddresses() []common.Address { list, _ := wiki.BlockChain.ReadShardState(wiki.BlockChain.CurrentBlock().Epoch()) return list.StakedValidators().Addrs } // GetAllValidatorAddresses returns the up to date validator candidates for next epoch func (wiki *Woop) GetAllValidatorAddresses() []common.Address { return wiki.BlockChain.ValidatorCandidates() } func (wiki *Woop) GetValidatorsStakeByBlockNumber( block *types.Block, ) (map[string]*big.Int, error) { if cachedReward, ok := wiki.stakeByBlockNumberCache.Get(block.Hash()); ok { return cachedReward.(map[string]*big.Int), nil } validatorAddresses := wiki.GetAllValidatorAddresses() stakes := make(map[string]*big.Int, len(validatorAddresses)) for _, validatorAddress := range validatorAddresses { wrapper, err := wiki.BlockChain.ReadValidatorInformationAtRoot(validatorAddress, block.Root()) if err != nil { if errors.Cause(err) != state.ErrAddressNotPresent { return nil, errors.Errorf( "cannot fetch information for validator %s at block %d due to %s", validatorAddress.Hex(), block.Number(), err, ) } else { // `validatorAddress` was not a validator back then continue } } stakes[validatorAddress.Hex()] = wrapper.TotalDelegation() } wiki.stakeByBlockNumberCache.Add(block.Hash(), stakes) return stakes, nil } var ( epochBlocksMap = map[common.Address]map[uint64]staking.EpochSigningEntry{} mapLock = sync.Mutex{} ) func (wiki *Woop) getEpochSigning(epoch *big.Int, addr common.Address) (staking.EpochSigningEntry, error) { entry := staking.EpochSigningEntry{} mapLock.Lock() defer mapLock.Unlock() if validatorMap, ok := epochBlocksMap[addr]; ok { if val, ok := validatorMap[epoch.Uint64()]; ok { return val, nil } } snapshot, err := wiki.BlockChain.ReadValidatorSnapshotAtEpoch(epoch, addr) if err != nil { return entry, err } // the signing information is for the previous epoch prevEpoch := big.NewInt(0).Sub(epoch, common.Big1) entry.Epoch = prevEpoch entry.Blocks = snapshot.Validator.Counters // subtract previous epoch counters if exists prevEpochSnap, err := wiki.BlockChain.ReadValidatorSnapshotAtEpoch(prevEpoch, addr) if err == nil { entry.Blocks.NumBlocksSigned.Sub( entry.Blocks.NumBlocksSigned, prevEpochSnap.Validator.Counters.NumBlocksSigned, ) entry.Blocks.NumBlocksToSign.Sub( entry.Blocks.NumBlocksToSign, prevEpochSnap.Validator.Counters.NumBlocksToSign, ) } // update map when adding new entry, also remove an entry beyond last 30 if _, ok := epochBlocksMap[addr]; !ok { epochBlocksMap[addr] = map[uint64]staking.EpochSigningEntry{} } epochBlocksMap[addr][epoch.Uint64()] = entry epochMinus30 := big.NewInt(0).Sub(epoch, big.NewInt(staking.SigningHistoryLength)) delete(epochBlocksMap[addr], epochMinus30.Uint64()) return entry, nil } // GetValidatorInformation returns the information of validator func (wiki *Woop) GetValidatorInformation( addr common.Address, block *types.Block, ) (*staking.ValidatorRPCEnhanced, error) { bc := wiki.BlockChain wrapper, err := bc.ReadValidatorInformationAtRoot(addr, block.Root()) if err != nil { s, _ := internalCommon.AddressToBech32(addr) return nil, errors.Wrapf(err, "not found address in current state %s", s) } now := block.Epoch() // At the last block of epoch, block epoch is e while val.LastEpochInCommittee // is already updated to e+1. So need the >= check rather than == inCommittee := wrapper.LastEpochInCommittee.Cmp(now) >= 0 defaultReply := &staking.ValidatorRPCEnhanced{ CurrentlyInCommittee: inCommittee, Wrapper: *wrapper, Performance: nil, ComputedMetrics: nil, TotalDelegated: wrapper.TotalDelegation(), EPoSStatus: effective.ValidatorStatus( inCommittee, wrapper.Status, ).String(), EPoSWinningStake: nil, BootedStatus: nil, ActiveStatus: wrapper.Validator.Status.String(), Lifetime: &staking.AccumulatedOverLifetime{ BlockReward: wrapper.BlockReward, Signing: wrapper.Counters, APR: zero, }, } snapshot, err := bc.ReadValidatorSnapshotAtEpoch( now, addr, ) if err != nil { return defaultReply, nil } computed := availability.ComputeCurrentSigning( snapshot.Validator, wrapper, ) lastBlockOfEpoch := shard.Schedule.EpochLastBlock(wiki.BeaconChain.CurrentBlock().Header().Epoch().Uint64()) computed.BlocksLeftInEpoch = lastBlockOfEpoch - wiki.BeaconChain.CurrentBlock().Header().Number().Uint64() if defaultReply.CurrentlyInCommittee { defaultReply.Performance = &staking.CurrentEpochPerformance{ CurrentSigningPercentage: *computed, Epoch: wiki.BeaconChain.CurrentBlock().Header().Epoch().Uint64(), Block: wiki.BeaconChain.CurrentBlock().Header().Number().Uint64(), } } stats, err := bc.ReadValidatorStats(addr) if err != nil { // when validator has no stats, default boot-status to not booted notBooted := effective.NotBooted.String() defaultReply.BootedStatus = ¬Booted return defaultReply, nil } latestAPR := numeric.ZeroDec() l := len(stats.APRs) if l > 0 { latestAPR = stats.APRs[l-1].Value } defaultReply.Lifetime.APR = latestAPR defaultReply.Lifetime.EpochAPRs = stats.APRs // average apr cache keys // key := fmt.Sprintf("apr-%s-%d", addr.Hex(), now.Uint64()) // prevKey := fmt.Sprintf("apr-%s-%d", addr.Hex(), now.Uint64()-1) // delete entry for previous epoch // b.apiCache.Forget(prevKey) // calculate last APRHistoryLength epochs for averaging APR // epochFrom := bc.GasPriceConfig().StakingEpoch // nowMinus := big.NewInt(0).Sub(now, big.NewInt(staking.APRHistoryLength)) // if nowMinus.Cmp(epochFrom) > 0 { // epochFrom = nowMinus // } // if len(stats.APRs) > 0 && stats.APRs[0].Epoch.Cmp(epochFrom) > 0 { // epochFrom = stats.APRs[0].Epoch // } // epochToAPRs := map[int64]numeric.Dec{} // for i := 0; i < len(stats.APRs); i++ { // entry := stats.APRs[i] // epochToAPRs[entry.Epoch.Int64()] = entry.Value // } // at this point, validator is active and has apr's for the recent 100 epochs // compute average apr over history // if avgAPR, err := b.SingleFlightRequest( // key, func() (interface{}, error) { // total := numeric.ZeroDec() // count := 0 // for i := epochFrom.Int64(); i < now.Int64(); i++ { // if apr, ok := epochToAPRs[i]; ok { // total = total.Add(apr) // } // count++ // } // if count == 0 { // return nil, errors.New("no apr snapshots available") // } // return total.QuoInt64(int64(count)), nil // }, // ); err != nil { // // could not compute average apr from snapshot // // assign the latest apr available from stats // defaultReply.Lifetime.APR = numeric.ZeroDec() // } else { // defaultReply.Lifetime.APR = avgAPR.(numeric.Dec) // } epochBlocks := []staking.EpochSigningEntry{} epochFrom := bc.Config().StakingEpoch nowMinus := big.NewInt(0).Sub(now, big.NewInt(staking.SigningHistoryLength)) if nowMinus.Cmp(epochFrom) > 0 { epochFrom = nowMinus } for i := now.Int64(); i > epochFrom.Int64(); i-- { epoch := big.NewInt(i) entry, err := wiki.getEpochSigning(epoch, addr) if err != nil { break } epochBlocks = append(epochBlocks, entry) } defaultReply.Lifetime.EpochBlocks = epochBlocks if defaultReply.CurrentlyInCommittee { defaultReply.ComputedMetrics = stats defaultReply.EPoSWinningStake = &stats.TotalEffectiveStake } if !defaultReply.CurrentlyInCommittee { reason := stats.BootedStatus.String() defaultReply.BootedStatus = &reason } return defaultReply, nil } // GetMedianRawStakeSnapshot .. func (wiki *Woop) GetMedianRawStakeSnapshot() ( *committee.CompletedEPoSRound, error, ) { blockNum := wiki.CurrentBlock().NumberU64() key := fmt.Sprintf("median-%d", blockNum) // delete cache for previous block prevKey := fmt.Sprintf("median-%d", blockNum-1) wiki.group.Forget(prevKey) res, err := wiki.SingleFlightRequest( key, func() (interface{}, error) { // Compute for next epoch epoch := big.NewInt(0).Add(wiki.CurrentBlock().Epoch(), big.NewInt(1)) instance := shard.Schedule.InstanceForEpoch(epoch) return committee.NewEPoSRound(epoch, wiki.BlockChain, wiki.BlockChain.Config().IsEPoSBound35(epoch), instance.SlotsLimit(), int(instance.NumShards())) }, ) if err != nil { return nil, err } return res.(*committee.CompletedEPoSRound), nil } // GetDelegationsByValidator returns all delegation information of a validator func (wiki *Woop) GetDelegationsByValidator(validator common.Address) []staking.Delegation { wrapper, err := wiki.BlockChain.ReadValidatorInformation(validator) if err != nil || wrapper == nil { return nil } return wrapper.Delegations } // GetDelegationsByValidatorAtBlock returns all delegation information of a validator at the given block func (wiki *Woop) GetDelegationsByValidatorAtBlock( validator common.Address, block *types.Block, ) []staking.Delegation { wrapper, err := wiki.BlockChain.ReadValidatorInformationAtRoot(validator, block.Root()) if err != nil || wrapper == nil { return nil } return wrapper.Delegations } // GetDelegationsByDelegator returns all delegation information of a delegator func (wiki *Woop) GetDelegationsByDelegator( delegator common.Address, ) ([]common.Address, []*staking.Delegation) { block := wiki.BlockChain.CurrentBlock() return wiki.GetDelegationsByDelegatorByBlock(delegator, block) } // GetDelegationsByDelegatorByBlock returns all delegation information of a delegator func (wiki *Woop) GetDelegationsByDelegatorByBlock( delegator common.Address, block *types.Block, ) ([]common.Address, []*staking.Delegation) { delegationIndexes, err := wiki.BlockChain. ReadDelegationsByDelegatorAt(delegator, block.Number()) if err != nil { return nil, nil } addresses := make([]common.Address, 0, len(delegationIndexes)) delegations := make([]*staking.Delegation, 0, len(delegationIndexes)) for i := range delegationIndexes { wrapper, err := wiki.BlockChain.ReadValidatorInformationAtRoot( delegationIndexes[i].ValidatorAddress, block.Root(), ) if err != nil || wrapper == nil { return nil, nil } if uint64(len(wrapper.Delegations)) > delegationIndexes[i].Index { delegations = append(delegations, &wrapper.Delegations[delegationIndexes[i].Index]) } else { delegations = append(delegations, nil) } addresses = append(addresses, delegationIndexes[i].ValidatorAddress) } return addresses, delegations } // UndelegationPayouts .. type UndelegationPayouts struct { Data map[common.Address]map[common.Address]*big.Int } func NewUndelegationPayouts() *UndelegationPayouts { return &UndelegationPayouts{ Data: make(map[common.Address]map[common.Address]*big.Int), } } func (u *UndelegationPayouts) SetPayoutByDelegatorAddrAndValidatorAddr( delegator, validator common.Address, amount *big.Int, ) { if u.Data[delegator] == nil { u.Data[delegator] = make(map[common.Address]*big.Int) } if totalPayout, ok := u.Data[delegator][validator]; ok { u.Data[delegator][validator] = new(big.Int).Add(totalPayout, amount) } else { u.Data[delegator][validator] = amount } } // GetUndelegationPayouts returns the undelegation payouts for each delegator // // Due to in-memory caching, it is possible to get undelegation payouts for a state / epoch // that has been pruned but have it be lost (and unable to recompute) after the node restarts. // This not a problem if a full (archival) DB is used. func (wiki *Woop) GetUndelegationPayouts( ctx context.Context, epoch *big.Int, ) (*UndelegationPayouts, error) { if !wiki.IsPreStakingEpoch(epoch) { return nil, fmt.Errorf("not pre-staking epoch or later") } payouts, ok := wiki.undelegationPayoutsCache.Get(epoch.Uint64()) if ok { return payouts.(*UndelegationPayouts), nil } undelegationPayouts := NewUndelegationPayouts() // require second to last block as saved undelegations are AFTER undelegations are payed out blockNumber := shard.Schedule.EpochLastBlock(epoch.Uint64()) - 1 undelegationPayoutBlock, err := wiki.BlockByNumber(ctx, rpc.BlockNumber(blockNumber)) if err != nil || undelegationPayoutBlock == nil { // Block not found, so no undelegationPayouts (not an error) return undelegationPayouts, nil } isMaxRate := wiki.IsMaxRate(epoch) lockingPeriod := wiki.GetDelegationLockingPeriodInEpoch(undelegationPayoutBlock.Epoch()) for _, validator := range wiki.GetAllValidatorAddresses() { wrapper, err := wiki.BlockChain.ReadValidatorInformationAtRoot(validator, undelegationPayoutBlock.Root()) if err != nil || wrapper == nil { continue // Not a validator at this epoch or unable to fetch validator info because of pruned state. } noEarlyUnlock := wiki.IsNoEarlyUnlockEpoch(epoch) for _, delegation := range wrapper.Delegations { withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod, noEarlyUnlock, isMaxRate) if withdraw.Cmp(bigZero) == 1 { undelegationPayouts.SetPayoutByDelegatorAddrAndValidatorAddr(validator, delegation.DelegatorAddress, withdraw) } } } wiki.undelegationPayoutsCache.Add(epoch.Uint64(), undelegationPayouts) return undelegationPayouts, nil } // GetTotalStakingSnapshot .. func (wiki *Woop) GetTotalStakingSnapshot() *big.Int { if stake := wiki.totalStakeCache.pop(wiki.CurrentBlock().NumberU64()); stake != nil { return stake } currHeight := wiki.CurrentBlock().NumberU64() candidates := wiki.BlockChain.ValidatorCandidates() if len(candidates) == 0 { stake := big.NewInt(0) wiki.totalStakeCache.push(currHeight, stake) return stake } stakes := big.NewInt(0) for i := range candidates { snapshot, _ := wiki.BlockChain.ReadValidatorSnapshot(candidates[i]) validator, _ := wiki.BlockChain.ReadValidatorInformation(candidates[i]) if !committee.IsEligibleForEPoSAuction( snapshot, validator, ) { continue } for i := range validator.Delegations { stakes.Add(stakes, validator.Delegations[i].Amount) } } wiki.totalStakeCache.push(currHeight, stakes) return stakes } // GetCurrentStakingErrorSink .. func (wiki *Woop) GetCurrentStakingErrorSink() types.TransactionErrorReports { return wiki.NodeAPI.ReportStakingErrorSink() } // totalStakeCache .. type totalStakeCache struct { sync.Mutex cachedBlockHeight uint64 stake *big.Int // duration is in blocks duration uint64 } // newTotalStakeCache .. func newTotalStakeCache(duration uint64) *totalStakeCache { return &totalStakeCache{ cachedBlockHeight: 0, stake: nil, duration: duration, } } func (c *totalStakeCache) push(currBlockHeight uint64, stake *big.Int) { c.Lock() defer c.Unlock() c.cachedBlockHeight = currBlockHeight c.stake = stake } func (c *totalStakeCache) pop(currBlockHeight uint64) *big.Int { if currBlockHeight > c.cachedBlockHeight+c.duration { return nil } return c.stake }