Coverage Summary for Class: MiningMainchainViewImpl (co.rsk.core.bc)
Class |
Class, %
|
Method, %
|
Line, %
|
MiningMainchainViewImpl |
100%
(1/1)
|
54.5%
(6/11)
|
50.6%
(40/79)
|
1 /*
2 * This file is part of RskJ
3 * Copyright (C) 2019 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.crypto.Keccak256;
22 import org.ethereum.core.Block;
23 import org.ethereum.core.BlockHeader;
24 import org.ethereum.db.BlockStore;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import javax.annotation.concurrent.GuardedBy;
29 import java.util.*;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 public class MiningMainchainViewImpl implements MiningMainchainView {
34 private static final Logger logger = LoggerFactory.getLogger("miningmainchainview");
35
36 private final Object internalBlockStoreReadWriteLock = new Object();
37
38 private final int height;
39
40 private BlockStore blockStore;
41
42 @GuardedBy("internalBlockStoreReadWriteLock")
43 private Map<Keccak256, BlockHeader> blocksByHash;
44
45 @GuardedBy("internalBlockStoreReadWriteLock")
46 private Map<Long, List<Keccak256>> blockHashesByNumber;
47
48 @GuardedBy("internalBlockStoreReadWriteLock")
49 private List<BlockHeader> mainchain;
50
51 public MiningMainchainViewImpl(BlockStore blockStore,
52 int height) {
53 this.height = height;
54 this.blockStore = blockStore;
55 this.blocksByHash = new HashMap<>();
56 this.blockHashesByNumber = new HashMap<>();
57
58 BlockHeader currentBest = blockStore.getBestBlock().getHeader();
59 addHeaderToMaps(currentBest);
60 buildMainchainFromList(Arrays.asList(currentBest));
61 }
62
63 public void addBest(BlockHeader bestHeader) {
64 synchronized (internalBlockStoreReadWriteLock) {
65 addHeaderToMaps(bestHeader);
66
67 // try to avoid recalculating the whole chain if the new header's parent already exists in the chain
68 OptionalInt parentIndex = findParentIndex(bestHeader);
69 if (parentIndex.isPresent()) {
70 addBestAndRebuildFromParent(bestHeader, parentIndex.getAsInt());
71 } else {
72 buildMainchainFromList(Arrays.asList(bestHeader));
73 }
74
75 deleteEntriesOutOfBoundaries(bestHeader.getNumber());
76 }
77 }
78
79 @Override
80 public List<BlockHeader> get() {
81 synchronized (internalBlockStoreReadWriteLock) {
82 return Collections.unmodifiableList(mainchain);
83 }
84 }
85
86 /**
87 * Given a new best header and the index of its parent rebuild the mainchain using the index as the pivot point
88 * by discarding all headers subsequent to the it, setting the new header as the tip and refilling with as many
89 * are needed to complete the desired height
90 *
91 * @param bestHeader The best header to be on top of the chain
92 * @param parentIndex List index of the best header's parent
93 */
94 private void addBestAndRebuildFromParent(BlockHeader bestHeader, int parentIndex) {
95 List<BlockHeader> commonAncestorChain = mainchain.stream()
96 .skip(parentIndex)
97 .collect(Collectors.toList());
98
99 List<BlockHeader> newMainchain = Stream.concat(
100 Arrays.asList(bestHeader).stream(),
101 commonAncestorChain.stream())
102 .collect(Collectors.toList());
103
104 buildMainchainFromList(newMainchain);
105 }
106
107 /**
108 * Given a source list take it as the new mainchain and refill it with as many block headers are needed or trim it
109 * to reach the desired depth/height
110 *
111 * @param sourceList
112 */
113 private void buildMainchainFromList(List<BlockHeader> sourceList) {
114 int sourceSize = sourceList.size();
115
116 if (sourceSize == 0) {
117 return;
118 }
119
120 if (height < sourceSize) {
121 mainchain = sourceList.stream()
122 .limit(height)
123 .collect(Collectors.toList());
124
125 return;
126 }
127
128 BlockHeader lastHeader = sourceList.get(sourceSize - 1);
129
130 List<BlockHeader> missingHeaders = retrieveAncestorsForHeader(lastHeader, height - sourceSize);
131
132 for (BlockHeader header : missingHeaders) {
133 if(!blocksByHash.containsKey(header.getHash())) {
134 addHeaderToMaps(header);
135 }
136 }
137
138 mainchain = Stream.concat(sourceList.stream(), missingHeaders.stream())
139 .collect(Collectors.toList());
140 }
141
142 /**
143 * Given a start block header and a chain length, retrieve a List of said length consisting of
144 * the start header's ancestors
145 *
146 * The returned list DOES NOT include the start header
147 *
148 * @param header The block header to look the ancestors for
149 * @param chainLength The max length of the returned ancestor chain
150 */
151 private List<BlockHeader> retrieveAncestorsForHeader(BlockHeader header, int chainLength) {
152 List<BlockHeader> missingHeaders = new ArrayList<>(chainLength);
153
154 BlockHeader currentHeader = header;
155 for(int i = 0; i < chainLength; i++) {
156
157 // genesis has no parent
158 if(currentHeader.isGenesis()) {
159 break;
160 }
161
162 Block nextBlock = blockStore.getBlockByHash(currentHeader.getParentHash().getBytes());
163 if (nextBlock == null) {
164 logger.error("Missing parent for block %s, number %d", currentHeader.getPrintableHash(), currentHeader.getNumber());
165 break;
166 }
167 currentHeader = nextBlock.getHeader();
168
169 missingHeaders.add(currentHeader);
170 }
171
172 return missingHeaders;
173 }
174
175 private void addHeaderToMaps(BlockHeader header) {
176 blocksByHash.put(header.getHash(), header);
177 addToBlockHashesByNumberMap(header);
178 }
179
180 private void addToBlockHashesByNumberMap(BlockHeader headerToAdd) {
181 long blockNumber = headerToAdd.getNumber();
182 if (blockHashesByNumber.containsKey(blockNumber)) {
183 blockHashesByNumber.get(blockNumber).add(headerToAdd.getHash());
184 } else {
185 blockHashesByNumber.put(headerToAdd.getNumber(), new ArrayList<>(Collections.singletonList(headerToAdd.getHash())));
186 }
187 }
188
189 private void deleteEntriesOutOfBoundaries(long bestBlockNumber) {
190 long blocksHeightToDelete = bestBlockNumber - height;
191 if(blocksHeightToDelete >= 0 && blockHashesByNumber.containsKey(blocksHeightToDelete)) {
192 blockHashesByNumber.get(blocksHeightToDelete).forEach(blockHashToDelete -> blocksByHash.remove(blockHashToDelete));
193 blockHashesByNumber.remove(blocksHeightToDelete);
194 }
195 }
196
197 private OptionalInt findParentIndex(BlockHeader header) {
198 for (int i = 0; i < mainchain.size(); i++) {
199 BlockHeader chainHeader = mainchain.get(i);
200 if (chainHeader.isParentOf(header)) {
201 return OptionalInt.of(i);
202 }
203 }
204 return OptionalInt.empty();
205 }
206 }