You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
190 lines
4.8 KiB
190 lines
4.8 KiB
package effective
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"math/big"
|
|
"sort"
|
|
|
|
"github.com/harmony-one/harmony/crypto/bls"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
common2 "github.com/harmony-one/harmony/internal/common"
|
|
"github.com/harmony-one/harmony/numeric"
|
|
)
|
|
|
|
// medium.com/harmony-one/introducing-harmonys-effective-proof-of-stake-epos-2d39b4b8d58
|
|
var (
|
|
two = numeric.NewDecFromBigInt(big.NewInt(2))
|
|
c, _ = numeric.NewDecFromStr("0.15")
|
|
cV2, _ = numeric.NewDecFromStr("0.35")
|
|
onePlusC = numeric.OneDec().Add(c)
|
|
oneMinusC = numeric.OneDec().Sub(c)
|
|
onePlusCV2 = numeric.OneDec().Add(cV2)
|
|
oneMinusCV2 = numeric.OneDec().Sub(cV2)
|
|
)
|
|
|
|
// SlotPurchase ..
|
|
type SlotPurchase struct {
|
|
Addr common.Address
|
|
Key bls.SerializedPublicKey
|
|
RawStake numeric.Dec
|
|
EPoSStake numeric.Dec
|
|
}
|
|
|
|
// MarshalJSON ..
|
|
func (p SlotPurchase) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Addr string `json:"slot-owner"`
|
|
Key string `json:"bls-public-key"`
|
|
RawStake numeric.Dec `json:"raw-stake"`
|
|
EPoSStake numeric.Dec `json:"eposed-stake"`
|
|
}{
|
|
common2.MustAddressToBech32(p.Addr),
|
|
p.Key.Hex(),
|
|
p.RawStake,
|
|
p.EPoSStake,
|
|
})
|
|
}
|
|
|
|
// SlotOrder ..
|
|
type SlotOrder struct {
|
|
Stake *big.Int `json:"stake"`
|
|
SpreadAmong []bls.SerializedPublicKey `json:"keys-at-auction"`
|
|
Percentage numeric.Dec `json:"percentage-of-total-auction-stake"`
|
|
}
|
|
|
|
// Median ..
|
|
func Median(stakes []SlotPurchase) numeric.Dec {
|
|
if len(stakes) == 0 {
|
|
return numeric.ZeroDec()
|
|
}
|
|
|
|
sort.SliceStable(
|
|
stakes,
|
|
func(i, j int) bool {
|
|
return stakes[i].RawStake.GT(stakes[j].RawStake)
|
|
},
|
|
)
|
|
const isEven = 0
|
|
switch l := len(stakes); l % 2 {
|
|
case isEven:
|
|
left := (l / 2) - 1
|
|
right := l / 2
|
|
return stakes[left].RawStake.Add(stakes[right].RawStake).Quo(two)
|
|
default:
|
|
return stakes[l/2].RawStake
|
|
}
|
|
}
|
|
|
|
// Compute ..
|
|
func Compute(
|
|
shortHand map[common.Address]*SlotOrder, pull, slotsLimit, shardCount int,
|
|
) (numeric.Dec, []SlotPurchase) {
|
|
if len(shortHand) == 0 {
|
|
return numeric.ZeroDec(), []SlotPurchase{}
|
|
}
|
|
|
|
type t struct {
|
|
addr common.Address
|
|
slot *SlotOrder
|
|
}
|
|
|
|
totalSlots := 0
|
|
shorter := []t{}
|
|
for key, value := range shortHand {
|
|
totalSlots += len(value.SpreadAmong)
|
|
shorter = append(shorter, t{key, value})
|
|
}
|
|
eposedSlots := make([]SlotPurchase, 0, totalSlots)
|
|
|
|
sort.SliceStable(
|
|
shorter,
|
|
func(i, j int) bool {
|
|
return bytes.Compare(
|
|
shorter[i].addr.Bytes(), shorter[j].addr.Bytes(),
|
|
) == -1
|
|
},
|
|
)
|
|
|
|
// Expand
|
|
for _, staker := range shorter {
|
|
slotsCount := len(staker.slot.SpreadAmong)
|
|
if slotsCount == 0 {
|
|
continue
|
|
}
|
|
shardSlotsCount := make([]int, shardCount)
|
|
// may changed spread later
|
|
spread := numeric.NewDecFromBigInt(staker.slot.Stake).
|
|
QuoInt64(int64(slotsCount))
|
|
startIndex := len(eposedSlots)
|
|
for i := 0; i < slotsCount; i++ {
|
|
slot := SlotPurchase{
|
|
Addr: staker.addr,
|
|
Key: staker.slot.SpreadAmong[i],
|
|
// NOTE these are same because later the .EPoSStake mutated
|
|
RawStake: spread,
|
|
EPoSStake: spread,
|
|
}
|
|
shard := new(big.Int).Mod(slot.Key.Big(), big.NewInt(int64(shardCount))).Int64()
|
|
shardSlotsCount[int(shard)]++
|
|
// skip if count of slots in this shard exceeds the limit
|
|
if slotsLimit > 0 && shardSlotsCount[int(shard)] > slotsLimit {
|
|
continue
|
|
}
|
|
eposedSlots = append(eposedSlots, slot)
|
|
}
|
|
// recalculate the effectiveSpread if slots exceed the limit
|
|
if limitedSlotsCount := len(eposedSlots) - startIndex; limitedSlotsCount != slotsCount {
|
|
effectiveSpread := numeric.NewDecFromBigInt(staker.slot.Stake).QuoInt64(int64(limitedSlotsCount))
|
|
// spread is wrapped pointer of big.Int, when we set it's value, releated slot.RawStake and slot.EPoSStake will also 'changed'
|
|
spread.Int.Set(effectiveSpread.Int)
|
|
//for _, slot := range eposedSlots[startIndex:] {
|
|
// slot.RawStake = effectiveSpread
|
|
// slot.EPoSStake = effectiveSpread
|
|
//}
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(
|
|
eposedSlots,
|
|
func(i, j int) bool {
|
|
return eposedSlots[i].RawStake.GT(eposedSlots[j].RawStake)
|
|
},
|
|
)
|
|
|
|
if l := len(eposedSlots); l < pull {
|
|
pull = l
|
|
}
|
|
picks := eposedSlots[:pull]
|
|
|
|
if len(picks) == 0 {
|
|
return numeric.ZeroDec(), []SlotPurchase{}
|
|
}
|
|
|
|
return Median(picks), picks
|
|
|
|
}
|
|
|
|
// Apply ..
|
|
func Apply(shortHand map[common.Address]*SlotOrder, pull int, isExtendedBound bool, slotsLimit int, shardCount int) (
|
|
numeric.Dec, []SlotPurchase,
|
|
) {
|
|
median, picks := Compute(shortHand, pull, slotsLimit, shardCount)
|
|
max := onePlusC.Mul(median)
|
|
min := oneMinusC.Mul(median)
|
|
if isExtendedBound {
|
|
max = onePlusCV2.Mul(median)
|
|
min = oneMinusCV2.Mul(median)
|
|
}
|
|
for i := range picks {
|
|
picks[i].EPoSStake = effectiveStake(min, max, picks[i].RawStake)
|
|
}
|
|
|
|
return median, picks
|
|
}
|
|
|
|
func effectiveStake(min, max, actual numeric.Dec) numeric.Dec {
|
|
newMax := numeric.MinDec(max, actual)
|
|
return numeric.MaxDec(newMax, min)
|
|
}
|
|
|