package sidechain import ( "encoding/binary" "fmt" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction" "git.gammaspectra.live/P2Pool/p2pool-observer/types" "git.gammaspectra.live/P2Pool/p2pool-observer/utils" "golang.org/x/exp/slices" "lukechampine.com/uint128" "math" "sync/atomic" ) type GetByMainIdFunc func(h types.Hash) *PoolBlock type GetByTemplateIdFunc func(h types.Hash) *PoolBlock type GetBySideHeightIdFunc func(height uint64) UniquePoolBlockSlice func CalculateOutputs(block *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, derivationCache DerivationCacheInterface, preAllocatedShares Shares) (outputs transaction.Outputs, bottomHeight uint64) { tmpShares, bottomHeight := GetShares(block, consensus, difficultyByHeight, getByTemplateId, preAllocatedShares) tmpRewards := SplitReward(block.Main.Coinbase.TotalReward, tmpShares) if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) { return nil, 0 } n := uint64(len(tmpShares)) outputs = make(transaction.Outputs, n) txType := block.GetTransactionOutputType() txPrivateKeySlice := block.Side.CoinbasePrivateKey.AsSlice() txPrivateKeyScalar := block.Side.CoinbasePrivateKey.AsScalar() _ = utils.SplitWork(-2, n, func(workIndex uint64, workerIndex int) error { output := transaction.Output{ Index: workIndex, Type: txType, } output.Reward = tmpRewards[output.Index] output.EphemeralPublicKey, output.ViewTag = derivationCache.GetEphemeralPublicKey(&tmpShares[output.Index].Address, txPrivateKeySlice, txPrivateKeyScalar, output.Index) outputs[output.Index] = output return nil }) return outputs, bottomHeight } type PoolBlockWindowSlot struct { Block *PoolBlock // Uncles that count for the window weight Uncles UniquePoolBlockSlice } type PoolBlockWindowAddWeightFunc func(b *PoolBlock, weight types.Difficulty) func IterateBlocksInPPLNSWindow(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, addWeightFunc PoolBlockWindowAddWeightFunc, errorPointer *atomic.Value) (results chan PoolBlockWindowSlot) { results = make(chan PoolBlockWindowSlot) go func() { defer close(results) cur := tip var blockDepth uint64 var mainchainDiff types.Difficulty if tip.Side.Parent != types.ZeroHash { seedHeight := randomx.SeedHeight(tip.Main.Coinbase.GenHeight) mainchainDiff = difficultyByHeight(seedHeight) if mainchainDiff == types.ZeroDifficulty { if errorPointer != nil { errorPointer.Store(fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)) } return } } // Dynamic PPLNS window starting from v2 // Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average) sidechainVersion := tip.ShareVersion() maxPplnsWeight := types.MaxDifficulty if sidechainVersion > ShareVersion_V1 { maxPplnsWeight = mainchainDiff.Mul64(2) } var pplnsWeight types.Difficulty for { curEntry := PoolBlockWindowSlot{ Block: cur, } curWeight := cur.Side.Difficulty for _, uncleId := range cur.Side.Uncles { if uncle := getByTemplateId(uncleId); uncle == nil { //cannot find uncles if errorPointer != nil { errorPointer.Store(fmt.Errorf("could not find uncle %s", uncleId.String())) } return } else { // Skip uncles which are already out of PPLNS window if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize { continue } // Take some % of uncle's weight into this share unclePenalty := uncle.Side.Difficulty.Mul64(consensus.UnclePenalty).Div64(100) uncleWeight := uncle.Side.Difficulty.Sub(unclePenalty) newPplnsWeight := pplnsWeight.Add(uncleWeight) // Skip uncles that push PPLNS weight above the limit if newPplnsWeight.Cmp(maxPplnsWeight) > 0 { continue } curWeight = curWeight.Add(unclePenalty) if addWeightFunc != nil { addWeightFunc(uncle, uncleWeight) } curEntry.Uncles = append(curEntry.Uncles, uncle) pplnsWeight = newPplnsWeight } } // Always add non-uncle shares even if PPLNS weight goes above the limit results <- curEntry if addWeightFunc != nil { addWeightFunc(cur, curWeight) } pplnsWeight = pplnsWeight.Add(curWeight) // One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty if pplnsWeight.Cmp(maxPplnsWeight) > 0 { break } blockDepth++ if blockDepth >= consensus.ChainWindowSize { break } // Reached the genesis block so we're done if cur.Side.Height == 0 { break } parentId := cur.Side.Parent cur = getByTemplateId(parentId) if cur == nil { errorPointer.Store(fmt.Errorf("could not find parent %s", parentId.String())) return } } }() return results } func GetShares(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, preAllocatedShares Shares) (shares Shares, bottomHeight uint64) { sharesSet := make(map[address.PackedAddress]*Share, consensus.ChainWindowSize*2) index := 0 indexGet := 0 l := len(preAllocatedShares) getPreAllocated := func() (s *Share) { if indexGet < l { s = preAllocatedShares[indexGet] } else { s = &Share{} } indexGet++ return s } insertSet := func(weight types.Difficulty, a *address.PackedAddress) { if _, ok := sharesSet[*a]; ok { sharesSet[*a].Weight = sharesSet[*a].Weight.Add(weight) } else { s := getPreAllocated() s.Weight = weight s.Address = *a sharesSet[*a] = s } } insertPreAllocated := func(share *Share) { if index < l { preAllocatedShares[index] = share } else { preAllocatedShares = append(preAllocatedShares, share) } index++ } var errValue atomic.Value for e := range IterateBlocksInPPLNSWindow(tip, consensus, difficultyByHeight, getByTemplateId, func(b *PoolBlock, weight types.Difficulty) { insertSet(weight, b.GetAddress()) }, &errValue) { bottomHeight = e.Block.Side.Height } if errValue.Load() != nil { return nil, 0 } for _, share := range sharesSet { insertPreAllocated(share) } shares = preAllocatedShares[:index] // Sort shares based on address slices.SortFunc(shares, func(a *Share, b *Share) bool { return a.Address.Compare(&b.Address) < 0 }) n := len(shares) //Shuffle shares if tip.ShareVersion() > ShareVersion_V1 && n > 1 { h := crypto.PooledKeccak256(tip.Side.CoinbasePrivateKeySeed[:]) seed := binary.LittleEndian.Uint64(h[:]) if seed == 0 { seed = 1 } for i := 0; i < (n - 1); i++ { seed = utils.XorShift64Star(seed) k := int(uint128.From64(seed).Mul64(uint64(n - i)).Hi) //swap shares[i], shares[i+k] = shares[i+k], shares[i] } } return shares, bottomHeight } func GetDifficulty(tip *PoolBlock, consensus *Consensus, getByTemplateId GetByTemplateIdFunc) types.Difficulty { difficultyData := make([]DifficultyData, 0, consensus.ChainWindowSize*2) cur := tip var blockDepth uint64 var oldestTimestamp uint64 = math.MaxUint64 for { oldestTimestamp = utils.Min(oldestTimestamp, cur.Main.Timestamp) difficultyData = append(difficultyData, DifficultyData{CumulativeDifficulty: cur.Side.CumulativeDifficulty, Timestamp: cur.Main.Timestamp}) for _, uncleId := range cur.Side.Uncles { if uncle := getByTemplateId(uncleId); uncle == nil { //cannot find uncles return types.ZeroDifficulty } else { // Skip uncles which are already out of PPLNS window if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize { continue } oldestTimestamp = utils.Min(oldestTimestamp, uncle.Main.Timestamp) difficultyData = append(difficultyData, DifficultyData{CumulativeDifficulty: uncle.Side.CumulativeDifficulty, Timestamp: uncle.Main.Timestamp}) } } blockDepth++ if blockDepth >= consensus.ChainWindowSize { break } // Reached the genesis block so we're done if cur.Side.Height == 0 { break } cur = getByTemplateId(cur.Side.Parent) if cur == nil { return types.ZeroDifficulty } } // Discard 10% oldest and 10% newest (by timestamp) blocks tmpTimestamps := make([]uint32, 0, len(difficultyData)) for i := range difficultyData { tmpTimestamps = append(tmpTimestamps, uint32(difficultyData[i].Timestamp-oldestTimestamp)) } cutSize := (len(difficultyData) + 9) / 10 index1 := cutSize - 1 index2 := len(difficultyData) - cutSize //TODO: replace this with introspective selection, use order for now slices.Sort(tmpTimestamps) timestamp1 := oldestTimestamp + uint64(tmpTimestamps[index1]) timestamp2 := oldestTimestamp + uint64(tmpTimestamps[index2]) deltaT := uint64(1) if timestamp2 > timestamp1 { deltaT = timestamp2 - timestamp1 } var diff1 = types.Difficulty{Hi: math.MaxUint64, Lo: math.MaxUint64} var diff2 types.Difficulty for i := range difficultyData { d := &difficultyData[i] if timestamp1 <= d.Timestamp && d.Timestamp <= timestamp2 { if d.CumulativeDifficulty.Cmp(diff1) < 0 { diff1 = d.CumulativeDifficulty } if diff2.Cmp(d.CumulativeDifficulty) < 0 { diff2 = d.CumulativeDifficulty } } } deltaDiff := diff2.Sub(diff1) curDifficulty := deltaDiff.Mul64(consensus.TargetBlockTime).Div64(deltaT) if curDifficulty.Cmp64(consensus.MinimumDifficulty) < 0 { curDifficulty = types.DifficultyFrom64(consensus.MinimumDifficulty) } return curDifficulty } func SplitReward(reward uint64, shares Shares) (rewards []uint64) { var totalWeight types.Difficulty for i := range shares { totalWeight = totalWeight.Add(shares[i].Weight) } if totalWeight.Equals64(0) { //TODO: err return nil } rewards = make([]uint64, len(shares)) var w types.Difficulty var rewardGiven uint64 for i := range shares { w = w.Add(shares[i].Weight) nextValue := w.Mul64(reward).Div(totalWeight) rewards[i] = nextValue.Lo - rewardGiven rewardGiven = nextValue.Lo } // Double check that we gave out the exact amount rewardGiven = 0 for _, r := range rewards { rewardGiven += r } if rewardGiven != reward { return nil } return rewards }