Java tutorial
/* * This file is part of RskJ * Copyright (C) 2017 RSK Labs Ltd. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package co.rsk.mine; import co.rsk.bitcoinj.core.BtcBlock; import co.rsk.bitcoinj.core.BtcTransaction; import co.rsk.config.MiningConfig; import co.rsk.config.RskMiningConstants; import co.rsk.config.RskSystemProperties; import co.rsk.core.Coin; import co.rsk.core.RskAddress; import co.rsk.crypto.Keccak256; import co.rsk.net.BlockProcessor; import co.rsk.panic.PanicProcessor; import co.rsk.util.DifficultyUtils; import co.rsk.validators.ProofOfWorkRule; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.ArrayUtils; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.util.Arrays; import org.ethereum.config.BlockchainNetConfig; import org.ethereum.core.*; import org.ethereum.facade.Ethereum; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.rpc.TypeConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import java.math.BigDecimal; import java.math.BigInteger; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * The MinerServer provides support to components that perform the actual mining. * It builds blocks to mine and publishes blocks once a valid nonce was found by the miner. * * @author Oscar Guindzberg */ @Component("MinerServer") public class MinerServerImpl implements MinerServer { private static final long DELAY_BETWEEN_BUILD_BLOCKS_MS = TimeUnit.MINUTES.toMillis(1); private static final Logger logger = LoggerFactory.getLogger("minerserver"); private static final PanicProcessor panicProcessor = new PanicProcessor(); private static final int CACHE_SIZE = 20; private final Ethereum ethereum; private final Blockchain blockchain; private final ProofOfWorkRule powRule; private final BlockToMineBuilder builder; private final BlockchainNetConfig blockchainConfig; private final MinerClock clock; private Timer refreshWorkTimer; private NewBlockListener blockListener; private boolean started; private byte[] extraData; @GuardedBy("lock") private LinkedHashMap<Keccak256, Block> blocksWaitingforPoW; @GuardedBy("lock") private Keccak256 latestParentHash; @GuardedBy("lock") private Block latestBlock; @GuardedBy("lock") private Coin latestPaidFeesWithNotify; @GuardedBy("lock") private volatile MinerWork currentWork; // This variable can be read at anytime without the lock. private final Object lock = new Object(); private final RskAddress coinbaseAddress; private final BigDecimal minFeesNotifyInDollars; private final BigDecimal gasUnitInDollars; private final BlockProcessor nodeBlockProcessor; @Autowired public MinerServerImpl(RskSystemProperties config, Ethereum ethereum, Blockchain blockchain, BlockProcessor nodeBlockProcessor, ProofOfWorkRule powRule, BlockToMineBuilder builder, MinerClock clock, MiningConfig miningConfig) { this.ethereum = ethereum; this.blockchain = blockchain; this.nodeBlockProcessor = nodeBlockProcessor; this.powRule = powRule; this.builder = builder; this.clock = clock; this.blockchainConfig = config.getBlockchainConfig(); blocksWaitingforPoW = createNewBlocksWaitingList(); latestPaidFeesWithNotify = Coin.ZERO; latestParentHash = null; coinbaseAddress = miningConfig.getCoinbaseAddress(); minFeesNotifyInDollars = BigDecimal.valueOf(miningConfig.getMinFeesNotifyInDollars()); gasUnitInDollars = BigDecimal.valueOf(miningConfig.getMinFeesNotifyInDollars()); } private LinkedHashMap<Keccak256, Block> createNewBlocksWaitingList() { return new LinkedHashMap<Keccak256, Block>(CACHE_SIZE) { @Override protected boolean removeEldestEntry(Map.Entry<Keccak256, Block> eldest) { return size() > CACHE_SIZE; } }; } @VisibleForTesting public Map<Keccak256, Block> getBlocksWaitingforPoW() { return blocksWaitingforPoW; } @Override public boolean isRunning() { return started; } @Override public void stop() { if (!started) { return; } synchronized (lock) { started = false; ethereum.removeListener(blockListener); refreshWorkTimer.cancel(); refreshWorkTimer = null; } } @Override public void start() { if (started) { return; } synchronized (lock) { started = true; blockListener = new NewBlockListener(); ethereum.addListener(blockListener); buildBlockToMine(blockchain.getBestBlock(), false); if (refreshWorkTimer != null) { refreshWorkTimer.cancel(); } refreshWorkTimer = new Timer("Refresh work for mining"); refreshWorkTimer.schedule(new RefreshBlock(), DELAY_BETWEEN_BUILD_BLOCKS_MS, DELAY_BETWEEN_BUILD_BLOCKS_MS); } } @Override public SubmitBlockResult submitBitcoinBlockPartialMerkle(String blockHashForMergedMining, BtcBlock blockWithHeaderOnly, BtcTransaction coinbase, List<String> merkleHashes, int blockTxnCount) { logger.debug("Received merkle solution with hash {} for merged mining", blockHashForMergedMining); return processSolution(blockHashForMergedMining, blockWithHeaderOnly, coinbase, (pb) -> pb.buildFromMerkleHashes(blockWithHeaderOnly, merkleHashes, blockTxnCount), true); } @Override public SubmitBlockResult submitBitcoinBlockTransactions(String blockHashForMergedMining, BtcBlock blockWithHeaderOnly, BtcTransaction coinbase, List<String> txHashes) { logger.debug("Received tx solution with hash {} for merged mining", blockHashForMergedMining); return processSolution(blockHashForMergedMining, blockWithHeaderOnly, coinbase, (pb) -> pb.buildFromTxHashes(blockWithHeaderOnly, txHashes), true); } @Override public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock) { return submitBitcoinBlock(blockHashForMergedMining, bitcoinMergedMiningBlock, true); } SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock, boolean lastTag) { logger.debug("Received block with hash {} for merged mining", blockHashForMergedMining); return processSolution(blockHashForMergedMining, bitcoinMergedMiningBlock, bitcoinMergedMiningBlock.getTransactions().get(0), (pb) -> pb.buildFromBlock(bitcoinMergedMiningBlock), lastTag); } private SubmitBlockResult processSolution(String blockHashForMergedMining, BtcBlock blockWithHeaderOnly, BtcTransaction coinbase, Function<MerkleProofBuilder, byte[]> proofBuilderFunction, boolean lastTag) { Block newBlock; Keccak256 key = new Keccak256(TypeConverter.removeZeroX(blockHashForMergedMining)); synchronized (lock) { Block workingBlock = blocksWaitingforPoW.get(key); if (workingBlock == null) { String message = "Cannot publish block, could not find hash " + blockHashForMergedMining + " in the cache"; logger.warn(message); return new SubmitBlockResult("ERROR", message); } // clone the block newBlock = workingBlock.cloneBlock(); logger.debug("blocksWaitingForPoW size {}", blocksWaitingforPoW.size()); } logger.info("Received block {} {}", newBlock.getNumber(), newBlock.getHash()); newBlock.setBitcoinMergedMiningHeader(blockWithHeaderOnly.cloneAsHeader().bitcoinSerialize()); newBlock.setBitcoinMergedMiningCoinbaseTransaction(compressCoinbase(coinbase.bitcoinSerialize(), lastTag)); newBlock.setBitcoinMergedMiningMerkleProof( MinerUtils.buildMerkleProof(blockchainConfig, proofBuilderFunction, newBlock.getNumber())); newBlock.seal(); if (!isValid(newBlock)) { String message = "Invalid block supplied by miner: " + newBlock.getShortHash() + " " + newBlock.getShortHashForMergedMining() + " at height " + newBlock.getNumber(); logger.error(message); return new SubmitBlockResult("ERROR", message); } else { ImportResult importResult = ethereum.addNewMinedBlock(newBlock); logger.info("Mined block import result is {}: {} {} at height {}", importResult, newBlock.getShortHash(), newBlock.getShortHashForMergedMining(), newBlock.getNumber()); SubmittedBlockInfo blockInfo = new SubmittedBlockInfo(importResult, newBlock.getHash().getBytes(), newBlock.getNumber()); return new SubmitBlockResult("OK", "OK", blockInfo); } } private boolean isValid(Block block) { try { return powRule.isValid(block); } catch (Exception e) { logger.error("Failed to validate PoW from block {}: {}", block.getShortHash(), e); return false; } } public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized) { return compressCoinbase(bitcoinMergedMiningCoinbaseTransactionSerialized, true); } public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized, boolean lastOccurrence) { List<Byte> coinBaseTransactionSerializedAsList = java.util.Arrays .asList(ArrayUtils.toObject(bitcoinMergedMiningCoinbaseTransactionSerialized)); List<Byte> tagAsList = java.util.Arrays.asList(ArrayUtils.toObject(RskMiningConstants.RSK_TAG)); int rskTagPosition; if (lastOccurrence) { rskTagPosition = Collections.lastIndexOfSubList(coinBaseTransactionSerializedAsList, tagAsList); } else { rskTagPosition = Collections.indexOfSubList(coinBaseTransactionSerializedAsList, tagAsList); } int remainingByteCount = bitcoinMergedMiningCoinbaseTransactionSerialized.length - rskTagPosition - RskMiningConstants.RSK_TAG.length - RskMiningConstants.BLOCK_HEADER_HASH_SIZE; if (remainingByteCount > RskMiningConstants.MAX_BYTES_AFTER_MERGED_MINING_HASH) { throw new IllegalArgumentException("More than 128 bytes after RSK tag"); } int sha256Blocks = rskTagPosition / 64; int bytesToHash = sha256Blocks * 64; SHA256Digest digest = new SHA256Digest(); digest.update(bitcoinMergedMiningCoinbaseTransactionSerialized, 0, bytesToHash); byte[] hashedContent = digest.getEncodedState(); byte[] trimmedHashedContent = new byte[RskMiningConstants.MIDSTATE_SIZE_TRIMMED]; System.arraycopy(hashedContent, 8, trimmedHashedContent, 0, RskMiningConstants.MIDSTATE_SIZE_TRIMMED); byte[] unHashedContent = new byte[bitcoinMergedMiningCoinbaseTransactionSerialized.length - bytesToHash]; System.arraycopy(bitcoinMergedMiningCoinbaseTransactionSerialized, bytesToHash, unHashedContent, 0, unHashedContent.length); return Arrays.concatenate(trimmedHashedContent, unHashedContent); } @Override public RskAddress getCoinbaseAddress() { return coinbaseAddress; } /** * getWork returns the latest MinerWork for miners. Subsequent calls to this function with no new work will return * currentWork with the notify flag turned off. (they will be different objects too). * * This method must be called with MinerServer started. That and the fact that work is never set to null * will ensure that currentWork is not null. * * @return the latest MinerWork available. */ @Override public MinerWork getWork() { MinerWork work = currentWork; if (work.getNotify()) { /** * Set currentWork.notify to false for the next time this function is called. * By doing it this way, we avoid taking the lock every time, we just take it once per MinerWork. * We have to take the lock to reassign currentWork, but it might have happened that * the currentWork got updated when we acquired the lock. In that case, we should just return the new * currentWork, regardless of what it is. */ synchronized (lock) { if (currentWork != work) { return currentWork; } currentWork = new MinerWork(currentWork.getBlockHashForMergedMining(), currentWork.getTarget(), currentWork.getFeesPaidToMiner(), false, currentWork.getParentBlockHash()); } } return work; } @VisibleForTesting public void setWork(MinerWork work) { this.currentWork = work; } public MinerWork updateGetWork(@Nonnull final Block block, @Nonnull final boolean notify) { Keccak256 blockMergedMiningHash = new Keccak256(block.getHashForMergedMining()); BigInteger targetBI = DifficultyUtils.difficultyToTarget(block.getDifficulty()); byte[] targetUnknownLengthArray = targetBI.toByteArray(); byte[] targetArray = new byte[32]; System.arraycopy(targetUnknownLengthArray, 0, targetArray, 32 - targetUnknownLengthArray.length, targetUnknownLengthArray.length); logger.debug("Sending work for merged mining. Hash: {}", block.getShortHashForMergedMining()); return new MinerWork(blockMergedMiningHash.toJsonString(), TypeConverter.toJsonHex(targetArray), String.valueOf(block.getFeesPaidToMiner()), notify, block.getParentHashJsonString()); } public void setExtraData(byte[] extraData) { this.extraData = extraData; } /** * buildBlockToMine creates a block to mine based on the given block as parent. * * @param newBlockParent the new block parent. * @param createCompetitiveBlock used for testing. */ @Override public void buildBlockToMine(@Nonnull Block newBlockParent, boolean createCompetitiveBlock) { // See BlockChainImpl.calclBloom() if blocks has txs if (createCompetitiveBlock) { // Just for testing, mine on top of bestblock's parent newBlockParent = blockchain.getBlockByHash(newBlockParent.getParentHash().getBytes()); } logger.info("Starting block to mine from parent {} {}", newBlockParent.getNumber(), newBlockParent.getHash()); final Block newBlock = builder.build(newBlockParent, extraData); clock.clearIncreaseTime(); synchronized (lock) { Keccak256 parentHash = newBlockParent.getHash(); boolean notify = this.getNotify(newBlock, parentHash); if (notify) { latestPaidFeesWithNotify = newBlock.getFeesPaidToMiner(); } latestParentHash = parentHash; latestBlock = newBlock; currentWork = updateGetWork(newBlock, notify); Keccak256 latestBlockHashWaitingForPoW = new Keccak256(newBlock.getHashForMergedMining()); blocksWaitingforPoW.put(latestBlockHashWaitingForPoW, latestBlock); logger.debug("blocksWaitingForPoW size {}", blocksWaitingforPoW.size()); } logger.debug("Built block {}. Parent {}", newBlock.getShortHashForMergedMining(), newBlockParent.getShortHashForMergedMining()); for (BlockHeader uncleHeader : newBlock.getUncleList()) { logger.debug("With uncle {}", uncleHeader.getShortHashForMergedMining()); } } /** * getNotifies determines whether miners should be notified or not. (Used for mining pools). * * @param block the block to mine. * @param parentHash block's parent hash. * @return true if miners should be notified about this new block to mine. */ @GuardedBy("lock") private boolean getNotify(Block block, Keccak256 parentHash) { if (!parentHash.equals(latestParentHash)) { return true; } // note: integer divisions might truncate values BigInteger percentage = BigInteger.valueOf(100L + RskMiningConstants.NOTIFY_FEES_PERCENTAGE_INCREASE); Coin minFeesNotify = latestPaidFeesWithNotify.multiply(percentage).divide(BigInteger.valueOf(100L)); Coin feesPaidToMiner = block.getFeesPaidToMiner(); BigDecimal feesPaidToMinerInDollars = new BigDecimal(feesPaidToMiner.asBigInteger()) .multiply(gasUnitInDollars); return feesPaidToMiner.compareTo(minFeesNotify) > 0 && feesPaidToMinerInDollars.compareTo(minFeesNotifyInDollars) >= 0; } @Override public Optional<Block> getLatestBlock() { return Optional.ofNullable(latestBlock); } class NewBlockListener extends EthereumListenerAdapter { @Override /** * onBlock checks if we have to mine over a new block. (Only if the blockchain's best block changed). * This method will be called on every block added to the blockchain, even if it doesn't go to the best chain. * TODO(???): It would be cleaner to just send this when the blockchain's best block changes. * **/ // This event executes in the thread context of the caller. // In case of private miner, it's the "Private Mining timer" task public void onBlock(Block block, List<TransactionReceipt> receipts) { if (isSyncing()) { return; } logger.trace("Start onBlock"); Block bestBlock = blockchain.getBestBlock(); MinerWork work = currentWork; String bestBlockHash = bestBlock.getHashJsonString(); if (!work.getParentBlockHash().equals(bestBlockHash)) { logger.debug("There is a new best block: {}, number: {}", bestBlock.getShortHashForMergedMining(), bestBlock.getNumber()); buildBlockToMine(bestBlock, false); } else { logger.debug("New block arrived but there is no need to build a new block to mine: {}", block.getShortHashForMergedMining()); } logger.trace("End onBlock"); } private boolean isSyncing() { return nodeBlockProcessor.hasBetterBlockToSync(); } } /** * RefreshBlocks rebuilds the block to mine. */ private class RefreshBlock extends TimerTask { @Override public void run() { Block bestBlock = blockchain.getBestBlock(); try { buildBlockToMine(bestBlock, false); } catch (Throwable th) { logger.error("Unexpected error: {}", th); panicProcessor.panic("mserror", th.getMessage()); } } } }