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 }