/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.crypto.Crypto; import nxt.util.Convert; import nxt.util.Listener; import nxt.util.Listeners; import nxt.util.Logger; import nxt.util.ThreadPool; import java.math.BigInteger; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; public final class Generator implements Comparable<Generator> { public enum Event { GENERATION_DEADLINE, START_FORGING, STOP_FORGING } private static final int MAX_FORGERS = Nxt.getIntProperty("nxt.maxNumberOfForgers"); private static final byte[] fakeForgingPublicKey; static { byte[] publicKey = null; if (Nxt.getBooleanProperty("nxt.enableFakeForging")) { Account fakeForgingAccount = Account.getAccount(Convert.parseAccountId(Nxt.getStringProperty("nxt.fakeForgingAccount"))); if (fakeForgingAccount != null) { publicKey = fakeForgingAccount.getPublicKey(); } } fakeForgingPublicKey = publicKey; } private static final Listeners<Generator,Event> listeners = new Listeners<>(); private static final ConcurrentMap<String, Generator> generators = new ConcurrentHashMap<>(); private static final Collection<Generator> allGenerators = Collections.unmodifiableCollection(generators.values()); private static volatile List<Generator> sortedForgers = null; private static long lastBlockId; private static int delayTime = Constants.FORGING_DELAY; private static final Runnable generateBlocksThread = new Runnable() { private volatile boolean logged; @Override public void run() { try { try { BlockchainImpl.getInstance().updateLock(); try { Block lastBlock = Nxt.getBlockchain().getLastBlock(); if (lastBlock == null || lastBlock.getHeight() < Constants.LAST_KNOWN_BLOCK) { return; } if (lastBlock.getId() != lastBlockId || sortedForgers == null) { lastBlockId = lastBlock.getId(); List<Generator> forgers = new ArrayList<>(); for (Generator generator : generators.values()) { generator.setLastBlock(lastBlock); if (generator.effectiveBalance.signum() > 0) { forgers.add(generator); } } Collections.sort(forgers); sortedForgers = Collections.unmodifiableList(forgers); logged = false; } int generationLimit = Nxt.getEpochTime() - delayTime; if (!logged) { for (Generator generator : sortedForgers) { if (generator.getHitTime() - generationLimit > 60) { break; } Logger.logDebugMessage(generator.toString()); logged = true; } } for (Generator generator : sortedForgers) { if (generator.getHitTime() > generationLimit || generator.forge(lastBlock, generationLimit)) { return; } } } finally { BlockchainImpl.getInstance().updateUnlock(); } } catch (Exception e) { Logger.logMessage("Error in block generation thread", e); } } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString()); t.printStackTrace(); System.exit(1); } } }; static { ThreadPool.scheduleThread("GenerateBlocks", generateBlocksThread, 500, TimeUnit.MILLISECONDS); } static void init() {} public static boolean addListener(Listener<Generator> listener, Event eventType) { return listeners.addListener(listener, eventType); } public static boolean removeListener(Listener<Generator> listener, Event eventType) { return listeners.removeListener(listener, eventType); } public static Generator startForging(String secretPhrase) { if (generators.size() >= MAX_FORGERS) { throw new RuntimeException("Cannot forge with more than " + MAX_FORGERS + " accounts on the same node"); } Generator generator = new Generator(secretPhrase); Generator old = generators.putIfAbsent(secretPhrase, generator); if (old != null) { Logger.logDebugMessage(old + " is already forging"); return old; } listeners.notify(generator, Event.START_FORGING); Logger.logDebugMessage(generator + " started"); return generator; } public static Generator stopForging(String secretPhrase) { Generator generator = generators.remove(secretPhrase); if (generator != null) { sortedForgers = null; Logger.logDebugMessage(generator + " stopped"); listeners.notify(generator, Event.STOP_FORGING); } return generator; } public static int stopForging() { int count = generators.size(); Iterator<Generator> iter = generators.values().iterator(); while (iter.hasNext()) { Generator generator = iter.next(); iter.remove(); Logger.logDebugMessage(generator + " stopped"); listeners.notify(generator, Event.STOP_FORGING); } sortedForgers = null; return count; } public static Generator getGenerator(String secretPhrase) { return generators.get(secretPhrase); } public static int getGeneratorCount() { return generators.size(); } public static Collection<Generator> getAllGenerators() { return allGenerators; } public static List<Generator> getSortedForgers() { return sortedForgers == null ? Collections.<Generator>emptyList() : sortedForgers; } public static long getNextHitTime(long lastBlockId, int curTime) { BlockchainImpl.getInstance().readLock(); try { if (lastBlockId == Generator.lastBlockId && sortedForgers != null) { for (Generator generator : sortedForgers) { if (generator.getHitTime() >= curTime - Constants.FORGING_DELAY) { return generator.getHitTime(); } } } return 0; } finally { BlockchainImpl.getInstance().readUnlock(); } } static void setDelay(int delay) { Generator.delayTime = delay; } static boolean verifyHit(BigInteger hit, BigInteger effectiveBalance, Block previousBlock, int timestamp) { int elapsedTime = timestamp - previousBlock.getTimestamp(); if (elapsedTime <= 0) { return false; } BigInteger effectiveBaseTarget = BigInteger.valueOf(previousBlock.getBaseTarget()).multiply(effectiveBalance); BigInteger prevTarget = effectiveBaseTarget.multiply(BigInteger.valueOf(elapsedTime - 1)); BigInteger target = prevTarget.add(effectiveBaseTarget); return hit.compareTo(target) < 0 && (previousBlock.getHeight() < Constants.TRANSPARENT_FORGING_BLOCK_8 || hit.compareTo(prevTarget) >= 0 || (Constants.isTestnet ? elapsedTime > 300 : elapsedTime > 3600) || Constants.isOffline); } static long getHitTime(Account account, Block block) { return getHitTime(BigInteger.valueOf(account.getEffectiveBalanceNXT(block.getHeight())), getHit(account.getPublicKey(), block), block); } static boolean allowsFakeForging(byte[] publicKey) { return Constants.isTestnet && publicKey != null && Arrays.equals(publicKey, fakeForgingPublicKey); } static BigInteger getHit(byte[] publicKey, Block block) { if (allowsFakeForging(publicKey)) { return BigInteger.ZERO; } if (block.getHeight() < Constants.TRANSPARENT_FORGING_BLOCK) { throw new IllegalArgumentException("Not supported below Transparent Forging Block"); } MessageDigest digest = Crypto.sha256(); digest.update(block.getGenerationSignature()); byte[] generationSignatureHash = digest.digest(publicKey); return new BigInteger(1, new byte[] {generationSignatureHash[7], generationSignatureHash[6], generationSignatureHash[5], generationSignatureHash[4], generationSignatureHash[3], generationSignatureHash[2], generationSignatureHash[1], generationSignatureHash[0]}); } static long getHitTime(BigInteger effectiveBalance, BigInteger hit, Block block) { return block.getTimestamp() + hit.divide(BigInteger.valueOf(block.getBaseTarget()).multiply(effectiveBalance)).longValue(); } private final long accountId; private final String secretPhrase; private final byte[] publicKey; private volatile long hitTime; private volatile BigInteger hit; private volatile BigInteger effectiveBalance; private Generator(String secretPhrase) { this.secretPhrase = secretPhrase; this.publicKey = Crypto.getPublicKey(secretPhrase); this.accountId = Account.getId(publicKey); if (Nxt.getBlockchain().getHeight() >= Constants.LAST_KNOWN_BLOCK) { setLastBlock(Nxt.getBlockchain().getLastBlock()); } sortedForgers = null; } public byte[] getPublicKey() { return publicKey; } public long getAccountId() { return accountId; } public long getDeadline() { return Math.max(hitTime - Nxt.getBlockchain().getLastBlock().getTimestamp(), 0); } public long getHitTime() { return hitTime; } @Override public int compareTo(Generator g) { int i = this.hit.multiply(g.effectiveBalance).compareTo(g.hit.multiply(this.effectiveBalance)); if (i != 0) { return i; } return Long.compare(accountId, g.accountId); } @Override public String toString() { return "Forger " + Long.toUnsignedString(accountId) + " deadline " + getDeadline() + " hit " + hitTime; } private void setLastBlock(Block lastBlock) { Account account = Account.getAccount(accountId); effectiveBalance = BigInteger.valueOf(account == null || account.getEffectiveBalanceNXT() <= 0 ? 0 : account.getEffectiveBalanceNXT()); if (effectiveBalance.signum() == 0) { return; } hit = getHit(publicKey, lastBlock); hitTime = getHitTime(effectiveBalance, hit, lastBlock); listeners.notify(this, Event.GENERATION_DEADLINE); } boolean forge(Block lastBlock, int generationLimit) throws BlockchainProcessor.BlockNotAcceptedException { int timestamp = (generationLimit - hitTime > 3600) ? generationLimit : (int)hitTime + 1; if (!verifyHit(hit, effectiveBalance, lastBlock, timestamp)) { Logger.logDebugMessage(this.toString() + " failed to forge at " + timestamp); return false; } int start = Nxt.getEpochTime(); while (true) { try { BlockchainProcessorImpl.getInstance().generateBlock(secretPhrase, timestamp); setDelay(Constants.FORGING_DELAY); return true; } catch (BlockchainProcessor.TransactionNotAcceptedException e) { // the bad transaction has been expunged, try again if (Nxt.getEpochTime() - start > 10) { // give up after trying for 10 s throw e; } } } } }