git.gammaspectra.live/P2Pool/consensus@v0.0.0-20240403173234-a039820b20c9/p2pool/sidechain/utils.go (about)

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