/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library 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.
*
* The ethereumJ library 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 the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.mine;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.commons.collections4.CollectionUtils;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.*;
import org.ethereum.db.BlockStore;
import org.ethereum.db.ByteArrayWrapper;
import org.ethereum.db.IndexedBlockStore;
import org.ethereum.facade.Ethereum;
import org.ethereum.facade.EthereumImpl;
import org.ethereum.listener.CompositeEthereumListener;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.mine.MinerIfc.MiningResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.*;
import static java.lang.Math.max;
/**
* Manages embedded CPU mining and allows to use external miners.
*
* Created by Anton Nashatyrev on 10.12.2015.
*/
@Component
public class BlockMiner {
private static final Logger logger = LoggerFactory.getLogger("mine");
private static ExecutorService executor = Executors.newSingleThreadExecutor();
private Blockchain blockchain;
private BlockStore blockStore;
@Autowired
private Ethereum ethereum;
protected PendingState pendingState;
private CompositeEthereumListener listener;
private SystemProperties config;
private List<MinerListener> listeners = new CopyOnWriteArrayList<>();
private BigInteger minGasPrice;
private long minBlockTimeout;
private int cpuThreads;
private boolean fullMining = true;
private volatile boolean isLocalMining;
private Block miningBlock;
private volatile MinerIfc externalMiner;
private final Queue<ListenableFuture<MiningResult>> currentMiningTasks = new ConcurrentLinkedQueue<>();
private long lastBlockMinedTime;
private int UNCLE_LIST_LIMIT;
private int UNCLE_GENERATION_LIMIT;
@Autowired
public BlockMiner(final SystemProperties config, final CompositeEthereumListener listener,
final Blockchain blockchain, final BlockStore blockStore,
final PendingState pendingState) {
this.listener = listener;
this.config = config;
this.blockchain = blockchain;
this.blockStore = blockStore;
this.pendingState = pendingState;
UNCLE_LIST_LIMIT = config.getBlockchainConfig().getCommonConstants().getUNCLE_LIST_LIMIT();
UNCLE_GENERATION_LIMIT = config.getBlockchainConfig().getCommonConstants().getUNCLE_GENERATION_LIMIT();
minGasPrice = config.getMineMinGasPrice();
minBlockTimeout = config.getMineMinBlockTimeoutMsec();
cpuThreads = config.getMineCpuThreads();
fullMining = config.isMineFullDataset();
listener.addListener(new EthereumListenerAdapter() {
@Override
public void onPendingStateChanged(PendingState pendingState) {
BlockMiner.this.onPendingStateChanged();
}
@Override
public void onSyncDone(SyncState state) {
if (config.minerStart() && config.isSyncEnabled()) {
logger.info("Sync complete, start mining...");
startMining();
}
}
});
if (config.minerStart() && !config.isSyncEnabled()) {
logger.info("Sync disabled, start mining now...");
startMining();
}
}
public void setFullMining(boolean fullMining) {
this.fullMining = fullMining;
}
public void setCpuThreads(int cpuThreads) {
this.cpuThreads = cpuThreads;
}
public void setMinGasPrice(BigInteger minGasPrice) {
this.minGasPrice = minGasPrice;
}
public void setExternalMiner(MinerIfc miner) {
externalMiner = miner;
restartMining();
}
public void startMining() {
isLocalMining = true;
fireMinerStarted();
logger.info("Miner started");
restartMining();
}
public void stopMining() {
isLocalMining = false;
cancelCurrentBlock();
fireMinerStopped();
logger.info("Miner stopped");
}
protected List<Transaction> getAllPendingTransactions() {
PendingStateImpl.TransactionSortedSet ret = new PendingStateImpl.TransactionSortedSet();
ret.addAll(pendingState.getPendingTransactions());
Iterator<Transaction> it = ret.iterator();
while(it.hasNext()) {
Transaction tx = it.next();
if (!isAcceptableTx(tx)) {
logger.debug("Miner excluded the transaction: {}", tx);
it.remove();
}
}
return new ArrayList<>(ret);
}
private void onPendingStateChanged() {
if (!isLocalMining && externalMiner == null) return;
logger.debug("onPendingStateChanged()");
if (miningBlock == null) {
restartMining();
} else if (miningBlock.getNumber() <= ((PendingStateImpl) pendingState).getBestBlock().getNumber()) {
logger.debug("Restart mining: new best block: " + blockchain.getBestBlock().getShortDescr());
restartMining();
} else if (!CollectionUtils.isEqualCollection(miningBlock.getTransactionsList(), getAllPendingTransactions())) {
logger.debug("Restart mining: pending transactions changed");
restartMining();
} else {
if (logger.isDebugEnabled()) {
String s = "onPendingStateChanged() event, but pending Txs the same as in currently mining block: ";
for (Transaction tx : getAllPendingTransactions()) {
s += "\n " + tx;
}
logger.debug(s);
}
}
}
protected boolean isAcceptableTx(Transaction tx) {
return minGasPrice.compareTo(new BigInteger(1, tx.getGasPrice())) <= 0;
}
protected synchronized void cancelCurrentBlock() {
for (ListenableFuture<MiningResult> task : currentMiningTasks) {
if (task != null && !task.isCancelled()) {
task.cancel(true);
}
}
currentMiningTasks.clear();
if (miningBlock != null) {
fireBlockCancelled(miningBlock);
logger.debug("Tainted block mining cancelled: {}", miningBlock.getShortDescr());
miningBlock = null;
}
}
protected List<BlockHeader> getUncles(Block mineBest) {
List<BlockHeader> ret = new ArrayList<>();
long miningNum = mineBest.getNumber() + 1;
Block mineChain = mineBest;
long limitNum = max(0, miningNum - UNCLE_GENERATION_LIMIT);
Set<ByteArrayWrapper> ancestors = BlockchainImpl.getAncestors(blockStore, mineBest, UNCLE_GENERATION_LIMIT + 1, true);
Set<ByteArrayWrapper> knownUncles = ((BlockchainImpl)blockchain).getUsedUncles(blockStore, mineBest, true);
knownUncles.addAll(ancestors);
knownUncles.add(new ByteArrayWrapper(mineBest.getHash()));
if (blockStore instanceof IndexedBlockStore) {
outer:
while (mineChain.getNumber() > limitNum) {
List<Block> genBlocks = ((IndexedBlockStore) blockStore).getBlocksByNumber(mineChain.getNumber());
if (genBlocks.size() > 1) {
for (Block uncleCandidate : genBlocks) {
if (!knownUncles.contains(new ByteArrayWrapper(uncleCandidate.getHash())) &&
ancestors.contains(new ByteArrayWrapper(blockStore.getBlockByHash(uncleCandidate.getParentHash()).getHash()))) {
ret.add(uncleCandidate.getHeader());
if (ret.size() >= UNCLE_LIST_LIMIT) {
break outer;
}
}
}
}
mineChain = blockStore.getBlockByHash(mineChain.getParentHash());
}
} else {
logger.warn("BlockStore is not instance of IndexedBlockStore: miner can't include uncles");
}
return ret;
}
protected Block getNewBlockForMining() {
Block bestBlockchain = blockchain.getBestBlock();
Block bestPendingState = ((PendingStateImpl) pendingState).getBestBlock();
logger.debug("getNewBlockForMining best blocks: PendingState: " + bestPendingState.getShortDescr() +
", Blockchain: " + bestBlockchain.getShortDescr());
Block newMiningBlock = blockchain.createNewBlock(bestPendingState, getAllPendingTransactions(),
getUncles(bestPendingState));
return newMiningBlock;
}
protected void restartMining() {
Block newMiningBlock = getNewBlockForMining();
synchronized(this) {
cancelCurrentBlock();
miningBlock = newMiningBlock;
if (externalMiner != null) {
currentMiningTasks.add(externalMiner.mine(cloneBlock(miningBlock)));
}
if (isLocalMining) {
MinerIfc localMiner = config.getBlockchainConfig()
.getConfigForBlock(miningBlock.getNumber())
.getMineAlgorithm(config);
currentMiningTasks.add(localMiner.mine(cloneBlock(miningBlock)));
}
for (final ListenableFuture<MiningResult> task : currentMiningTasks) {
task.addListener(new Runnable() {
@Override
public void run() {
try {
// wow, block mined!
final Block minedBlock = task.get().block;
blockMined(minedBlock);
} catch (InterruptedException | CancellationException e) {
// OK, we've been cancelled, just exit
} catch (Exception e) {
logger.warn("Exception during mining: ", e);
}
}
}, MoreExecutors.sameThreadExecutor());
}
}
fireBlockStarted(newMiningBlock);
logger.debug("New block mining started: {}", newMiningBlock.getShortHash());
}
/**
* Block cloning is required before passing block to concurrent miner env.
* In success result miner will modify this block instance.
*/
private Block cloneBlock(Block block) {
return new Block(block.getEncoded());
}
protected void blockMined(Block newBlock) throws InterruptedException {
long t = System.currentTimeMillis();
if (t - lastBlockMinedTime < minBlockTimeout) {
long sleepTime = minBlockTimeout - (t - lastBlockMinedTime);
logger.debug("Last block was mined " + (t - lastBlockMinedTime) + " ms ago. Sleeping " +
sleepTime + " ms before importing...");
Thread.sleep(sleepTime);
}
fireBlockMined(newBlock);
logger.info("Wow, block mined !!!: {}", newBlock.toString());
lastBlockMinedTime = t;
miningBlock = null;
// cancel all tasks
cancelCurrentBlock();
// broadcast the block
logger.debug("Importing newly mined block {} {} ...", newBlock.getShortHash(), newBlock.getNumber());
ImportResult importResult = ((EthereumImpl) ethereum).addNewMinedBlock(newBlock);
logger.debug("Mined block import result is " + importResult);
}
public boolean isMining() {
return isLocalMining || externalMiner != null;
}
/***** Listener boilerplate ******/
public void addListener(MinerListener l) {
listeners.add(l);
}
public void removeListener(MinerListener l) {
listeners.remove(l);
}
protected void fireMinerStarted() {
for (MinerListener l : listeners) {
l.miningStarted();
}
}
protected void fireMinerStopped() {
for (MinerListener l : listeners) {
l.miningStopped();
}
}
protected void fireBlockStarted(Block b) {
for (MinerListener l : listeners) {
l.blockMiningStarted(b);
}
}
protected void fireBlockCancelled(Block b) {
for (MinerListener l : listeners) {
l.blockMiningCanceled(b);
}
}
protected void fireBlockMined(Block b) {
for (MinerListener l : listeners) {
l.blockMined(b);
}
}
}