consensus/cmd/index/utils.go

448 lines
13 KiB
Go

package index
import (
"errors"
"fmt"
"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/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"git.gammaspectra.live/P2Pool/sha3"
"slices"
)
type GetByTemplateIdFunc func(h types.Hash) *SideBlock
type GetUnclesByTemplateIdFunc func(h types.Hash) QueryIterator[SideBlock]
type SideBlockWindowAddWeightFunc func(b *SideBlock, weight types.Difficulty)
type SideBlockWindowSlot struct {
Block *SideBlock
// Uncles that count for the window weight
Uncles []*SideBlock
}
// IterateSideBlocksInPPLNSWindow
// Copy of sidechain.IterateBlocksInPPLNSWindow
func IterateSideBlocksInPPLNSWindow(tip *SideBlock, consensus *sidechain.Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, addWeightFunc SideBlockWindowAddWeightFunc, slotFunc func(slot SideBlockWindowSlot)) error {
cur := tip
var blockDepth uint64
var mainchainDiff types.Difficulty
if tip.ParentTemplateId != types.ZeroHash {
seedHeight := randomx.SeedHeight(tip.MainHeight)
mainchainDiff = difficultyByHeight(seedHeight)
if mainchainDiff == types.ZeroDifficulty {
return fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
}
}
// Dynamic PPLNS window starting from v2
// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
sidechainVersion := sidechain.P2PoolShareVersion(consensus, tip.Timestamp)
maxPplnsWeight := types.MaxDifficulty
if sidechainVersion > sidechain.ShareVersion_V1 {
maxPplnsWeight = mainchainDiff.Mul64(2)
}
var pplnsWeight types.Difficulty
for {
curEntry := SideBlockWindowSlot{
Block: cur,
}
curWeight := types.DifficultyFrom64(cur.Difficulty)
QueryIterate(getUnclesByTemplateId(cur.TemplateId), func(_ int, uncle *SideBlock) (stop bool) {
//Needs to be added regardless - for other consumers
if !slices.ContainsFunc(curEntry.Uncles, func(sideBlock *SideBlock) bool {
return sideBlock.TemplateId == uncle.TemplateId
}) {
curEntry.Uncles = append(curEntry.Uncles, uncle)
}
// Skip uncles which are already out of PPLNS window
if (tip.SideHeight - uncle.SideHeight) >= consensus.ChainWindowSize {
return false
}
// Take some % of uncle's weight into this share
uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(types.DifficultyFrom64(uncle.Difficulty))
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
return false
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
return false
})
// Always add non-uncle shares even if PPLNS weight goes above the limit
slotFunc(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.SideHeight == 0 {
break
}
parentId := cur.ParentTemplateId
cur = getByTemplateId(parentId)
if cur == nil {
return fmt.Errorf("could not find parent %s", parentId.String())
}
}
return nil
}
// BlocksInPPLNSWindow
// Copy of sidechain.BlocksInPPLNSWindow
func BlocksInPPLNSWindow(tip *SideBlock, consensus *sidechain.Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, addWeightFunc SideBlockWindowAddWeightFunc) (bottomHeight uint64, err error) {
cur := tip
var blockDepth uint64
var mainchainDiff types.Difficulty
if tip.ParentTemplateId != types.ZeroHash {
seedHeight := randomx.SeedHeight(tip.MainHeight)
mainchainDiff = difficultyByHeight(seedHeight)
if mainchainDiff == types.ZeroDifficulty {
return 0, fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
}
}
// Dynamic PPLNS window starting from v2
// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
sidechainVersion := sidechain.P2PoolShareVersion(consensus, tip.Timestamp)
maxPplnsWeight := types.MaxDifficulty
if sidechainVersion > sidechain.ShareVersion_V1 {
maxPplnsWeight = mainchainDiff.Mul64(2)
}
var pplnsWeight types.Difficulty
for {
curEntry := SideBlockWindowSlot{
Block: cur,
}
curWeight := types.DifficultyFrom64(cur.Difficulty)
QueryIterate(getUnclesByTemplateId(cur.TemplateId), func(_ int, uncle *SideBlock) (stop bool) {
//Needs to be added regardless - for other consumers
if !slices.ContainsFunc(curEntry.Uncles, func(sideBlock *SideBlock) bool {
return sideBlock.TemplateId == uncle.TemplateId
}) {
curEntry.Uncles = append(curEntry.Uncles, uncle)
}
// Skip uncles which are already out of PPLNS window
if (tip.SideHeight - uncle.SideHeight) >= consensus.ChainWindowSize {
return false
}
// Take some % of uncle's weight into this share
uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(types.DifficultyFrom64(uncle.Difficulty))
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
return false
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
return false
})
// Always add non-uncle shares even if PPLNS weight goes above the limit
bottomHeight = cur.SideHeight
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.SideHeight == 0 {
break
}
parentId := cur.ParentTemplateId
cur = getByTemplateId(parentId)
if cur == nil {
return 0, fmt.Errorf("could not find parent %s", parentId.String())
}
}
return bottomHeight, nil
}
func IterateSideBlocksInPPLNSWindowFast(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, addWeightFunc SideBlockWindowAddWeightFunc, slotFunc func(slot SideBlockWindowSlot)) error {
if tip == nil {
return errors.New("nil tip")
}
window := QueryIterateToSlice(indexDb.GetSideBlocksInPPLNSWindow(tip))
if len(window) == 0 {
return errors.New("nil window")
}
var hintIndex int
getByTemplateIdFull := func(h types.Hash) *SideBlock {
if i := slices.IndexFunc(window, func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex = i
return window[i]
}
return nil
}
getByTemplateId := func(h types.Hash) *SideBlock {
//fast lookup first
if i := slices.IndexFunc(window[hintIndex:], func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex += i
return window[hintIndex]
}
return getByTemplateIdFull(h)
}
getUnclesOf := func(h types.Hash) QueryIterator[SideBlock] {
parentEffectiveHeight := window[hintIndex].EffectiveHeight
if window[hintIndex].TemplateId != h {
parentEffectiveHeight = 0
}
startIndex := 0
return &FakeQueryResult[SideBlock]{
NextFunction: func() (int, *SideBlock) {
for _, b := range window[startIndex+hintIndex:] {
if b.UncleOf == h {
startIndex++
return startIndex, b
}
startIndex++
if parentEffectiveHeight != 0 && b.EffectiveHeight < parentEffectiveHeight {
//early exit
break
}
}
return 0, nil
},
}
}
return IterateSideBlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesOf, addWeightFunc, slotFunc)
}
func BlocksInPPLNSWindowFast(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, addWeightFunc SideBlockWindowAddWeightFunc) (bottomHeight uint64, err error) {
if tip == nil {
return 0, errors.New("nil tip")
}
window := QueryIterateToSlice(indexDb.GetSideBlocksInPPLNSWindow(tip))
if len(window) == 0 {
return 0, errors.New("nil window")
}
var hintIndex int
getByTemplateIdFull := func(h types.Hash) *SideBlock {
if i := slices.IndexFunc(window, func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex = i
return window[i]
}
return nil
}
getByTemplateId := func(h types.Hash) *SideBlock {
//fast lookup first
if i := slices.IndexFunc(window[hintIndex:], func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex += i
return window[hintIndex]
}
return getByTemplateIdFull(h)
}
getUnclesOf := func(h types.Hash) QueryIterator[SideBlock] {
parentEffectiveHeight := window[hintIndex].EffectiveHeight
if window[hintIndex].TemplateId != h {
parentEffectiveHeight = 0
}
startIndex := 0
return &FakeQueryResult[SideBlock]{
NextFunction: func() (int, *SideBlock) {
for _, b := range window[startIndex+hintIndex:] {
if b.UncleOf == h {
startIndex++
return startIndex, b
}
startIndex++
if parentEffectiveHeight != 0 && b.EffectiveHeight < parentEffectiveHeight {
//early exit
break
}
}
return 0, nil
},
}
}
return BlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesOf, addWeightFunc)
}
// GetSharesOrdered
// Copy of sidechain.GetSharesOrdered
func GetSharesOrdered(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, preAllocatedShares sidechain.Shares) (shares sidechain.Shares, bottomHeight uint64) {
index := 0
l := len(preAllocatedShares)
if bottomHeight, err := BlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesByTemplateId, func(b *SideBlock, weight types.Difficulty) {
addr := indexDb.GetMiner(b.Miner).Address().ToPackedAddress()
if index < l {
preAllocatedShares[index].Address = addr
preAllocatedShares[index].Weight = weight
} else {
preAllocatedShares = append(preAllocatedShares, &sidechain.Share{
Address: addr,
Weight: weight,
})
}
index++
}); err != nil {
return nil, 0
} else {
shares = preAllocatedShares[:index]
//remove dupes
shares = shares.Compact()
return shares, bottomHeight
}
}
// GetShares
// Copy of sidechain.GetShares
func GetShares(indexDb *Index, tip *SideBlock, coinbasePrivateKeySeed types.Hash, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, preAllocatedShares sidechain.Shares) (shares sidechain.Shares, bottomHeight uint64) {
shares, bottomHeight = GetSharesOrdered(indexDb, tip, difficultyByHeight, getByTemplateId, getUnclesByTemplateId, preAllocatedShares)
if shares == nil {
return
}
//Shuffle shares
sidechain.ShuffleShares(shares, sidechain.P2PoolShareVersion(indexDb.Consensus(), tip.Timestamp), coinbasePrivateKeySeed)
return shares, bottomHeight
}
// CalculateOutputs
// Copy of sidechain.CalculateOutputs
func CalculateOutputs(indexDb *Index, block *SideBlock, transactionOutputType uint8, totalReward uint64, coinbasePrivateKey crypto.PrivateKey, coinbasePrivateKeySeed types.Hash, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, derivationCache sidechain.DerivationCacheInterface, preAllocatedShares sidechain.Shares, preAllocatedRewards []uint64) (outputs transaction.Outputs, bottomHeight uint64) {
tmpShares, bottomHeight := GetShares(indexDb, block, coinbasePrivateKeySeed, difficultyByHeight, getByTemplateId, getUnclesByTemplateId, preAllocatedShares)
if preAllocatedRewards == nil {
preAllocatedRewards = make([]uint64, 0, len(tmpShares))
}
tmpRewards := sidechain.SplitReward(preAllocatedRewards, totalReward, tmpShares)
if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) {
return nil, 0
}
n := uint64(len(tmpShares))
outputs = make(transaction.Outputs, n)
txType := transactionOutputType
txPrivateKeySlice := coinbasePrivateKey.AsSlice()
txPrivateKeyScalar := coinbasePrivateKey.AsScalar()
var hashers []*sha3.HasherState
defer func() {
for _, h := range hashers {
crypto.PutKeccak256Hasher(h)
}
}()
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, hashers[workerIndex])
outputs[output.Index] = output
return nil
}, func(routines, routineIndex int) error {
hashers = append(hashers, crypto.GetKeccak256Hasher())
return nil
}, nil)
return outputs, bottomHeight
}