Coverage Summary for Class: BlockSyncService (co.rsk.net)

Class Class, % Method, % Line, %
BlockSyncService 100% (1/1) 10.5% (2/19) 9.5% (10/105)


1 /* 2  * This file is part of RskJ 3  * Copyright (C) 2017 RSK Labs Ltd. 4  * 5  * This program is free software: you can redistribute it and/or modify 6  * it under the terms of the GNU Lesser General Public License as published by 7  * the Free Software Foundation, either version 3 of the License, or 8  * (at your option) any later version. 9  * 10  * This program is distributed in the hope that it will be useful, 11  * but WITHOUT ANY WARRANTY; without even the implied warranty of 12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13  * GNU Lesser General Public License for more details. 14  * 15  * You should have received a copy of the GNU Lesser General Public License 16  * along with this program. If not, see <http://www.gnu.org/licenses/>. 17  */ 18  19 package co.rsk.net; 20  21 import co.rsk.config.RskSystemProperties; 22 import co.rsk.core.bc.BlockUtils; 23 import co.rsk.crypto.Keccak256; 24 import co.rsk.net.messages.GetBlockMessage; 25 import co.rsk.net.sync.SyncConfiguration; 26 import co.rsk.validators.BlockValidator; 27 import org.ethereum.core.Block; 28 import org.ethereum.core.Blockchain; 29 import org.ethereum.core.ImportResult; 30 import org.slf4j.Logger; 31 import org.slf4j.LoggerFactory; 32  33 import javax.annotation.CheckForNull; 34 import javax.annotation.Nonnull; 35 import javax.annotation.Nullable; 36 import java.time.Instant; 37 import java.util.*; 38  39 /** 40  * BlockSyncService processes blocks to add into a blockchain. 41  * If a block is not ready to be added to the blockchain, it will be on hold in a BlockStore. 42  */ 43 public class BlockSyncService { 44  private static final Logger logger = LoggerFactory.getLogger("blocksyncservice"); 45  46  private static final int PROCESSED_BLOCKS_TO_CHECK_STORE = 200; 47  private static final int RELEASED_RANGE = 1000; 48  49  private long processedBlocksCounter; 50  private long lastKnownBlockNumber = 0; 51  52  private final NetBlockStore store; 53  private final Blockchain blockchain; 54  private final SyncConfiguration syncConfiguration; 55  private final BlockNodeInformation nodeInformation; // keep tabs on which nodes know which blocks. 56  private final RskSystemProperties config; 57  private final BlockValidator blockHeaderValidator; 58  59  // this is tightly coupled with NodeProcessorService and SyncProcessor, 60  // and we should use the same objects everywhere to ensure consistency 61  public BlockSyncService( 62  @Nonnull final RskSystemProperties config, 63  @Nonnull final NetBlockStore store, 64  @Nonnull final Blockchain blockchain, 65  @Nonnull final BlockNodeInformation nodeInformation, 66  @Nonnull final SyncConfiguration syncConfiguration, 67  @Nonnull final BlockValidator blockHeaderValidator) { 68  this.store = store; 69  this.blockchain = blockchain; 70  this.syncConfiguration = syncConfiguration; 71  this.nodeInformation = nodeInformation; 72  this.config = config; 73  this.blockHeaderValidator = blockHeaderValidator; 74  } 75  76  protected boolean preprocessBlock(@Nonnull Block block, Peer sender, boolean ignoreMissingHashes) { 77  final long bestBlockNumber = this.getBestBlockNumber(); 78  final long blockNumber = block.getNumber(); 79  final Keccak256 blockHash = block.getHash(); 80  final int syncMaxDistance = syncConfiguration.getChunkSize() * syncConfiguration.getMaxSkeletonChunks(); 81  82  tryReleaseStore(bestBlockNumber); 83  store.removeHeader(block.getHeader()); 84  85  if (blockNumber > bestBlockNumber + syncMaxDistance) { 86  logger.trace("Block too advanced {} {} from {} ", blockNumber, block.getPrintableHash(), 87  sender != null ? sender.getPeerNodeID().toString() : "N/A"); 88  return false; 89  } 90  91  if (sender != null) { 92  nodeInformation.addBlockToNode(blockHash, sender.getPeerNodeID()); 93  } 94  95  // already in a blockchain 96  if (BlockUtils.blockInSomeBlockChain(block, blockchain)) { 97  logger.trace("Block already in a chain {} {}", blockNumber, block.getPrintableHash()); 98  return false; 99  } 100  trySaveStore(block); 101  102  Set<Keccak256> unknownHashes = BlockUtils.unknownDirectAncestorsHashes(block, blockchain, store); 103  // We can't add the block if there are missing ancestors or uncles. Request the missing blocks to the sender. 104  if (!unknownHashes.isEmpty()) { 105  if (!ignoreMissingHashes) { 106  logger.trace("Missing hashes for block in process {} {}", blockNumber, block.getPrintableHash()); 107  requestMissingHashes(sender, unknownHashes); 108  } 109  return false; 110  } 111  112  return true; 113  } 114  115  public BlockProcessResult processBlock(@Nonnull Block block, Peer sender, boolean ignoreMissingHashes) { 116  final Instant start = Instant.now(); 117  118  // Should be refactored later to prevent block header validation in a few places. 119  // Validate block header first to see if its PoW is valid at all 120  if (!isBlockHeaderValid(block)) { 121  logger.warn("Invalid block with number {} {} from {} ", block.getNumber(), block.getHash(), sender); 122  return invalidBlockResult(block, start); 123  } 124  125  boolean readyForProcessing = preprocessBlock(block, sender, ignoreMissingHashes); 126  if (!readyForProcessing) { 127  return BlockProcessResult.ignoreBlockResult(block, start); 128  } 129  130  logger.trace("Trying to add to blockchain"); 131  132  Map<Keccak256, ImportResult> connectResult = connectBlocksAndDescendants(sender, 133  BlockUtils.sortBlocksByNumber(this.getParentsNotInBlockchain(block)), ignoreMissingHashes); 134  135  return BlockProcessResult.connectResult(block, start, connectResult); 136  } 137  138  private void tryReleaseStore(long bestBlockNumber) { 139  if ((++processedBlocksCounter % PROCESSED_BLOCKS_TO_CHECK_STORE) == 0) { 140  long minimal = store.minimalHeight(); 141  long maximum = store.maximumHeight(); 142  logger.trace("Blocks in block processor {} from height {} to height {}", this.store.size(), minimal, maximum); 143  144  if (minimal < bestBlockNumber - RELEASED_RANGE) { 145  store.releaseRange(minimal, minimal + RELEASED_RANGE); 146  } 147  } 148  } 149  150  public boolean hasBetterBlockToSync() { 151  return getLastKnownBlockNumber() >= getBestBlockNumber() + syncConfiguration.getLongSyncLimit(); 152  } 153  154  public boolean canBeIgnoredForUnclesRewards(long blockNumber) { 155  int blockDistance = config.getNetworkConstants().getUncleGenerationLimit(); 156  return blockNumber < getBestBlockNumber() - blockDistance; 157  } 158  159  public long getLastKnownBlockNumber() { 160  return this.lastKnownBlockNumber; 161  } 162  163  public void setLastKnownBlockNumber(long lastKnownBlockNumber) { 164  this.lastKnownBlockNumber = lastKnownBlockNumber; 165  } 166  167  private long getBestBlockNumber() { 168  return this.blockchain.getBestBlock().getNumber(); 169  } 170  171  private void trySaveStore(@Nonnull Block block) { 172  if (!this.store.hasBlock(block)) { 173  this.store.saveBlock(block); 174  } 175  } 176  177  private Map<Keccak256, ImportResult> connectBlocksAndDescendants(Peer sender, List<Block> blocks, boolean ignoreMissingHashes) { 178  Map<Keccak256, ImportResult> connectionsResult = new HashMap<>(); 179  List<Block> remainingBlocks = blocks; 180  while (!remainingBlocks.isEmpty()) { 181  Set<Block> connected = getConnectedBlocks(remainingBlocks, sender, connectionsResult, ignoreMissingHashes); 182  remainingBlocks = this.store.getChildrenOf(connected); 183  } 184  185  return connectionsResult; 186  } 187  188  private Set<Block> getConnectedBlocks(List<Block> remainingBlocks, Peer sender, Map<Keccak256, ImportResult> connectionsResult, boolean ignoreMissingHashes) { 189  Set<Block> connected = new HashSet<>(); 190  191  for (Block block : remainingBlocks) { 192  logger.trace("Trying to add block {} {}", block.getNumber(), block.getPrintableHash()); 193  194  Set<Keccak256> missingHashes = BlockUtils.unknownDirectAncestorsHashes(block, blockchain, store); 195  196  if (!missingHashes.isEmpty()) { 197  if (!ignoreMissingHashes){ 198  logger.trace("Missing hashes for block in process {} {}", block.getNumber(), block.getPrintableHash()); 199  requestMissingHashes(sender, missingHashes); 200  } 201  continue; 202  } 203  204  connectionsResult.put(block.getHash(), blockchain.tryToConnect(block)); 205  206  if (BlockUtils.blockInSomeBlockChain(block, blockchain)) { 207  this.store.removeBlock(block); 208  connected.add(block); 209  } 210  } 211  return connected; 212  } 213  214  private void requestMissingHashes(Peer sender, Set<Keccak256> hashes) { 215  logger.trace("Missing blocks to process {}", hashes.size()); 216  217  for (Keccak256 hash : hashes) { 218  this.requestMissingHash(sender, hash); 219  } 220  } 221  222  private void requestMissingHash(Peer sender, Keccak256 hash) { 223  if (sender == null) { 224  return; 225  } 226  227  logger.trace("Missing block {}", hash.toHexString()); 228  229  sender.sendMessage(new GetBlockMessage(hash.getBytes())); 230  } 231  232  /** 233  * getParentsNotInBlockchain returns all the ancestors of the block (including the block itself) that are not 234  * on the blockchain. It should be part of BlockChainImpl but is here because 235  * BlockChain is coupled with the old org.ethereum.db.BlockStore. 236  * 237  * @param block the base block. 238  * @return A list with the blocks sorted by ascending block number (the base block would be the last element). 239  */ 240  @Nonnull 241  private List<Block> getParentsNotInBlockchain(@Nullable Block block) { 242  final List<Block> blocks = new ArrayList<>(); 243  Block currentBlock = block; 244  while (currentBlock != null && !blockchain.hasBlockInSomeBlockchain(currentBlock.getHash().getBytes())) { 245  BlockUtils.addBlockToList(blocks, currentBlock); 246  247  currentBlock = getBlockFromStoreOrBlockchain(currentBlock.getParentHash().getBytes()); 248  } 249  250  return blocks; 251  } 252  253  /** 254  * getBlockFromStoreOrBlockchain retrieves a block from the store if it's available, 255  * or else from the blockchain. It should be part of BlockChainImpl but is here because 256  * BlockChain is coupled with the old org.ethereum.db.BlockStore. 257  */ 258  @CheckForNull 259  public Block getBlockFromStoreOrBlockchain(@Nonnull final byte[] hash) { 260  final Block block = store.getBlockByHash(hash); 261  262  if (block != null) { 263  return block; 264  } 265  266  return blockchain.getBlockByHash(hash); 267  } 268  269  private boolean isBlockHeaderValid(Block block) { 270  return blockHeaderValidator.isValid(block); 271  } 272  273  private static BlockProcessResult invalidBlockResult(@Nonnull Block block, @Nonnull Instant start) { 274  Map<Keccak256, ImportResult> result = Collections.singletonMap(block.getHash(), ImportResult.INVALID_BLOCK); 275  return BlockProcessResult.connectResult(block, start, result); 276  } 277 }