/****************************************************************************** * 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.AccountLedger.LedgerEvent; import nxt.crypto.Crypto; import nxt.util.Convert; import nxt.util.Logger; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; final class BlockImpl implements Block { private final int version; private final int timestamp; private final long previousBlockId; private volatile byte[] generatorPublicKey; private final byte[] previousBlockHash; private final long totalAmountNQT; private final long totalFeeNQT; private final int payloadLength; private final byte[] generationSignature; private final byte[] payloadHash; private volatile List<TransactionImpl> blockTransactions; private byte[] blockSignature; private BigInteger cumulativeDifficulty = BigInteger.ZERO; private long baseTarget = Constants.INITIAL_BASE_TARGET; private volatile long nextBlockId; private int height = -1; private volatile long id; private volatile String stringId = null; private volatile long generatorId; private volatile byte[] bytes = null; BlockImpl(int version, int timestamp, long previousBlockId, long totalAmountNQT, long totalFeeNQT, int payloadLength, byte[] payloadHash, byte[] generatorPublicKey, byte[] generationSignature, byte[] previousBlockHash, List<TransactionImpl> transactions, String secretPhrase) { this(version, timestamp, previousBlockId, totalAmountNQT, totalFeeNQT, payloadLength, payloadHash, generatorPublicKey, generationSignature, null, previousBlockHash, transactions); blockSignature = Crypto.sign(bytes(), secretPhrase); bytes = null; } BlockImpl(int version, int timestamp, long previousBlockId, long totalAmountNQT, long totalFeeNQT, int payloadLength, byte[] payloadHash, byte[] generatorPublicKey, byte[] generationSignature, byte[] blockSignature, byte[] previousBlockHash, List<TransactionImpl> transactions) { this.version = version; this.timestamp = timestamp; this.previousBlockId = previousBlockId; this.totalAmountNQT = totalAmountNQT; this.totalFeeNQT = totalFeeNQT; this.payloadLength = payloadLength; this.payloadHash = payloadHash; this.generatorPublicKey = generatorPublicKey; this.generationSignature = generationSignature; this.blockSignature = blockSignature; this.previousBlockHash = previousBlockHash; if (transactions != null) { this.blockTransactions = Collections.unmodifiableList(transactions); } } BlockImpl(int version, int timestamp, long previousBlockId, long totalAmountNQT, long totalFeeNQT, int payloadLength, byte[] payloadHash, long generatorId, byte[] generationSignature, byte[] blockSignature, byte[] previousBlockHash, BigInteger cumulativeDifficulty, long baseTarget, long nextBlockId, int height, long id, List<TransactionImpl> blockTransactions) { this(version, timestamp, previousBlockId, totalAmountNQT, totalFeeNQT, payloadLength, payloadHash, null, generationSignature, blockSignature, previousBlockHash, null); this.cumulativeDifficulty = cumulativeDifficulty; this.baseTarget = baseTarget; this.nextBlockId = nextBlockId; this.height = height; this.id = id; this.generatorId = generatorId; this.blockTransactions = blockTransactions; } @Override public int getVersion() { return version; } @Override public int getTimestamp() { return timestamp; } @Override public long getPreviousBlockId() { return previousBlockId; } @Override public byte[] getGeneratorPublicKey() { if (generatorPublicKey == null) { generatorPublicKey = Account.getPublicKey(generatorId); } return generatorPublicKey; } @Override public byte[] getPreviousBlockHash() { return previousBlockHash; } @Override public long getTotalAmountNQT() { return totalAmountNQT; } @Override public long getTotalFeeNQT() { return totalFeeNQT; } @Override public int getPayloadLength() { return payloadLength; } @Override public byte[] getPayloadHash() { return payloadHash; } @Override public byte[] getGenerationSignature() { return generationSignature; } @Override public byte[] getBlockSignature() { return blockSignature; } @Override public List<TransactionImpl> getTransactions() { if (blockTransactions == null) { this.blockTransactions = Collections.unmodifiableList(TransactionDb.findBlockTransactions(getId())); for (TransactionImpl transaction : this.blockTransactions) { transaction.setBlock(this); } } return blockTransactions; } @Override public long getBaseTarget() { return baseTarget; } @Override public BigInteger getCumulativeDifficulty() { return cumulativeDifficulty; } @Override public long getNextBlockId() { return nextBlockId; } @Override public int getHeight() { if (height == -1) { throw new IllegalStateException("Block height not yet set"); } return height; } @Override public long getId() { if (id == 0) { if (blockSignature == null) { throw new IllegalStateException("Block is not signed yet"); } byte[] hash = Crypto.sha256().digest(bytes()); BigInteger bigInteger = new BigInteger(1, new byte[] {hash[7], hash[6], hash[5], hash[4], hash[3], hash[2], hash[1], hash[0]}); id = bigInteger.longValue(); stringId = bigInteger.toString(); } return id; } @Override public String getStringId() { if (stringId == null) { getId(); if (stringId == null) { stringId = Long.toUnsignedString(id); } } return stringId; } @Override public long getGeneratorId() { if (generatorId == 0) { generatorId = Account.getId(getGeneratorPublicKey()); } return generatorId; } @Override public boolean equals(Object o) { return o instanceof BlockImpl && this.getId() == ((BlockImpl)o).getId(); } @Override public int hashCode() { return (int)(getId() ^ (getId() >>> 32)); } @Override public JSONObject getJSONObject() { JSONObject json = new JSONObject(); json.put("version", version); json.put("timestamp", timestamp); json.put("previousBlock", Long.toUnsignedString(previousBlockId)); json.put("totalAmountNQT", totalAmountNQT); json.put("totalFeeNQT", totalFeeNQT); json.put("payloadLength", payloadLength); json.put("payloadHash", Convert.toHexString(payloadHash)); json.put("generatorPublicKey", Convert.toHexString(getGeneratorPublicKey())); json.put("generationSignature", Convert.toHexString(generationSignature)); if (version > 1) { json.put("previousBlockHash", Convert.toHexString(previousBlockHash)); } json.put("blockSignature", Convert.toHexString(blockSignature)); JSONArray transactionsData = new JSONArray(); getTransactions().forEach(transaction -> transactionsData.add(transaction.getJSONObject())); json.put("transactions", transactionsData); return json; } static BlockImpl parseBlock(JSONObject blockData) throws NxtException.NotValidException { try { int version = ((Long) blockData.get("version")).intValue(); int timestamp = ((Long) blockData.get("timestamp")).intValue(); long previousBlock = Convert.parseUnsignedLong((String) blockData.get("previousBlock")); long totalAmountNQT = Convert.parseLong(blockData.get("totalAmountNQT")); long totalFeeNQT = Convert.parseLong(blockData.get("totalFeeNQT")); int payloadLength = ((Long) blockData.get("payloadLength")).intValue(); byte[] payloadHash = Convert.parseHexString((String) blockData.get("payloadHash")); byte[] generatorPublicKey = Convert.parseHexString((String) blockData.get("generatorPublicKey")); byte[] generationSignature = Convert.parseHexString((String) blockData.get("generationSignature")); byte[] blockSignature = Convert.parseHexString((String) blockData.get("blockSignature")); byte[] previousBlockHash = version == 1 ? null : Convert.parseHexString((String) blockData.get("previousBlockHash")); List<TransactionImpl> blockTransactions = new ArrayList<>(); for (Object transactionData : (JSONArray) blockData.get("transactions")) { blockTransactions.add(TransactionImpl.parseTransaction((JSONObject) transactionData)); } BlockImpl block = new BlockImpl(version, timestamp, previousBlock, totalAmountNQT, totalFeeNQT, payloadLength, payloadHash, generatorPublicKey, generationSignature, blockSignature, previousBlockHash, blockTransactions); if (!block.checkSignature()) { throw new NxtException.NotValidException("Invalid block signature"); } return block; } catch (NxtException.NotValidException|RuntimeException e) { Logger.logDebugMessage("Failed to parse block: " + blockData.toJSONString()); throw e; } } @Override public byte[] getBytes() { return Arrays.copyOf(bytes(), bytes.length); } byte[] bytes() { if (bytes == null) { ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + 8 + 4 + (version < 3 ? (4 + 4) : (8 + 8)) + 4 + 32 + 32 + (32 + 32) + (blockSignature != null ? 64 : 0)); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(version); buffer.putInt(timestamp); buffer.putLong(previousBlockId); buffer.putInt(getTransactions().size()); if (version < 3) { buffer.putInt((int) (totalAmountNQT / Constants.ONE_NXT)); buffer.putInt((int) (totalFeeNQT / Constants.ONE_NXT)); } else { buffer.putLong(totalAmountNQT); buffer.putLong(totalFeeNQT); } buffer.putInt(payloadLength); buffer.put(payloadHash); buffer.put(getGeneratorPublicKey()); buffer.put(generationSignature); if (version > 1) { buffer.put(previousBlockHash); } if (blockSignature != null) { buffer.put(blockSignature); } bytes = buffer.array(); } return bytes; } boolean verifyBlockSignature() { return checkSignature() && Account.setOrVerify(getGeneratorId(), getGeneratorPublicKey()); } private volatile boolean hasValidSignature = false; private boolean checkSignature() { if (! hasValidSignature) { byte[] data = Arrays.copyOf(bytes(), bytes.length - 64); hasValidSignature = blockSignature != null && Crypto.verify(blockSignature, data, getGeneratorPublicKey(), version >= 3); } return hasValidSignature; } boolean verifyGenerationSignature() throws BlockchainProcessor.BlockOutOfOrderException { try { BlockImpl previousBlock = BlockchainImpl.getInstance().getBlock(getPreviousBlockId()); if (previousBlock == null) { throw new BlockchainProcessor.BlockOutOfOrderException("Can't verify signature because previous block is missing", this); } if (version == 1 && !Crypto.verify(generationSignature, previousBlock.generationSignature, getGeneratorPublicKey(), version >= 3)) { return false; } Account account = Account.getAccount(getGeneratorId()); long effectiveBalance = account == null ? 0 : account.getEffectiveBalanceNXT(); if (effectiveBalance <= 0) { return false; } MessageDigest digest = Crypto.sha256(); byte[] generationSignatureHash; if (version == 1) { generationSignatureHash = digest.digest(generationSignature); } else { digest.update(previousBlock.generationSignature); generationSignatureHash = digest.digest(getGeneratorPublicKey()); if (!Arrays.equals(generationSignature, generationSignatureHash)) { return false; } } BigInteger hit = new BigInteger(1, new byte[]{generationSignatureHash[7], generationSignatureHash[6], generationSignatureHash[5], generationSignatureHash[4], generationSignatureHash[3], generationSignatureHash[2], generationSignatureHash[1], generationSignatureHash[0]}); return Generator.verifyHit(hit, BigInteger.valueOf(effectiveBalance), previousBlock, timestamp) || (this.height < Constants.TRANSPARENT_FORGING_BLOCK_5 && Arrays.binarySearch(badBlocks, this.getId()) >= 0); } catch (RuntimeException e) { Logger.logMessage("Error verifying block generation signature", e); return false; } } private static final long[] badBlocks = new long[] { 5113090348579089956L, 8032405266942971936L, 7702042872885598917L, -407022268390237559L, -3320029330888410250L, -6568770202903512165L, 4288642518741472722L, 5315076199486616536L, -6175599071600228543L}; static { Arrays.sort(badBlocks); } void apply() { Account generatorAccount = Account.addOrGetAccount(getGeneratorId()); generatorAccount.apply(getGeneratorPublicKey()); long totalBackFees = 0; if (this.height > Constants.SHUFFLING_BLOCK) { long[] backFees = new long[3]; for (TransactionImpl transaction : getTransactions()) { long[] fees = transaction.getBackFees(); for (int i = 0; i < fees.length; i++) { backFees[i] += fees[i]; } } for (int i = 0; i < backFees.length; i++) { if (backFees[i] == 0) { break; } totalBackFees += backFees[i]; Account previousGeneratorAccount = Account.getAccount(BlockDb.findBlockAtHeight(this.height - i - 1).getGeneratorId()); Logger.logDebugMessage("Back fees %f NXT to forger at height %d", ((double)backFees[i])/Constants.ONE_NXT, this.height - i - 1); previousGeneratorAccount.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.BLOCK_GENERATED, getId(), backFees[i]); previousGeneratorAccount.addToForgedBalanceNQT(backFees[i]); } } if (totalBackFees != 0) { Logger.logDebugMessage("Fee reduced by %f NXT at height %d", ((double)totalBackFees)/Constants.ONE_NXT, this.height); } generatorAccount.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.BLOCK_GENERATED, getId(), totalFeeNQT - totalBackFees); generatorAccount.addToForgedBalanceNQT(totalFeeNQT - totalBackFees); } void setPrevious(BlockImpl block) { if (block != null) { if (block.getId() != getPreviousBlockId()) { // shouldn't happen as previous id is already verified, but just in case throw new IllegalStateException("Previous block id doesn't match"); } this.height = block.getHeight() + 1; this.calculateBaseTarget(block); } else { this.height = 0; } short index = 0; for (TransactionImpl transaction : getTransactions()) { transaction.setBlock(this); transaction.setIndex(index++); } } void loadTransactions() { for (TransactionImpl transaction : getTransactions()) { transaction.bytes(); transaction.getAppendages(); } } private void calculateBaseTarget(BlockImpl previousBlock) { long prevBaseTarget = previousBlock.baseTarget; if (previousBlock.getHeight() < Constants.SHUFFLING_BLOCK) { baseTarget = BigInteger.valueOf(prevBaseTarget) .multiply(BigInteger.valueOf(this.timestamp - previousBlock.timestamp)) .divide(BigInteger.valueOf(60)).longValue(); if (baseTarget < 0 || baseTarget > Constants.MAX_BASE_TARGET) { baseTarget = Constants.MAX_BASE_TARGET; } if (baseTarget < prevBaseTarget / 2) { baseTarget = prevBaseTarget / 2; } if (baseTarget == 0) { baseTarget = 1; } long twofoldCurBaseTarget = prevBaseTarget * 2; if (twofoldCurBaseTarget < 0) { twofoldCurBaseTarget = Constants.MAX_BASE_TARGET; } if (baseTarget > twofoldCurBaseTarget) { baseTarget = twofoldCurBaseTarget; } } else if (previousBlock.getHeight() % 2 == 0) { BlockImpl block = BlockDb.findBlockAtHeight(previousBlock.getHeight() - 2); int blocktimeAverage = (this.timestamp - block.timestamp) / 3; if (blocktimeAverage > 60) { baseTarget = (prevBaseTarget * Math.min(blocktimeAverage, Constants.MAX_BLOCKTIME_LIMIT)) / 60; } else { baseTarget = prevBaseTarget - prevBaseTarget * Constants.BASE_TARGET_GAMMA * (60 - Math.max(blocktimeAverage, Constants.MIN_BLOCKTIME_LIMIT)) / 6000; } if (baseTarget < 0 || baseTarget > Constants.MAX_BASE_TARGET_2) { baseTarget = Constants.MAX_BASE_TARGET_2; } if (baseTarget < Constants.MIN_BASE_TARGET) { baseTarget = Constants.MIN_BASE_TARGET; } } else { baseTarget = prevBaseTarget; } cumulativeDifficulty = previousBlock.cumulativeDifficulty.add(Convert.two64.divide(BigInteger.valueOf(baseTarget))); } }