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 }