Coverage Summary for Class: MinerServerImpl (co.rsk.mine)
Class |
Method, %
|
Line, %
|
MinerServerImpl |
0%
(0/30)
|
0%
(0/161)
|
MinerServerImpl$1 |
0%
(0/2)
|
0%
(0/2)
|
MinerServerImpl$NewBlockListener |
0%
(0/3)
|
0%
(0/10)
|
MinerServerImpl$RefreshBlock |
0%
(0/2)
|
0%
(0/6)
|
Total |
0%
(0/37)
|
0%
(0/179)
|
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.mine;
20
21 import co.rsk.bitcoinj.core.BtcBlock;
22 import co.rsk.bitcoinj.core.BtcTransaction;
23 import co.rsk.config.MiningConfig;
24 import co.rsk.config.RskMiningConstants;
25 import co.rsk.config.RskSystemProperties;
26 import co.rsk.core.Coin;
27 import co.rsk.core.RskAddress;
28 import co.rsk.core.bc.MiningMainchainView;
29 import co.rsk.crypto.Keccak256;
30 import co.rsk.net.BlockProcessor;
31 import co.rsk.panic.PanicProcessor;
32 import co.rsk.util.DifficultyUtils;
33 import co.rsk.util.ListArrayUtil;
34 import co.rsk.validators.ProofOfWorkRule;
35 import com.google.common.annotations.VisibleForTesting;
36 import org.bouncycastle.crypto.digests.SHA256Digest;
37 import org.bouncycastle.util.Arrays;
38 import org.ethereum.config.blockchain.upgrades.ActivationConfig;
39 import org.ethereum.core.*;
40 import org.ethereum.facade.Ethereum;
41 import org.ethereum.listener.EthereumListenerAdapter;
42 import org.ethereum.rpc.TypeConverter;
43 import org.ethereum.util.BuildInfo;
44 import org.ethereum.util.RLP;
45 import org.ethereum.util.RLPList;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import javax.annotation.Nonnull;
50 import javax.annotation.concurrent.GuardedBy;
51 import java.math.BigDecimal;
52 import java.math.BigInteger;
53 import java.util.*;
54 import java.util.concurrent.TimeUnit;
55 import java.util.function.Function;
56
57 /**
58 * The MinerServer provides support to components that perform the actual mining.
59 * It builds blocks to mine and publishes blocks once a valid nonce was found by the miner.
60 *
61 * @author Oscar Guindzberg
62 */
63
64 public class MinerServerImpl implements MinerServer {
65 private static final long DELAY_BETWEEN_BUILD_BLOCKS_MS = TimeUnit.MINUTES.toMillis(1);
66
67 private static final Logger logger = LoggerFactory.getLogger("minerserver");
68 private static final PanicProcessor panicProcessor = new PanicProcessor();
69
70 private static final int CACHE_SIZE = 20;
71
72 private static final int EXTRA_DATA_MAX_SIZE = 32;
73 private static final int EXTRA_DATA_VERSION = 1;
74
75 private final Ethereum ethereum;
76 private final MiningMainchainView mainchainView;
77 private final ProofOfWorkRule powRule;
78 private final BlockToMineBuilder builder;
79 private final ActivationConfig activationConfig;
80 private final MinerClock clock;
81 private final BlockFactory blockFactory;
82
83 private Timer refreshWorkTimer;
84 private NewBlockListener blockListener;
85
86 private boolean started;
87
88 private byte[] extraData;
89
90 @GuardedBy("lock")
91 private LinkedHashMap<Keccak256, Block> blocksWaitingforPoW;
92 @GuardedBy("lock")
93 private Keccak256 latestParentHash;
94 @GuardedBy("lock")
95 private Block latestBlock;
96 @GuardedBy("lock")
97 private Coin latestPaidFeesWithNotify;
98 @GuardedBy("lock")
99 private volatile MinerWork currentWork; // This variable can be read at anytime without the lock.
100 private final Object lock = new Object();
101
102 private final RskAddress coinbaseAddress;
103 private final BigDecimal minFeesNotifyInDollars;
104 private final BigDecimal gasUnitInDollars;
105
106 private final BlockProcessor nodeBlockProcessor;
107
108 public MinerServerImpl(
109 RskSystemProperties config,
110 Ethereum ethereum,
111 MiningMainchainView mainchainView,
112 BlockProcessor nodeBlockProcessor,
113 ProofOfWorkRule powRule,
114 BlockToMineBuilder builder,
115 MinerClock clock,
116 BlockFactory blockFactory,
117 BuildInfo buildInfo,
118 MiningConfig miningConfig) {
119 this.ethereum = ethereum;
120 this.mainchainView = mainchainView;
121 this.nodeBlockProcessor = nodeBlockProcessor;
122 this.powRule = powRule;
123 this.builder = builder;
124 this.clock = clock;
125 this.blockFactory = blockFactory;
126 this.activationConfig = config.getActivationConfig();
127
128 blocksWaitingforPoW = createNewBlocksWaitingList();
129
130 latestPaidFeesWithNotify = Coin.ZERO;
131 latestParentHash = null;
132 coinbaseAddress = miningConfig.getCoinbaseAddress();
133 minFeesNotifyInDollars = BigDecimal.valueOf(miningConfig.getMinFeesNotifyInDollars());
134 gasUnitInDollars = BigDecimal.valueOf(miningConfig.getGasUnitInDollars());
135
136 extraData = buildExtraData(config, buildInfo);
137 }
138
139 private byte[] buildExtraData(RskSystemProperties config, BuildInfo buildInfo) {
140 String identity = config.projectVersionModifier() + "-" + buildInfo.getBuildHash();
141 return RLP.encodeList(RLP.encodeElement(RLP.encodeInt(EXTRA_DATA_VERSION)), RLP.encodeString(identity));
142 }
143
144 private LinkedHashMap<Keccak256, Block> createNewBlocksWaitingList() {
145 return new LinkedHashMap<Keccak256, Block>(CACHE_SIZE) {
146 @Override
147 protected boolean removeEldestEntry(Map.Entry<Keccak256, Block> eldest) {
148 return size() > CACHE_SIZE;
149 }
150 };
151
152 }
153
154 @VisibleForTesting
155 public Map<Keccak256, Block> getBlocksWaitingforPoW() {
156 return blocksWaitingforPoW;
157 }
158
159 @Override
160 public boolean isRunning() {
161 return started;
162 }
163
164 @Override
165 public void stop() {
166 if (!started) {
167 return;
168 }
169
170 synchronized (lock) {
171 started = false;
172 ethereum.removeListener(blockListener);
173 if (refreshWorkTimer != null) {
174 refreshWorkTimer.cancel();
175 refreshWorkTimer = null;
176 }
177 }
178 }
179
180 @Override
181 public void start() {
182 if (started) {
183 return;
184 }
185
186 synchronized (lock) {
187 started = true;
188 blockListener = new NewBlockListener();
189 ethereum.addListener(blockListener);
190 buildBlockToMine(false);
191
192 if (refreshWorkTimer != null) {
193 refreshWorkTimer.cancel();
194 }
195
196 refreshWorkTimer = new Timer("Refresh work for mining");
197 refreshWorkTimer.schedule(new RefreshBlock(), DELAY_BETWEEN_BUILD_BLOCKS_MS, DELAY_BETWEEN_BUILD_BLOCKS_MS);
198 }
199 }
200
201 @Override
202 public SubmitBlockResult submitBitcoinBlockPartialMerkle(
203 String blockHashForMergedMining,
204 BtcBlock blockWithHeaderOnly,
205 BtcTransaction coinbase,
206 List<String> merkleHashes,
207 int blockTxnCount) {
208 logger.debug("Received merkle solution with hash {} for merged mining", blockHashForMergedMining);
209
210 return processSolution(
211 blockHashForMergedMining,
212 blockWithHeaderOnly,
213 coinbase,
214 (pb) -> pb.buildFromMerkleHashes(blockWithHeaderOnly, merkleHashes, blockTxnCount),
215 true
216 );
217 }
218
219 @Override
220 public SubmitBlockResult submitBitcoinBlockTransactions(
221 String blockHashForMergedMining,
222 BtcBlock blockWithHeaderOnly,
223 BtcTransaction coinbase,
224 List<String> txHashes) {
225 logger.debug("Received tx solution with hash {} for merged mining", blockHashForMergedMining);
226
227 return processSolution(
228 blockHashForMergedMining,
229 blockWithHeaderOnly,
230 coinbase,
231 (pb) -> pb.buildFromTxHashes(blockWithHeaderOnly, txHashes),
232 true
233 );
234 }
235
236 @Override
237 public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock) {
238 return submitBitcoinBlock(blockHashForMergedMining, bitcoinMergedMiningBlock, true);
239 }
240
241 SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock, boolean lastTag) {
242 logger.debug("Received block with hash {} for merged mining", blockHashForMergedMining);
243
244 return processSolution(
245 blockHashForMergedMining,
246 bitcoinMergedMiningBlock,
247 bitcoinMergedMiningBlock.getTransactions().get(0),
248 (pb) -> pb.buildFromBlock(bitcoinMergedMiningBlock),
249 lastTag
250 );
251 }
252
253 private SubmitBlockResult processSolution(
254 String blockHashForMergedMining,
255 BtcBlock blockWithHeaderOnly,
256 BtcTransaction coinbase,
257 Function<MerkleProofBuilder, byte[]> proofBuilderFunction,
258 boolean lastTag) {
259 Block newBlock;
260 Keccak256 key = new Keccak256(TypeConverter.removeZeroX(blockHashForMergedMining));
261
262 synchronized (lock) {
263 Block workingBlock = blocksWaitingforPoW.get(key);
264
265 if (workingBlock == null) {
266 String message = "Cannot publish block, could not find hash " + blockHashForMergedMining + " in the cache";
267 logger.warn(message);
268
269 return new SubmitBlockResult("ERROR", message);
270 }
271
272 newBlock = blockFactory.cloneBlockForModification(workingBlock);
273
274 logger.debug("blocksWaitingForPoW size {}", blocksWaitingforPoW.size());
275 }
276
277 logger.info("Received block {} {}", newBlock.getNumber(), newBlock.getHash());
278
279 newBlock.setBitcoinMergedMiningHeader(blockWithHeaderOnly.cloneAsHeader().bitcoinSerialize());
280 newBlock.setBitcoinMergedMiningCoinbaseTransaction(compressCoinbase(coinbase.bitcoinSerialize(), lastTag));
281 newBlock.setBitcoinMergedMiningMerkleProof(MinerUtils.buildMerkleProof(activationConfig, proofBuilderFunction, newBlock.getNumber()));
282 newBlock.seal();
283
284 if (!isValid(newBlock)) {
285 String message = "Invalid block supplied by miner: " + newBlock.getPrintableHash() + " " + newBlock.getPrintableHashForMergedMining() + " at height " + newBlock.getNumber();
286 logger.error(message);
287
288 return new SubmitBlockResult("ERROR", message);
289 } else {
290 ImportResult importResult = ethereum.addNewMinedBlock(newBlock);
291
292 logger.info("Mined block import result is {}: {} {} at height {}", importResult, newBlock.getPrintableHash(), newBlock.getPrintableHashForMergedMining(), newBlock.getNumber());
293 SubmittedBlockInfo blockInfo = new SubmittedBlockInfo(importResult, newBlock.getHash().getBytes(), newBlock.getNumber());
294
295 return new SubmitBlockResult("OK", "OK", blockInfo);
296 }
297 }
298
299 private boolean isValid(Block block) {
300 try {
301 return powRule.isValid(block);
302 } catch (Exception e) {
303 logger.error("Failed to validate PoW from block {}: {}", block.getPrintableHash(), e);
304 return false;
305 }
306 }
307
308 public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized) {
309 return compressCoinbase(bitcoinMergedMiningCoinbaseTransactionSerialized, true);
310 }
311
312 public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized, boolean lastOccurrence) {
313 List<Byte> coinBaseTransactionSerializedAsList = ListArrayUtil.asByteList(bitcoinMergedMiningCoinbaseTransactionSerialized);
314 List<Byte> tagAsList = ListArrayUtil.asByteList(RskMiningConstants.RSK_TAG);
315
316 int rskTagPosition;
317 if (lastOccurrence) {
318 rskTagPosition = Collections.lastIndexOfSubList(coinBaseTransactionSerializedAsList, tagAsList);
319 } else {
320 rskTagPosition = Collections.indexOfSubList(coinBaseTransactionSerializedAsList, tagAsList);
321 }
322
323 int sha256Blocks = rskTagPosition / 64;
324 int bytesToHash = sha256Blocks * 64;
325 SHA256Digest digest = new SHA256Digest();
326 digest.update(bitcoinMergedMiningCoinbaseTransactionSerialized, 0, bytesToHash);
327 byte[] hashedContent = digest.getEncodedState();
328 byte[] trimmedHashedContent = new byte[RskMiningConstants.MIDSTATE_SIZE_TRIMMED];
329 System.arraycopy(hashedContent, 8, trimmedHashedContent, 0, RskMiningConstants.MIDSTATE_SIZE_TRIMMED);
330 byte[] unHashedContent = new byte[bitcoinMergedMiningCoinbaseTransactionSerialized.length - bytesToHash];
331 System.arraycopy(bitcoinMergedMiningCoinbaseTransactionSerialized, bytesToHash, unHashedContent, 0, unHashedContent.length);
332 return Arrays.concatenate(trimmedHashedContent, unHashedContent);
333 }
334
335 @Override
336 public RskAddress getCoinbaseAddress() {
337 return coinbaseAddress;
338 }
339
340 /**
341 * getWork returns the latest MinerWork for miners. Subsequent calls to this function with no new work will return
342 * currentWork with the notify flag turned off. (they will be different objects too).
343 *
344 * This method must be called with MinerServer started. That and the fact that work is never set to null
345 * will ensure that currentWork is not null.
346 *
347 * @return the latest MinerWork available.
348 */
349 @Override
350 public MinerWork getWork() {
351 MinerWork work = currentWork;
352
353 if (work.getNotify()) {
354 /**
355 * Set currentWork.notify to false for the next time this function is called.
356 * By doing it this way, we avoid taking the lock every time, we just take it once per MinerWork.
357 * We have to take the lock to reassign currentWork, but it might have happened that
358 * the currentWork got updated when we acquired the lock. In that case, we should just return the new
359 * currentWork, regardless of what it is.
360 */
361 synchronized (lock) {
362 if (currentWork != work) {
363 return currentWork;
364 }
365 currentWork = new MinerWork(currentWork.getBlockHashForMergedMining(), currentWork.getTarget(),
366 currentWork.getFeesPaidToMiner(), false, currentWork.getParentBlockHash());
367 }
368 }
369 return work;
370 }
371
372 @VisibleForTesting
373 public void setWork(MinerWork work) {
374 this.currentWork = work;
375 }
376
377 public MinerWork updateGetWork(@Nonnull final Block block, @Nonnull final boolean notify) {
378 Keccak256 blockMergedMiningHash = new Keccak256(block.getHashForMergedMining());
379
380 BigInteger targetBI = DifficultyUtils.difficultyToTarget(block.getDifficulty());
381 byte[] targetUnknownLengthArray = targetBI.toByteArray();
382 byte[] targetArray = new byte[32];
383 System.arraycopy(targetUnknownLengthArray, 0, targetArray, 32 - targetUnknownLengthArray.length, targetUnknownLengthArray.length);
384
385 logger.debug("Sending work for merged mining. Hash: {}", block.getPrintableHashForMergedMining());
386 return new MinerWork(blockMergedMiningHash.toJsonString(), TypeConverter.toJsonHex(targetArray), String.valueOf(block.getFeesPaidToMiner()), notify, block.getParentHashJsonString());
387 }
388
389 public void setExtraData(byte[] clientExtraData) {
390 RLPList decodedExtraData = RLP.decodeList(this.extraData);
391 byte[] version = decodedExtraData.get(0).getRLPData();
392 byte[] identity = decodedExtraData.get(1).getRLPData();
393
394 int rlpClientExtraDataEncodingOverhead = 3;
395 int clientExtraDataSize = EXTRA_DATA_MAX_SIZE
396 - (version != null ? version.length : 0)
397 - (identity != null ? identity.length : 0)
398 - rlpClientExtraDataEncodingOverhead;
399 byte[] clientExtraDataResized = Arrays.copyOf(clientExtraData, Math.min(clientExtraData.length, clientExtraDataSize));
400
401 this.extraData = RLP.encodeList(version, RLP.encode(identity), RLP.encodeElement(clientExtraDataResized));
402 }
403
404 @VisibleForTesting
405 public byte[] getExtraData() {
406 return Arrays.copyOf(extraData, extraData.length);
407 }
408
409 /**
410 * buildBlockToMine creates a block to mine using the block received as parent.
411 * This method calls buildBlockToMine and that one uses the internal mainchainView
412 * Hence, mainchainView must be updated to reflect the new mainchain status.
413 * Note. This method is NOT intended to be used in any part of the mining flow and
414 * is only here to be consumed from SnapshotManager.
415 *
416 * @param blockToMineOnTopOf parent of the block to be built.
417 * @param createCompetitiveBlock used for testing.
418 */
419 @Override
420 public void buildBlockToMine(@Nonnull Block blockToMineOnTopOf, boolean createCompetitiveBlock) {
421 mainchainView.addBest(blockToMineOnTopOf.getHeader());
422 buildBlockToMine(createCompetitiveBlock);
423 }
424
425 /**
426 * buildBlockToMine creates a block to mine using the current best block as parent.
427 * best block is obtained from a blockchain view that has the latest mainchain blocks.
428 *
429 * @param createCompetitiveBlock used for testing.
430 */
431 @Override
432 public void buildBlockToMine(boolean createCompetitiveBlock) {
433 BlockHeader newBlockParentHeader = mainchainView.get().get(0);
434 // See BlockChainImpl.calclBloom() if blocks has txs
435 if (createCompetitiveBlock) {
436 // Just for testing, mine on top of best block's parent
437 newBlockParentHeader = mainchainView.get().get(1);
438 }
439
440 logger.info("Starting block to mine from parent {} {}", newBlockParentHeader.getNumber(), newBlockParentHeader.getHash());
441
442 List<BlockHeader> mainchainHeaders = mainchainView.get();
443 final Block newBlock = builder.build(mainchainHeaders, extraData).getBlock();
444 clock.clearIncreaseTime();
445
446 synchronized (lock) {
447 Keccak256 parentHash = newBlockParentHeader.getHash();
448 boolean notify = this.getNotify(newBlock, parentHash);
449
450 if (notify) {
451 latestPaidFeesWithNotify = newBlock.getFeesPaidToMiner();
452 }
453
454 latestParentHash = parentHash;
455 latestBlock = newBlock;
456
457 currentWork = updateGetWork(newBlock, notify);
458 Keccak256 latestBlockHashWaitingForPoW = new Keccak256(newBlock.getHashForMergedMining());
459
460 blocksWaitingforPoW.put(latestBlockHashWaitingForPoW, latestBlock);
461 logger.debug("blocksWaitingForPoW size {}", blocksWaitingforPoW.size());
462 }
463
464 logger.debug("Built block {}. Parent {}", newBlock.getPrintableHashForMergedMining(), newBlockParentHeader.getPrintableHashForMergedMining());
465 for (BlockHeader uncleHeader : newBlock.getUncleList()) {
466 logger.debug("With uncle {}", uncleHeader.getPrintableHashForMergedMining());
467 }
468 }
469
470 /**
471 * getNotifies determines whether miners should be notified or not. (Used for mining pools).
472 *
473 * @param block the block to mine.
474 * @param parentHash block's parent hash.
475 * @return true if miners should be notified about this new block to mine.
476 */
477 @GuardedBy("lock")
478 private boolean getNotify(Block block, Keccak256 parentHash) {
479 if (!parentHash.equals(latestParentHash)) {
480 return true;
481 }
482
483 // note: integer divisions might truncate values
484 BigInteger percentage = BigInteger.valueOf(100L + RskMiningConstants.NOTIFY_FEES_PERCENTAGE_INCREASE);
485 Coin minFeesNotify = latestPaidFeesWithNotify.multiply(percentage).divide(BigInteger.valueOf(100L));
486 Coin feesPaidToMiner = block.getFeesPaidToMiner();
487 BigDecimal feesPaidToMinerInDollars = new BigDecimal(feesPaidToMiner.asBigInteger()).multiply(gasUnitInDollars);
488 return feesPaidToMiner.compareTo(minFeesNotify) > 0
489 && feesPaidToMinerInDollars.compareTo(minFeesNotifyInDollars) >= 0;
490
491 }
492
493 @Override
494 public Optional<Block> getLatestBlock() {
495 return Optional.ofNullable(latestBlock);
496 }
497
498 class NewBlockListener extends EthereumListenerAdapter {
499
500 @Override
501 // This event executes in the thread context of the caller.
502 // In case of private miner, it's the "Private Mining timer" task
503 public void onBestBlock(Block newBestBlock, List<TransactionReceipt> receipts) {
504 if (isSyncing()) {
505 return;
506 }
507
508 logger.trace("Start onBestBlock");
509
510 logger.debug(
511 "There is a new best block: {}, number: {}",
512 newBestBlock.getPrintableHashForMergedMining(),
513 newBestBlock.getNumber());
514 mainchainView.addBest(newBestBlock.getHeader());
515 buildBlockToMine(false);
516
517 logger.trace("End onBestBlock");
518 }
519
520 private boolean isSyncing() {
521 return nodeBlockProcessor.hasBetterBlockToSync();
522 }
523 }
524
525 /**
526 * RefreshBlocks rebuilds the block to mine.
527 */
528 private class RefreshBlock extends TimerTask {
529 @Override
530 public void run() {
531 try {
532 buildBlockToMine(false);
533 } catch (Throwable th) {
534 logger.error("Unexpected error: {}", th);
535 panicProcessor.panic("mserror", th.getMessage());
536 }
537 }
538 }
539 }