git.gammaspectra.live/P2Pool/consensus/v3@v3.8.0/p2pool/sidechain/utils.go (about)

     1  package sidechain
     2  
     3  import (
     4  	"fmt"
     5  	"git.gammaspectra.live/P2Pool/consensus/v3/monero"
     6  	"git.gammaspectra.live/P2Pool/consensus/v3/monero/block"
     7  	"git.gammaspectra.live/P2Pool/consensus/v3/monero/crypto"
     8  	"git.gammaspectra.live/P2Pool/consensus/v3/monero/randomx"
     9  	"git.gammaspectra.live/P2Pool/consensus/v3/monero/transaction"
    10  	"git.gammaspectra.live/P2Pool/consensus/v3/types"
    11  	"git.gammaspectra.live/P2Pool/consensus/v3/utils"
    12  	"git.gammaspectra.live/P2Pool/sha3"
    13  	"math"
    14  	"math/bits"
    15  	"slices"
    16  )
    17  
    18  type GetByMainIdFunc func(h types.Hash) *PoolBlock
    19  type GetByMainHeightFunc func(height uint64) UniquePoolBlockSlice
    20  type GetByTemplateIdFunc func(h types.Hash) *PoolBlock
    21  type GetBySideHeightFunc func(height uint64) UniquePoolBlockSlice
    22  
    23  // GetChainMainByHashFunc if h = types.ZeroHash, return tip
    24  type GetChainMainByHashFunc func(h types.Hash) *ChainMain
    25  
    26  func CalculateOutputs(block *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, derivationCache DerivationCacheInterface, preAllocatedShares Shares, preAllocatedRewards []uint64) (outputs transaction.Outputs, bottomHeight uint64) {
    27  	tmpShares, bottomHeight := GetShares(block, consensus, difficultyByHeight, getByTemplateId, preAllocatedShares)
    28  	if preAllocatedRewards == nil {
    29  		preAllocatedRewards = make([]uint64, 0, len(tmpShares))
    30  	}
    31  	tmpRewards := SplitReward(preAllocatedRewards, block.Main.Coinbase.AuxiliaryData.TotalReward, tmpShares)
    32  
    33  	if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) {
    34  		return nil, 0
    35  	}
    36  
    37  	n := uint64(len(tmpShares))
    38  
    39  	outputs = make(transaction.Outputs, n)
    40  
    41  	txType := block.GetTransactionOutputType()
    42  
    43  	txPrivateKeySlice := block.Side.CoinbasePrivateKey.AsSlice()
    44  	txPrivateKeyScalar := block.Side.CoinbasePrivateKey.AsScalar()
    45  
    46  	var hashers []*sha3.HasherState
    47  
    48  	defer func() {
    49  		for _, h := range hashers {
    50  			crypto.PutKeccak256Hasher(h)
    51  		}
    52  	}()
    53  
    54  	err := utils.SplitWork(-2, n, func(workIndex uint64, workerIndex int) error {
    55  		output := transaction.Output{
    56  			Index: workIndex,
    57  			Type:  txType,
    58  		}
    59  		output.Reward = tmpRewards[output.Index]
    60  		output.EphemeralPublicKey, output.ViewTag = derivationCache.GetEphemeralPublicKey(&tmpShares[output.Index].Address, txPrivateKeySlice, txPrivateKeyScalar, output.Index, hashers[workerIndex])
    61  
    62  		outputs[output.Index] = output
    63  
    64  		return nil
    65  	}, func(routines, routineIndex int) error {
    66  		hashers = append(hashers, crypto.GetKeccak256Hasher())
    67  		return nil
    68  	})
    69  
    70  	if err != nil {
    71  		return nil, 0
    72  	}
    73  
    74  	return outputs, bottomHeight
    75  }
    76  
    77  type PoolBlockWindowSlot struct {
    78  	Block *PoolBlock
    79  	// Uncles that count for the window weight
    80  	Uncles UniquePoolBlockSlice
    81  }
    82  
    83  type PoolBlockWindowAddWeightFunc func(b *PoolBlock, weight types.Difficulty)
    84  
    85  func IterateBlocksInPPLNSWindow(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, addWeightFunc PoolBlockWindowAddWeightFunc, slotFunc func(slot PoolBlockWindowSlot)) error {
    86  
    87  	cur := tip
    88  
    89  	var blockDepth uint64
    90  
    91  	var mainchainDiff types.Difficulty
    92  
    93  	if tip.Side.Parent != types.ZeroHash {
    94  		seedHeight := randomx.SeedHeight(tip.Main.Coinbase.GenHeight)
    95  		mainchainDiff = difficultyByHeight(seedHeight)
    96  		if mainchainDiff == types.ZeroDifficulty {
    97  			return fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
    98  		}
    99  	}
   100  
   101  	// Dynamic PPLNS window starting from v2
   102  	// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
   103  	sidechainVersion := tip.ShareVersion()
   104  
   105  	maxPplnsWeight := types.MaxDifficulty
   106  
   107  	if sidechainVersion > ShareVersion_V1 {
   108  		maxPplnsWeight = mainchainDiff.Mul64(2)
   109  	}
   110  
   111  	var pplnsWeight types.Difficulty
   112  
   113  	for {
   114  		curEntry := PoolBlockWindowSlot{
   115  			Block: cur,
   116  		}
   117  		curWeight := cur.Side.Difficulty
   118  
   119  		if err := cur.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
   120  			//Needs to be added regardless - for other consumers
   121  			curEntry.Uncles = append(curEntry.Uncles, uncle)
   122  
   123  			// Skip uncles which are already out of PPLNS window
   124  			if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
   125  				return
   126  			}
   127  
   128  			// Take some % of uncle's weight into this share
   129  			uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(uncle.Side.Difficulty)
   130  			newPplnsWeight := pplnsWeight.Add(uncleWeight)
   131  
   132  			// Skip uncles that push PPLNS weight above the limit
   133  			if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
   134  				return
   135  			}
   136  			curWeight = curWeight.Add(unclePenalty)
   137  
   138  			if addWeightFunc != nil {
   139  				addWeightFunc(uncle, uncleWeight)
   140  			}
   141  
   142  			pplnsWeight = newPplnsWeight
   143  		}); err != nil {
   144  			return err
   145  		}
   146  
   147  		// Always add non-uncle shares even if PPLNS weight goes above the limit
   148  		slotFunc(curEntry)
   149  
   150  		if addWeightFunc != nil {
   151  			addWeightFunc(cur, curWeight)
   152  		}
   153  
   154  		pplnsWeight = pplnsWeight.Add(curWeight)
   155  
   156  		// One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty
   157  		if pplnsWeight.Cmp(maxPplnsWeight) > 0 {
   158  			break
   159  		}
   160  
   161  		blockDepth++
   162  
   163  		if blockDepth >= consensus.ChainWindowSize {
   164  			break
   165  		}
   166  
   167  		// Reached the genesis block so we're done
   168  		if cur.Side.Height == 0 {
   169  			break
   170  		}
   171  
   172  		parentId := cur.Side.Parent
   173  		cur = cur.iteratorGetParent(getByTemplateId)
   174  
   175  		if cur == nil {
   176  			return fmt.Errorf("could not find parent %s", parentId.String())
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  func BlocksInPPLNSWindow(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, addWeightFunc PoolBlockWindowAddWeightFunc) (bottomHeight uint64, err error) {
   183  
   184  	cur := tip
   185  
   186  	var blockDepth uint64
   187  
   188  	var mainchainDiff types.Difficulty
   189  
   190  	if tip.Side.Parent != types.ZeroHash {
   191  		seedHeight := randomx.SeedHeight(tip.Main.Coinbase.GenHeight)
   192  		mainchainDiff = difficultyByHeight(seedHeight)
   193  		if mainchainDiff == types.ZeroDifficulty {
   194  			return 0, fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
   195  		}
   196  	}
   197  
   198  	// Dynamic PPLNS window starting from v2
   199  	// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
   200  	sidechainVersion := tip.ShareVersion()
   201  
   202  	maxPplnsWeight := types.MaxDifficulty
   203  
   204  	if sidechainVersion > ShareVersion_V1 {
   205  		maxPplnsWeight = mainchainDiff.Mul64(2)
   206  	}
   207  
   208  	var pplnsWeight types.Difficulty
   209  
   210  	for {
   211  		curWeight := cur.Side.Difficulty
   212  
   213  		if err := cur.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
   214  			// Skip uncles which are already out of PPLNS window
   215  			if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
   216  				return
   217  			}
   218  
   219  			// Take some % of uncle's weight into this share
   220  			uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(uncle.Side.Difficulty)
   221  
   222  			newPplnsWeight := pplnsWeight.Add(uncleWeight)
   223  
   224  			// Skip uncles that push PPLNS weight above the limit
   225  			if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
   226  				return
   227  			}
   228  			curWeight = curWeight.Add(unclePenalty)
   229  
   230  			addWeightFunc(uncle, uncleWeight)
   231  
   232  			pplnsWeight = newPplnsWeight
   233  
   234  		}); err != nil {
   235  			return 0, err
   236  		}
   237  
   238  		// Always add non-uncle shares even if PPLNS weight goes above the limit
   239  		bottomHeight = cur.Side.Height
   240  
   241  		addWeightFunc(cur, curWeight)
   242  
   243  		pplnsWeight = pplnsWeight.Add(curWeight)
   244  
   245  		// One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty
   246  		if pplnsWeight.Cmp(maxPplnsWeight) > 0 {
   247  			break
   248  		}
   249  
   250  		blockDepth++
   251  
   252  		if blockDepth >= consensus.ChainWindowSize {
   253  			break
   254  		}
   255  
   256  		// Reached the genesis block so we're done
   257  		if cur.Side.Height == 0 {
   258  			break
   259  		}
   260  
   261  		parentId := cur.Side.Parent
   262  		cur = cur.iteratorGetParent(getByTemplateId)
   263  
   264  		if cur == nil {
   265  			return 0, fmt.Errorf("could not find parent %s", parentId.String())
   266  		}
   267  	}
   268  	return bottomHeight, nil
   269  }
   270  
   271  func GetSharesOrdered(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, preAllocatedShares Shares) (shares Shares, bottomHeight uint64) {
   272  	index := 0
   273  	l := len(preAllocatedShares)
   274  
   275  	if bottomHeight, err := BlocksInPPLNSWindow(tip, consensus, difficultyByHeight, getByTemplateId, func(b *PoolBlock, weight types.Difficulty) {
   276  		if index < l {
   277  			preAllocatedShares[index].Address = b.Side.PublicKey
   278  
   279  			preAllocatedShares[index].Weight = weight
   280  		} else {
   281  			preAllocatedShares = append(preAllocatedShares, &Share{
   282  				Address: b.Side.PublicKey,
   283  				Weight:  weight,
   284  			})
   285  		}
   286  		index++
   287  	}); err != nil {
   288  		return nil, 0
   289  	} else {
   290  		shares = preAllocatedShares[:index]
   291  
   292  		//remove dupes
   293  		shares = shares.Compact()
   294  
   295  		return shares, bottomHeight
   296  	}
   297  }
   298  
   299  func GetShares(tip *PoolBlock, consensus *Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, preAllocatedShares Shares) (shares Shares, bottomHeight uint64) {
   300  	shares, bottomHeight = GetSharesOrdered(tip, consensus, difficultyByHeight, getByTemplateId, preAllocatedShares)
   301  	if shares == nil {
   302  		return
   303  	}
   304  
   305  	//Shuffle shares
   306  	ShuffleShares(shares, tip.ShareVersion(), tip.Side.CoinbasePrivateKeySeed)
   307  
   308  	return shares, bottomHeight
   309  }
   310  
   311  // ShuffleShares Shuffles shares according to consensus parameters via ShuffleSequence. Requires pre-sorted shares based on address
   312  func ShuffleShares[T any](shares []T, shareVersion ShareVersion, privateKeySeed types.Hash) {
   313  	ShuffleSequence(shareVersion, privateKeySeed, len(shares), func(i, j int) {
   314  		shares[i], shares[j] = shares[j], shares[i]
   315  	})
   316  }
   317  
   318  // ShuffleSequence Iterates through a swap sequence according to consensus parameters.
   319  func ShuffleSequence(shareVersion ShareVersion, privateKeySeed types.Hash, items int, swap func(i, j int)) {
   320  	n := uint64(items)
   321  	if shareVersion > ShareVersion_V1 && n > 1 {
   322  		seed := crypto.PooledKeccak256(privateKeySeed[:]).Uint64()
   323  
   324  		if seed == 0 {
   325  			seed = 1
   326  		}
   327  
   328  		for i := uint64(0); i < (n - 1); i++ {
   329  			seed = utils.XorShift64Star(seed)
   330  			k, _ := bits.Mul64(seed, n-i)
   331  			//swap
   332  			swap(int(i), int(i+k))
   333  		}
   334  	}
   335  }
   336  
   337  type DifficultyData struct {
   338  	cumulativeDifficulty types.Difficulty
   339  	timestamp            uint64
   340  }
   341  
   342  // GetDifficultyForNextBlock Gets the difficulty at tip (the next block will require this difficulty)
   343  // preAllocatedDifficultyData should contain enough capacity to fit all entries to iterate through.
   344  // preAllocatedTimestampDifferences should contain enough capacity to fit all differences.
   345  //
   346  // Ported from SideChain::get_difficulty() from C p2pool,
   347  // somewhat based on Blockchain::get_difficulty_for_next_block() from Monero with the addition of uncles
   348  func GetDifficultyForNextBlock(tip *PoolBlock, consensus *Consensus, getByTemplateId GetByTemplateIdFunc, preAllocatedDifficultyData []DifficultyData, preAllocatedTimestampData []uint64) (difficulty types.Difficulty, verifyError, invalidError error) {
   349  
   350  	difficultyData := preAllocatedDifficultyData[:0]
   351  
   352  	timestampData := preAllocatedTimestampData[:0]
   353  
   354  	cur := tip
   355  	var blockDepth uint64
   356  
   357  	for {
   358  		difficultyData = append(difficultyData, DifficultyData{
   359  			cumulativeDifficulty: cur.Side.CumulativeDifficulty,
   360  			timestamp:            cur.Main.Timestamp,
   361  		})
   362  
   363  		timestampData = append(timestampData, cur.Main.Timestamp)
   364  
   365  		if err := cur.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
   366  			// Skip uncles which are already out of PPLNS window
   367  			if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
   368  				return
   369  			}
   370  
   371  			difficultyData = append(difficultyData, DifficultyData{
   372  				cumulativeDifficulty: uncle.Side.CumulativeDifficulty,
   373  				timestamp:            uncle.Main.Timestamp,
   374  			})
   375  
   376  			timestampData = append(timestampData, uncle.Main.Timestamp)
   377  		}); err != nil {
   378  			return types.ZeroDifficulty, err, nil
   379  		}
   380  
   381  		blockDepth++
   382  
   383  		if blockDepth >= consensus.ChainWindowSize {
   384  			break
   385  		}
   386  
   387  		// Reached the genesis block so we're done
   388  		if cur.Side.Height == 0 {
   389  			break
   390  		}
   391  
   392  		parentId := cur.Side.Parent
   393  		cur = cur.iteratorGetParent(getByTemplateId)
   394  
   395  		if cur == nil {
   396  			return types.ZeroDifficulty, fmt.Errorf("could not find parent %s", parentId.String()), nil
   397  		}
   398  	}
   399  
   400  	difficulty, invalidError = NextDifficulty(consensus, timestampData, difficultyData)
   401  	return
   402  }
   403  
   404  // NextDifficulty returns the next block difficulty based on gathered timestamp/difficulty data
   405  // Returns error on wrap/overflow/underflow on uint128 operations
   406  func NextDifficulty(consensus *Consensus, timestamps []uint64, difficultyData []DifficultyData) (nextDifficulty types.Difficulty, err error) {
   407  	// Discard 10% oldest and 10% newest (by timestamp) blocks
   408  
   409  	cutSize := (len(timestamps) + 9) / 10
   410  	lowIndex := cutSize - 1
   411  	upperIndex := len(timestamps) - cutSize
   412  
   413  	utils.NthElementSlice(timestamps, lowIndex)
   414  	timestampLowerBound := timestamps[lowIndex]
   415  
   416  	utils.NthElementSlice(timestamps, upperIndex)
   417  	timestampUpperBound := timestamps[upperIndex]
   418  
   419  	// Make a reasonable assumption that each block has higher timestamp, so deltaTimestamp can't be less than deltaIndex
   420  	// Because if it is, someone is trying to mess with timestamps
   421  	// In reality, deltaTimestamp ~ deltaIndex*10 (sidechain block time)
   422  	deltaIndex := uint64(1)
   423  	if upperIndex > lowIndex {
   424  		deltaIndex = uint64(upperIndex - lowIndex)
   425  	}
   426  	deltaTimestamp := deltaIndex
   427  	if timestampUpperBound > (timestampLowerBound + deltaIndex) {
   428  		deltaTimestamp = timestampUpperBound - timestampLowerBound
   429  	}
   430  
   431  	var minDifficulty = types.Difficulty{Hi: math.MaxUint64, Lo: math.MaxUint64}
   432  	var maxDifficulty types.Difficulty
   433  
   434  	for i := range difficultyData {
   435  		// Pick only the cumulative difficulty from specifically the entries that are within the timestamp upper and low bounds
   436  		if timestampLowerBound <= difficultyData[i].timestamp && difficultyData[i].timestamp <= timestampUpperBound {
   437  			if minDifficulty.Cmp(difficultyData[i].cumulativeDifficulty) > 0 {
   438  				minDifficulty = difficultyData[i].cumulativeDifficulty
   439  			}
   440  			if maxDifficulty.Cmp(difficultyData[i].cumulativeDifficulty) < 0 {
   441  				maxDifficulty = difficultyData[i].cumulativeDifficulty
   442  			}
   443  		}
   444  	}
   445  
   446  	// Specific section that could wrap and needs to be detected
   447  	// Use calls that panic on wrap/overflow/underflow
   448  	{
   449  		defer func() {
   450  			if e := recover(); e != nil {
   451  				if panicError, ok := e.(error); ok {
   452  					err = fmt.Errorf("panic in NextDifficulty, wrap occured?: %w", panicError)
   453  				} else {
   454  					err = fmt.Errorf("panic in NextDifficulty, wrap occured?: %v", e)
   455  				}
   456  			}
   457  		}()
   458  
   459  		deltaDifficulty := maxDifficulty.Sub(minDifficulty)
   460  		curDifficulty := deltaDifficulty.Mul64(consensus.TargetBlockTime).Div64(deltaTimestamp)
   461  
   462  		if curDifficulty.Cmp64(consensus.MinimumDifficulty) < 0 {
   463  			return types.DifficultyFrom64(consensus.MinimumDifficulty), nil
   464  		}
   465  		return curDifficulty, nil
   466  	}
   467  }
   468  
   469  func SplitRewardAllocate(reward uint64, shares Shares) (rewards []uint64) {
   470  	return SplitReward(make([]uint64, 0, len(shares)), reward, shares)
   471  }
   472  
   473  func SplitReward(preAllocatedRewards []uint64, reward uint64, shares Shares) (rewards []uint64) {
   474  	var totalWeight types.Difficulty
   475  	for i := range shares {
   476  		totalWeight = totalWeight.Add(shares[i].Weight)
   477  	}
   478  
   479  	if totalWeight.Equals64(0) {
   480  		//TODO: err
   481  		return nil
   482  	}
   483  
   484  	rewards = preAllocatedRewards[:0]
   485  
   486  	var w types.Difficulty
   487  	var rewardGiven uint64
   488  
   489  	for _, share := range shares {
   490  		w = w.Add(share.Weight)
   491  		nextValue := w.Mul64(reward).Div(totalWeight)
   492  		rewards = append(rewards, nextValue.Lo-rewardGiven)
   493  		rewardGiven = nextValue.Lo
   494  	}
   495  
   496  	// Double check that we gave out the exact amount
   497  	rewardGiven = 0
   498  	for _, r := range rewards {
   499  		rewardGiven += r
   500  	}
   501  	if rewardGiven != reward {
   502  		return nil
   503  	}
   504  
   505  	return rewards
   506  }
   507  
   508  func IsLongerChain(block, candidate *PoolBlock, consensus *Consensus, getByTemplateId GetByTemplateIdFunc, getChainMainByHash GetChainMainByHashFunc) (isLonger, isAlternative bool) {
   509  	if candidate == nil || !candidate.Verified.Load() || candidate.Invalid.Load() {
   510  		return false, false
   511  	}
   512  
   513  	// Switching from an empty to a non-empty chain
   514  	if block == nil {
   515  		return true, true
   516  	}
   517  
   518  	// If these two blocks are on the same chain, they must have a common ancestor
   519  
   520  	blockAncestor := block
   521  	for blockAncestor != nil && blockAncestor.Side.Height > candidate.Side.Height {
   522  		blockAncestor = blockAncestor.iteratorGetParent(getByTemplateId)
   523  		//TODO: err on blockAncestor nil
   524  	}
   525  
   526  	if blockAncestor != nil {
   527  		candidateAncestor := candidate
   528  		for candidateAncestor != nil && candidateAncestor.Side.Height > blockAncestor.Side.Height {
   529  			candidateAncestor = candidateAncestor.iteratorGetParent(getByTemplateId)
   530  			//TODO: err on candidateAncestor nil
   531  		}
   532  
   533  		for blockAncestor != nil && candidateAncestor != nil {
   534  			if blockAncestor.Side.Parent == candidateAncestor.Side.Parent {
   535  				return block.Side.CumulativeDifficulty.Cmp(candidate.Side.CumulativeDifficulty) < 0, false
   536  			}
   537  			blockAncestor = blockAncestor.iteratorGetParent(getByTemplateId)
   538  			candidateAncestor = candidateAncestor.iteratorGetParent(getByTemplateId)
   539  		}
   540  	}
   541  
   542  	// They're on totally different chains. Compare total difficulties over the last m_chainWindowSize blocks
   543  
   544  	var blockTotalDiff, candidateTotalDiff types.Difficulty
   545  
   546  	oldChain := block
   547  	newChain := candidate
   548  
   549  	var candidateMainchainHeight, candidateMainchainMinHeight uint64
   550  
   551  	var moneroBlocksReserve = consensus.ChainWindowSize * consensus.TargetBlockTime * 2 / monero.BlockTime
   552  	currentChainMoneroBlocks, candidateChainMoneroBlocks := make([]types.Hash, 0, moneroBlocksReserve), make([]types.Hash, 0, moneroBlocksReserve)
   553  
   554  	for i := uint64(0); i < consensus.ChainWindowSize && (oldChain != nil || newChain != nil); i++ {
   555  		if oldChain != nil {
   556  			blockTotalDiff = blockTotalDiff.Add(oldChain.Side.Difficulty)
   557  			_ = oldChain.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
   558  				blockTotalDiff = blockTotalDiff.Add(uncle.Side.Difficulty)
   559  			})
   560  			if !slices.Contains(currentChainMoneroBlocks, oldChain.Main.PreviousId) && getChainMainByHash(oldChain.Main.PreviousId) != nil {
   561  				currentChainMoneroBlocks = append(currentChainMoneroBlocks, oldChain.Main.PreviousId)
   562  			}
   563  			oldChain = oldChain.iteratorGetParent(getByTemplateId)
   564  		}
   565  
   566  		if newChain != nil {
   567  			if candidateMainchainMinHeight != 0 {
   568  				candidateMainchainMinHeight = min(candidateMainchainMinHeight, newChain.Main.Coinbase.GenHeight)
   569  			} else {
   570  				candidateMainchainMinHeight = newChain.Main.Coinbase.GenHeight
   571  			}
   572  			candidateTotalDiff = candidateTotalDiff.Add(newChain.Side.Difficulty)
   573  			_ = newChain.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
   574  				candidateTotalDiff = candidateTotalDiff.Add(uncle.Side.Difficulty)
   575  			})
   576  			if !slices.Contains(candidateChainMoneroBlocks, newChain.Main.PreviousId) {
   577  				if data := getChainMainByHash(newChain.Main.PreviousId); data != nil {
   578  					candidateChainMoneroBlocks = append(candidateChainMoneroBlocks, newChain.Main.PreviousId)
   579  					candidateMainchainHeight = max(candidateMainchainHeight, data.Height)
   580  				}
   581  			}
   582  
   583  			newChain = newChain.iteratorGetParent(getByTemplateId)
   584  		}
   585  	}
   586  
   587  	if blockTotalDiff.Cmp(candidateTotalDiff) >= 0 {
   588  		return false, true
   589  	}
   590  
   591  	// Candidate chain must be built on top of recent mainchain blocks
   592  	if headerTip := getChainMainByHash(types.ZeroHash); headerTip != nil {
   593  		if candidateMainchainHeight+10 < headerTip.Height {
   594  			utils.Logf("SideChain", "Received a longer alternative chain but it's stale: height %d, current height %d", candidateMainchainHeight, headerTip.Height)
   595  			return false, true
   596  		}
   597  
   598  		limit := consensus.ChainWindowSize * 4 * consensus.TargetBlockTime / monero.BlockTime
   599  		if candidateMainchainMinHeight+limit < headerTip.Height {
   600  			utils.Logf("SideChain", "Received a longer alternative chain but it's stale: min height %d, must be >= %d", candidateMainchainMinHeight, headerTip.Height-limit)
   601  			return false, true
   602  		}
   603  
   604  		// Candidate chain must have been mined on top of at least half as many known Monero blocks, compared to the current chain
   605  		if len(candidateChainMoneroBlocks)*2 < len(currentChainMoneroBlocks) {
   606  			utils.Logf("SideChain", "Received a longer alternative chain but it wasn't mined on current Monero blockchain: only %d / %d blocks found", len(candidateChainMoneroBlocks), len(currentChainMoneroBlocks))
   607  			return false, true
   608  		}
   609  
   610  		return true, true
   611  	} else {
   612  		return false, true
   613  	}
   614  }