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 }