Coverage Summary for Class: BlockChainImpl (co.rsk.core.bc)

Class Class, % Method, % Line, %
BlockChainImpl 100% (1/1) 67.9% (19/28) 76.2% (157/206)


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.core.bc; 20  21 import co.rsk.core.BlockDifficulty; 22 import co.rsk.db.StateRootHandler; 23 import co.rsk.metrics.profilers.Metric; 24 import co.rsk.metrics.profilers.Profiler; 25 import co.rsk.metrics.profilers.ProfilerFactory; 26 import co.rsk.panic.PanicProcessor; 27 import co.rsk.util.FormatUtils; 28 import co.rsk.validators.BlockValidator; 29 import com.google.common.annotations.VisibleForTesting; 30 import org.ethereum.core.*; 31 import org.ethereum.db.BlockInformation; 32 import org.ethereum.db.BlockStore; 33 import org.ethereum.db.ReceiptStore; 34 import org.ethereum.db.TransactionInfo; 35 import org.ethereum.listener.EthereumListener; 36 import org.slf4j.Logger; 37 import org.slf4j.LoggerFactory; 38  39 import javax.annotation.Nonnull; 40 import java.util.List; 41 import java.util.concurrent.locks.ReentrantReadWriteLock; 42  43 /** 44  * Created by ajlopez on 29/07/2016. 45  */ 46  47 /** 48  * Original comment: 49  * 50  * The Ethereum blockchain is in many ways similar to the Bitcoin blockchain, 51  * although it does have some differences. 52  * <p> 53  * The main difference between Ethereum and Bitcoin with regard to the blockchain architecture 54  * is that, unlike Bitcoin, Ethereum blocks contain a copy of both the transaction list 55  * and the most recent state. Aside from that, two other values, the block number and 56  * the difficulty, are also stored in the block. 57  * </p> 58  * The block validation algorithm in Ethereum is as follows: 59  * <ol> 60  * <li>Check if the previous block referenced exists and is valid.</li> 61  * <li>Check that the timestamp of the block is greater than that of the referenced previous block and less than 15 minutes into the future</li> 62  * <li>Check that the block number, difficulty, transaction root, uncle root and gas limit (various low-level Ethereum-specific concepts) are valid.</li> 63  * <li>Check that the proof of work on the block is valid.</li> 64  * <li>Let S[0] be the STATE_ROOT of the previous block.</li> 65  * <li>Let TX be the block's transaction list, with n transactions. 66  * For all in in 0...n-1, set S[i+1] = APPLY(S[i],TX[i]). 67  * If any applications returns an error, or if the total gas consumed in the block 68  * up until this point exceeds the GASLIMIT, return an error.</li> 69  * <li>Let S_FINAL be S[n], but adding the block reward paid to the miner.</li> 70  * <li>Check if S_FINAL is the same as the STATE_ROOT. If it is, the block is valid; otherwise, it is not valid.</li> 71  * </ol> 72  * See <a href="https://github.com/ethereum/wiki/wiki/White-Paper#blockchain-and-mining">Ethereum Whitepaper</a> 73  * 74  */ 75  76 public class BlockChainImpl implements Blockchain { 77  private static final Profiler profiler = ProfilerFactory.getInstance(); 78  private static final Logger logger = LoggerFactory.getLogger("blockchain"); 79  private static final PanicProcessor panicProcessor = new PanicProcessor(); 80  81  private final BlockStore blockStore; 82  private final ReceiptStore receiptStore; 83  private final TransactionPool transactionPool; 84  private final StateRootHandler stateRootHandler; 85  private final EthereumListener listener; 86  private BlockValidator blockValidator; 87  88  private volatile BlockChainStatus status = new BlockChainStatus(null, BlockDifficulty.ZERO); 89  90  private final Object connectLock = new Object(); 91  private final Object accessLock = new Object(); 92  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 93  94  private final BlockExecutor blockExecutor; 95  private boolean noValidation; 96  97  public BlockChainImpl(BlockStore blockStore, 98  ReceiptStore receiptStore, 99  TransactionPool transactionPool, 100  EthereumListener listener, 101  BlockValidator blockValidator, 102  BlockExecutor blockExecutor, 103  StateRootHandler stateRootHandler) { 104  this.blockStore = blockStore; 105  this.receiptStore = receiptStore; 106  this.listener = listener; 107  this.blockValidator = blockValidator; 108  this.blockExecutor = blockExecutor; 109  this.transactionPool = transactionPool; 110  this.stateRootHandler = stateRootHandler; 111  } 112  113  @VisibleForTesting 114  public void setBlockValidator(BlockValidator validator) { 115  this.blockValidator = validator; 116  } 117  118  @Override 119  public long getSize() { 120  return status.getBestBlock().getNumber() + 1; 121  } 122  123  /** 124  * Try to add a block to a blockchain 125  * 126  * @param block A block to try to add 127  * @return IMPORTED_BEST if the block is the new best block 128  * IMPORTED_NOT_BEST if it was added to alternative chain 129  * NO_PARENT the block parent is unknown yet 130  * INVALID_BLOCK the block has invalida data/state 131  * EXISTS the block was already processed 132  */ 133  @Override 134  public ImportResult tryToConnect(Block block) { 135  this.lock.readLock().lock(); 136  137  try { 138  if (block == null) { 139  return ImportResult.INVALID_BLOCK; 140  } 141  142  if (!block.isSealed()) { 143  panicProcessor.panic("unsealedblock", String.format("Unsealed block %s %s", block.getNumber(), block.getHash())); 144  block.seal(); 145  } 146  147  try { 148  org.slf4j.MDC.put("blockHash", block.getHash().toHexString()); 149  org.slf4j.MDC.put("blockHeight", Long.toString(block.getNumber())); 150  151  logger.trace("Try connect block hash: {}, number: {}", 152  block.getPrintableHash(), 153  block.getNumber()); 154  155  synchronized (connectLock) { 156  logger.trace("Start try connect"); 157  long saveTime = System.nanoTime(); 158  ImportResult result = internalTryToConnect(block); 159  long totalTime = System.nanoTime() - saveTime; 160  String timeInSeconds = FormatUtils.formatNanosecondsToSeconds(totalTime); 161  162  if (BlockUtils.tooMuchProcessTime(totalTime)) { 163  logger.warn("block: num: [{}] hash: [{}], processed after: [{}]seconds, result {}", block.getNumber(), block.getPrintableHash(), timeInSeconds, result); 164  } 165  else { 166  logger.info("block: num: [{}] hash: [{}], processed after: [{}]seconds, result {}", block.getNumber(), block.getPrintableHash(), timeInSeconds, result); 167  } 168  169  return result; 170  } 171  } catch (Throwable t) { 172  logger.error("Unexpected error: ", t); 173  return ImportResult.INVALID_BLOCK; 174  } 175  finally { 176  org.slf4j.MDC.remove("blockHash"); 177  org.slf4j.MDC.remove("blockHeight"); 178  179  } 180  } 181  finally { 182  this.lock.readLock().unlock(); 183  } 184  185  } 186  187  private ImportResult internalTryToConnect(Block block) { 188  Metric metric = profiler.start(Profiler.PROFILING_TYPE.BEFORE_BLOCK_EXEC); 189  190  if (blockStore.getBlockByHash(block.getHash().getBytes()) != null && 191  !BlockDifficulty.ZERO.equals(blockStore.getTotalDifficultyForHash(block.getHash().getBytes()))) { 192  logger.debug("Block already exist in chain hash: {}, number: {}", 193  block.getPrintableHash(), 194  block.getNumber()); 195  profiler.stop(metric); 196  return ImportResult.EXIST; 197  } 198  199  Block bestBlock; 200  BlockDifficulty bestTotalDifficulty; 201  202  logger.trace("get current state"); 203  204  // get current state 205  synchronized (accessLock) { 206  bestBlock = status.getBestBlock(); 207  bestTotalDifficulty = status.getTotalDifficulty(); 208  } 209  210  Block parent; 211  BlockDifficulty parentTotalDifficulty; 212  213  // Incoming block is child of current best block 214  if (bestBlock == null || bestBlock.isParentOf(block)) { 215  parent = bestBlock; 216  parentTotalDifficulty = bestTotalDifficulty; 217  } 218  // else, Get parent AND total difficulty 219  else { 220  logger.trace("get parent and total difficulty"); 221  parent = blockStore.getBlockByHash(block.getParentHash().getBytes()); 222  223  if (parent == null) { 224  profiler.stop(metric); 225  return ImportResult.NO_PARENT; 226  } 227  228  parentTotalDifficulty = blockStore.getTotalDifficultyForHash(parent.getHash().getBytes()); 229  230  if (parentTotalDifficulty == null || parentTotalDifficulty.equals(BlockDifficulty.ZERO)) { 231  profiler.stop(metric); 232  return ImportResult.NO_PARENT; 233  } 234  } 235  236  // Validate incoming block before its processing 237  if (!isValid(block)) { 238  long blockNumber = block.getNumber(); 239  logger.warn("Invalid block with number: {}", blockNumber); 240  panicProcessor.panic("invalidblock", String.format("Invalid block %s %s", blockNumber, block.getHash())); 241  profiler.stop(metric); 242  return ImportResult.INVALID_BLOCK; 243  } 244  245  profiler.stop(metric); 246  BlockResult result = null; 247  248  if (parent != null) { 249  long saveTime = System.nanoTime(); 250  logger.trace("execute start"); 251  252  result = blockExecutor.execute(block, parent.getHeader(), false, noValidation); 253  254  logger.trace("execute done"); 255  256  metric = profiler.start(Profiler.PROFILING_TYPE.AFTER_BLOCK_EXEC); 257  boolean isValid = noValidation ? true : blockExecutor.validate(block, result); 258  259  logger.trace("validate done"); 260  261  if (!isValid) { 262  profiler.stop(metric); 263  return ImportResult.INVALID_BLOCK; 264  } 265  // Now that we know it's valid, we can commit the changes made by the block 266  // to the parent's repository. 267  268  long totalTime = System.nanoTime() - saveTime; 269  String timeInSeconds = FormatUtils.formatNanosecondsToSeconds(totalTime); 270  271  if (BlockUtils.tooMuchProcessTime(totalTime)) { 272  logger.warn("block: num: [{}] hash: [{}], executed after: [{}]seconds", block.getNumber(), block.getPrintableHash(), timeInSeconds); 273  } 274  else { 275  logger.trace("block: num: [{}] hash: [{}], executed after: [{}]seconds", block.getNumber(), block.getPrintableHash(), timeInSeconds); 276  } 277  278  // the block is valid at this point 279  stateRootHandler.register(block.getHeader(), result.getFinalState()); 280  profiler.stop(metric); 281  } 282  283  metric = profiler.start(Profiler.PROFILING_TYPE.AFTER_BLOCK_EXEC); 284  // the new accumulated difficulty 285  BlockDifficulty totalDifficulty = parentTotalDifficulty.add(block.getCumulativeDifficulty()); 286  logger.trace("TD: updated to {}", totalDifficulty); 287  288  // It is the new best block 289  if (SelectionRule.shouldWeAddThisBlock(totalDifficulty, status.getTotalDifficulty(),block, bestBlock)) { 290  if (bestBlock != null && !bestBlock.isParentOf(block)) { 291  logger.trace("Rebranching: {} ~> {} From block {} ~> {} Difficulty {} Challenger difficulty {}", 292  bestBlock.getPrintableHash(), block.getPrintableHash(), bestBlock.getNumber(), block.getNumber(), 293  status.getTotalDifficulty(), totalDifficulty); 294  blockStore.reBranch(block); 295  } 296  297  logger.trace("Start switchToBlockChain"); 298  switchToBlockChain(block, totalDifficulty); 299  logger.trace("Start saveReceipts"); 300  saveReceipts(block, result); 301  logger.trace("Start processBest"); 302  processBest(block); 303  logger.trace("Start onBestBlock"); 304  onBestBlock(block, result); 305  logger.trace("Start onBlock"); 306  onBlock(block, result); 307  logger.trace("Start flushData"); 308  309  logger.trace("Better block {} {}", block.getNumber(), block.getPrintableHash()); 310  311  logger.debug("block added to the blockChain: index: [{}]", block.getNumber()); 312  if (block.getNumber() % 100 == 0) { 313  logger.info("*** Last block added [ #{} ]", block.getNumber()); 314  } 315  316  profiler.stop(metric); 317  return ImportResult.IMPORTED_BEST; 318  } 319  // It is not the new best block 320  else { 321  if (bestBlock != null && !bestBlock.isParentOf(block)) { 322  logger.trace("No rebranch: {} ~> {} From block {} ~> {} Difficulty {} Challenger difficulty {}", 323  bestBlock.getPrintableHash(), block.getPrintableHash(), bestBlock.getNumber(), block.getNumber(), 324  status.getTotalDifficulty(), totalDifficulty); 325  } 326  327  logger.trace("Start extendAlternativeBlockChain"); 328  extendAlternativeBlockChain(block, totalDifficulty); 329  logger.trace("Start saveReceipts"); 330  saveReceipts(block, result); 331  logger.trace("Start onBlock"); 332  onBlock(block, result); 333  logger.trace("Start flushData"); 334  335  if (bestBlock != null && block.getNumber() > bestBlock.getNumber()) { 336  logger.warn("Strange block number state"); 337  } 338  339  logger.trace("Block not imported {} {}", block.getNumber(), block.getPrintableHash()); 340  profiler.stop(metric); 341  return ImportResult.IMPORTED_NOT_BEST; 342  } 343  } 344  345  @Override 346  public BlockChainStatus getStatus() { 347  return status; 348  } 349  350  /** 351  * Change the blockchain status, to a new best block with difficulty 352  * 353  * @param block The new best block 354  * @param totalDifficulty The total difficulty of the new blockchain 355  */ 356  @Override 357  public void setStatus(Block block, BlockDifficulty totalDifficulty) { 358  synchronized (accessLock) { 359  status = new BlockChainStatus(block, totalDifficulty); 360  blockStore.saveBlock(block, totalDifficulty, true); 361  } 362  } 363  364  @Override 365  public Block getBlockByHash(byte[] hash) { 366  return blockStore.getBlockByHash(hash); 367  } 368  369  @Override 370  public List<Block> getBlocksByNumber(long number) { 371  return blockStore.getChainBlocksByNumber(number); 372  } 373  374  @Override 375  public List<BlockInformation> getBlocksInformationByNumber(long number) { 376  synchronized (accessLock) { 377  return this.blockStore.getBlocksInformationByNumber(number); 378  } 379  } 380  381  @Override 382  public boolean hasBlockInSomeBlockchain(@Nonnull final byte[] hash) { 383  final Block block = this.getBlockByHash(hash); 384  return block != null && this.blockIsInIndex(block); 385  } 386  387  /** 388  * blockIsInIndex returns true if a given block is indexed in the blockchain (it might not be the in the 389  * canonical branch). 390  * 391  * @param block the block to check for. 392  * @return true if there is a block in the blockchain with that hash. 393  */ 394  private boolean blockIsInIndex(@Nonnull final Block block) { 395  final List<Block> blocks = this.getBlocksByNumber(block.getNumber()); 396  397  return blocks.stream().anyMatch(block::fastEquals); 398  } 399  400  @Override 401  public void removeBlocksByNumber(long number) { 402  this.lock.writeLock().lock(); 403  404  try { 405  List<Block> blocks = this.getBlocksByNumber(number); 406  407  for (Block block : blocks) { 408  blockStore.removeBlock(block); 409  } 410  } 411  finally { 412  this.lock.writeLock().unlock(); 413  } 414  } 415  416  @Override 417  public Block getBlockByNumber(long number) { return blockStore.getChainBlockByNumber(number); } 418  419  @Override 420  public Block getBestBlock() { 421  return this.status.getBestBlock(); 422  } 423  424  public void setNoValidation(boolean noValidation) { 425  this.noValidation = noValidation; 426  } 427  428  /** 429  * Returns transaction info by hash 430  * 431  * @param hash the hash of the transaction 432  * @return transaction info, null if the transaction does not exist 433  */ 434  @Override 435  public TransactionInfo getTransactionInfo(byte[] hash) { 436  TransactionInfo txInfo = receiptStore.get(hash); 437  438  if (txInfo == null) { 439  return null; 440  } 441  442  Transaction tx = this.getBlockByHash(txInfo.getBlockHash()).getTransactionsList().get(txInfo.getIndex()); 443  txInfo.setTransaction(tx); 444  445  return txInfo; 446  } 447  448  @Override 449  public BlockDifficulty getTotalDifficulty() { 450  return status.getTotalDifficulty(); 451  } 452  453  @Override @VisibleForTesting 454  public byte[] getBestBlockHash() { 455  return getBestBlock().getHash().getBytes(); 456  } 457  458  private void switchToBlockChain(Block block, BlockDifficulty totalDifficulty) { 459  setStatus(block, totalDifficulty); 460  } 461  462  private void extendAlternativeBlockChain(Block block, BlockDifficulty totalDifficulty) { 463  storeBlock(block, totalDifficulty, false); 464  } 465  466  private void storeBlock(Block block, BlockDifficulty totalDifficulty, boolean inBlockChain) { 467  blockStore.saveBlock(block, totalDifficulty, inBlockChain); 468  logger.trace("Block saved: number: {}, hash: {}, TD: {}", 469  block.getNumber(), block.getPrintableHash(), totalDifficulty); 470  } 471  472  private void saveReceipts(Block block, BlockResult result) { 473  if (result == null) { 474  return; 475  } 476  477  if (result.getTransactionReceipts().isEmpty()) { 478  return; 479  } 480  481  receiptStore.saveMultiple(block.getHash().getBytes(), result.getTransactionReceipts()); 482  } 483  484  private void processBest(final Block block) { 485  logger.debug("Starting to run transactionPool.processBest(block)"); 486  // this has to happen in the same thread so the TransactionPool is immediately aware of the new best block 487  transactionPool.processBest(block); 488  logger.debug("Finished running transactionPool.processBest(block)"); 489  490  } 491  492  private void onBlock(Block block, BlockResult result) { 493  if (result != null && listener != null) { 494  listener.trace(String.format("Block chain size: [ %d ]", this.getSize())); 495  listener.onBlock(block, result.getTransactionReceipts()); 496  } 497  } 498  499  private void onBestBlock(Block block, BlockResult result) { 500  if (result != null && listener != null){ 501  listener.onBestBlock(block, result.getTransactionReceipts()); 502  } 503  } 504  505  private boolean isValid(Block block) { 506  Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_VALIDATION); 507  boolean validation = blockValidator.isValid(block); 508  profiler.stop(metric); 509  return validation; 510  } 511 }