Coverage Summary for Class: RepositoryBtcBlockStoreWithCache (co.rsk.peg)
Class |
Method, %
|
Line, %
|
RepositoryBtcBlockStoreWithCache |
0%
(0/18)
|
0%
(0/137)
|
RepositoryBtcBlockStoreWithCache$Factory |
66.7%
(2/3)
|
78.6%
(11/14)
|
Total |
9.5%
(2/21)
|
7.3%
(11/151)
|
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.peg;
20
21 import co.rsk.bitcoinj.core.BtcBlock;
22 import co.rsk.bitcoinj.core.NetworkParameters;
23 import co.rsk.bitcoinj.core.Sha256Hash;
24 import co.rsk.bitcoinj.core.StoredBlock;
25 import co.rsk.bitcoinj.store.BlockStoreException;
26 import co.rsk.config.BridgeConstants;
27 import co.rsk.core.RskAddress;
28 import co.rsk.util.MaxSizeHashMap;
29 import java.util.Optional;
30 import com.google.common.annotations.VisibleForTesting;
31 import org.ethereum.config.blockchain.upgrades.ActivationConfig;
32 import org.ethereum.config.blockchain.upgrades.ConsensusRule;
33 import org.ethereum.config.blockchain.upgrades.ActivationConfig.ForBlock;
34 import org.ethereum.core.Repository;
35 import org.ethereum.vm.DataWord;
36 import org.ethereum.vm.PrecompiledContracts;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import java.nio.ByteBuffer;
41 import java.util.Map;
42
43 /**
44 * Implementation of a bitcoinj blockstore that persists to RSK's Repository
45 *
46 * @author Oscar Guindzberg
47 */
48 public class RepositoryBtcBlockStoreWithCache implements BtcBlockStoreWithCache {
49
50 private static final Logger logger = LoggerFactory.getLogger(RepositoryBtcBlockStoreWithCache.class);
51
52 private static final String BLOCK_STORE_CHAIN_HEAD_KEY = "blockStoreChainHead";
53 private static final int DEFAULT_MAX_DEPTH_BLOCK_CACHE = 5_000;
54 private static final int DEFAULT_MAX_SIZE_BLOCK_CACHE = 10_000;
55
56 private final Repository repository;
57 private final RskAddress contractAddress;
58 private final NetworkParameters btcNetworkParams;
59 private final BridgeConstants bridgeConstants;
60 private final BridgeStorageProvider bridgeStorageProvider;
61 private final ActivationConfig.ForBlock activations;
62 private final int maxDepthBlockCache;
63 private final Map<Sha256Hash, StoredBlock> cacheBlocks;
64
65 public RepositoryBtcBlockStoreWithCache(
66 NetworkParameters btcNetworkParams,
67 Repository repository,
68 Map<Sha256Hash, StoredBlock> cacheBlocks,
69 RskAddress contractAddress,
70 BridgeConstants bridgeConstants,
71 BridgeStorageProvider bridgeStorageProvider,
72 ForBlock activations) {
73
74 this(
75 btcNetworkParams,
76 repository,
77 cacheBlocks,
78 contractAddress,
79 bridgeConstants,
80 bridgeStorageProvider,
81 activations,
82 DEFAULT_MAX_DEPTH_BLOCK_CACHE
83 );
84 }
85
86 public RepositoryBtcBlockStoreWithCache(
87 NetworkParameters btcNetworkParams,
88 Repository repository,
89 Map<Sha256Hash, StoredBlock> cacheBlocks,
90 RskAddress contractAddress,
91 BridgeConstants bridgeConstants,
92 BridgeStorageProvider bridgeStorageProvider,
93 ForBlock activations,
94 int maxDepthBlockCache) {
95
96 this.cacheBlocks = cacheBlocks;
97 this.repository = repository;
98 this.contractAddress = contractAddress;
99 this.btcNetworkParams = btcNetworkParams;
100 this.bridgeConstants = bridgeConstants;
101 this.bridgeStorageProvider = bridgeStorageProvider;
102 this.activations = activations;
103 this.maxDepthBlockCache = maxDepthBlockCache;
104
105 checkIfInitialized();
106 }
107
108 @Override
109 public synchronized void put(StoredBlock storedBlock) {
110 Sha256Hash hash = storedBlock.getHeader().getHash();
111 byte[] ba = storedBlockToByteArray(storedBlock);
112 repository.addStorageBytes(contractAddress, DataWord.valueFromHex(hash.toString()), ba);
113 if (cacheBlocks != null) {
114 StoredBlock chainHead = getChainHead();
115 if (chainHead == null || chainHead.getHeight() - storedBlock.getHeight() < this.maxDepthBlockCache) {
116 cacheBlocks.put(storedBlock.getHeader().getHash(), storedBlock);
117 }
118 }
119 }
120
121 @Override
122 public synchronized StoredBlock get(Sha256Hash hash) {
123 logger.trace("[get] Looking in storage for block with hash {}", hash);
124 byte[] ba = repository.getStorageBytes(contractAddress, DataWord.valueFromHex(hash.toString()));
125 if (ba == null) {
126 logger.trace("[get] Block with hash {} not found in storage", hash);
127 return null;
128 }
129 StoredBlock storedBlock = byteArrayToStoredBlock(ba);
130 return storedBlock;
131 }
132
133 @Override
134 public synchronized StoredBlock getChainHead() {
135 byte[] ba = repository.getStorageBytes(contractAddress, DataWord.fromString(BLOCK_STORE_CHAIN_HEAD_KEY));
136 if (ba == null) {
137 return null;
138 }
139 return byteArrayToStoredBlock(ba);
140 }
141
142 @Override
143 public synchronized void setChainHead(StoredBlock newChainHead) {
144 logger.trace("Set new chain head with height: {}.", newChainHead.getHeight());
145 byte[] ba = storedBlockToByteArray(newChainHead);
146 repository.addStorageBytes(contractAddress, DataWord.fromString(BLOCK_STORE_CHAIN_HEAD_KEY), ba);
147 if (cacheBlocks != null) {
148 populateCache(newChainHead);
149 }
150 setMainChainBlock(newChainHead.getHeight(), newChainHead.getHeader().getHash());
151 }
152
153 @Override
154 public Optional<StoredBlock> getInMainchain(int height) {
155 Optional<Sha256Hash> bestBlockHash = bridgeStorageProvider.getBtcBestBlockHashByHeight(height);
156 if (!bestBlockHash.isPresent()) {
157 logger.trace("[getInMainchain] Block at height {} not present in storage", height);
158 return Optional.empty();
159 }
160
161 StoredBlock block = get(bestBlockHash.get());
162 if (block == null) {
163 logger.trace("[getInMainchain] Block with hash {} not found in storage", bestBlockHash.get());
164 return Optional.empty();
165 }
166
167 logger.trace("[getInMainchain] Found block with hash {} at height {}", bestBlockHash.get(), height);
168 return Optional.of(block);
169 }
170
171 @Override
172 public void setMainChainBlock(int height, Sha256Hash blockHash) {
173 logger.trace("[setMainChainBlock] Set block with hash {} at height {}", blockHash, height);
174 bridgeStorageProvider.setBtcBestBlockHashByHeight(height, blockHash);
175 }
176
177 @Override
178 public void close() {
179 }
180
181 @Override
182 public NetworkParameters getParams() {
183 return btcNetworkParams;
184 }
185
186 @Override
187 public StoredBlock getFromCache(Sha256Hash branchBlockHash) {
188 if (cacheBlocks == null) {
189 logger.trace("[getFromCache] Block with hash {} not found in cache", branchBlockHash);
190 return null;
191 }
192 return cacheBlocks.get(branchBlockHash);
193 }
194
195 @Override
196 public StoredBlock getStoredBlockAtMainChainHeight(int height) throws BlockStoreException {
197 StoredBlock chainHead = getChainHead();
198 int depth = chainHead.getHeight() - height;
199 logger.trace("Getting btc block at depth: {}", depth);
200
201 if (depth < 0) {
202 String message = String.format(
203 "Height provided is higher than chain head. provided: %n. chain head: %n",
204 height,
205 chainHead.getHeight()
206 );
207 logger.trace("[getStoredBlockAtMainChainHeight] {}", message);
208 throw new BlockStoreException(message);
209 }
210
211 if (activations.isActive(ConsensusRule.RSKIP199)) {
212 int btcHeightWhenBlockIndexActivates = this.bridgeConstants.getBtcHeightWhenBlockIndexActivates();
213 int maxDepthToSearch = this.bridgeConstants.getMaxDepthToSearchBlocksBelowIndexActivation();
214 int limit;
215 if (chainHead.getHeight() - btcHeightWhenBlockIndexActivates > maxDepthToSearch) {
216 limit = btcHeightWhenBlockIndexActivates;
217 } else {
218 limit = chainHead.getHeight() - maxDepthToSearch;
219 }
220 logger.trace("[getStoredBlockAtMainChainHeight] Chain head height is {} and the depth limit {}", chainHead.getHeight(), limit);
221
222 if (height < limit) {
223 String message = String.format(
224 "Height provided is lower than the depth limit defined to search for blocks. Provided: %n, limit: %n",
225 height,
226 limit
227 );
228 logger.trace("[getStoredBlockAtMainChainHeight] {}", message);
229 throw new BlockStoreException(message);
230 }
231 }
232
233 StoredBlock block;
234 Optional<StoredBlock> blockOptional = getInMainchain(height);
235 if (blockOptional.isPresent()) {
236 block = blockOptional.get();
237 } else {
238 block = getStoredBlockAtMainChainDepth(depth);
239 }
240
241 return block;
242 }
243
244 private synchronized void populateCache(StoredBlock chainHead) {
245 logger.trace("Populating BTC Block Store Cache.");
246 if (this.btcNetworkParams.getGenesisBlock().equals(chainHead.getHeader())) {
247 return;
248 }
249 cacheBlocks.put(chainHead.getHeader().getHash(), chainHead);
250 Sha256Hash blockHash = chainHead.getHeader().getPrevBlockHash();
251 int depth = this.maxDepthBlockCache - 1;
252 while (blockHash != null && depth > 0) {
253 if (cacheBlocks.get(blockHash) != null) {
254 break;
255 }
256 StoredBlock currentBlock = get(blockHash);
257 if (currentBlock == null) {
258 break;
259 }
260 cacheBlocks.put(currentBlock.getHeader().getHash(), currentBlock);
261 depth--;
262 blockHash = currentBlock.getHeader().getPrevBlockHash();
263 }
264 logger.trace("END Populating BTC Block Store Cache.");
265 }
266
267 @Override
268 @Deprecated
269 public StoredBlock getStoredBlockAtMainChainDepth(int depth) throws BlockStoreException {
270 logger.trace("[getStoredBlockAtMainChainDepth] Looking for block at depth {}", depth);
271 StoredBlock chainHead = getChainHead();
272 Sha256Hash blockHash = chainHead.getHeader().getHash();
273
274 for (int i = 0; i < depth && blockHash != null; i++) {
275 //If its older than cache go to disk
276 StoredBlock currentBlock = getFromCache(blockHash);
277 if (currentBlock == null) {
278 logger.trace("[getStoredBlockAtMainChainDepth] Block with hash {} not in cache, getting from disk", blockHash);
279 currentBlock = get(blockHash);
280 if (currentBlock == null) {
281 return null;
282 }
283 }
284 blockHash = currentBlock.getHeader().getPrevBlockHash();
285 }
286
287 if (blockHash == null) {
288 logger.trace("[getStoredBlockAtMainChainDepth] Block not found");
289 return null;
290 }
291 StoredBlock block = getFromCache(blockHash);
292 if (block == null) {
293 block = get(blockHash);
294 }
295 int expectedHeight = chainHead.getHeight() - depth;
296 if (block != null && block.getHeight() != expectedHeight) {
297 String message = String.format("Block %s at depth %d Height is %d but should be %d",
298 block.getHeader().getHash(),
299 depth,
300 block.getHeight(),
301 expectedHeight
302 );
303 logger.trace("[getStoredBlockAtMainChainDepth] {}", message);
304 throw new BlockStoreException(message);
305 }
306
307 return block;
308 }
309
310 private byte[] storedBlockToByteArray(StoredBlock block) {
311 ByteBuffer byteBuffer = ByteBuffer.allocate(128);
312 block.serializeCompact(byteBuffer);
313 byte[] ba = new byte[byteBuffer.position()];
314 byteBuffer.flip();
315 byteBuffer.get(ba);
316 return ba;
317 }
318
319 private StoredBlock byteArrayToStoredBlock(byte[] ba) {
320 ByteBuffer byteBuffer = ByteBuffer.wrap(ba);
321 return StoredBlock.deserializeCompact(btcNetworkParams, byteBuffer);
322 }
323
324 private void checkIfInitialized() {
325 if (getChainHead() == null) {
326 BtcBlock genesisHeader = this.btcNetworkParams.getGenesisBlock().cloneAsHeader();
327 StoredBlock storedGenesis = new StoredBlock(genesisHeader, genesisHeader.getWork(), 0);
328 put(storedGenesis);
329 setChainHead(storedGenesis);
330 }
331 }
332
333 public static class Factory implements BtcBlockStoreWithCache.Factory {
334
335 private final int maxSizeBlockCache;
336 //This is ok as we don't have parallel execution, in the feature we should move to a concurrentHashMap
337 private final Map<Sha256Hash, StoredBlock> cacheBlocks;
338 private final RskAddress contractAddress;
339 private final NetworkParameters btcNetworkParams;
340 private final int maxDepthBlockCache;
341
342 @VisibleForTesting
343 public Factory(NetworkParameters btcNetworkParams) {
344 this(btcNetworkParams, DEFAULT_MAX_DEPTH_BLOCK_CACHE, DEFAULT_MAX_SIZE_BLOCK_CACHE);
345 }
346
347 public Factory(NetworkParameters btcNetworkParams, int maxDepthBlockCache, int maxSizeBlockCache) {
348 this.contractAddress = PrecompiledContracts.BRIDGE_ADDR;
349 this.btcNetworkParams = btcNetworkParams;
350 this.maxDepthBlockCache = maxDepthBlockCache;
351 this.maxSizeBlockCache = maxSizeBlockCache;
352 this.cacheBlocks = new MaxSizeHashMap<>(this.maxSizeBlockCache, true);
353
354 if (this.maxDepthBlockCache > this.maxSizeBlockCache) {
355 logger.warn("Max depth ({}) is greater than Max Size ({}). This could lead to a misbehaviour.", this.maxDepthBlockCache, this.maxSizeBlockCache);
356 }
357
358 if (this.maxDepthBlockCache < DEFAULT_MAX_DEPTH_BLOCK_CACHE) {
359 logger.warn("Max depth ({}) is lower than the default ({}). This could lead to a misbehaviour.", this.maxDepthBlockCache, DEFAULT_MAX_DEPTH_BLOCK_CACHE);
360 }
361 }
362
363 @Override
364 public BtcBlockStoreWithCache newInstance(
365 Repository track,
366 BridgeConstants bridgeConstants,
367 BridgeStorageProvider bridgeStorageProvider,
368 ActivationConfig.ForBlock activations) {
369
370 return new RepositoryBtcBlockStoreWithCache(
371 btcNetworkParams,
372 track,
373 cacheBlocks,
374 contractAddress,
375 bridgeConstants,
376 bridgeStorageProvider,
377 activations,
378 this.maxDepthBlockCache
379 );
380 }
381 }
382 }