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 }