/****************************************************************************** * 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.db.DbIterator; import nxt.db.DbUtils; import nxt.util.Convert; import nxt.util.Filter; import nxt.util.ReadWriteUpdateLock; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; final class BlockchainImpl implements Blockchain { private static final BlockchainImpl instance = new BlockchainImpl(); static BlockchainImpl getInstance() { return instance; } private BlockchainImpl() {} private final ReadWriteUpdateLock lock = new ReadWriteUpdateLock(); private final AtomicReference<BlockImpl> lastBlock = new AtomicReference<>(); @Override public void readLock() { lock.readLock().lock(); } @Override public void readUnlock() { lock.readLock().unlock(); } @Override public void updateLock() { lock.updateLock().lock(); } @Override public void updateUnlock() { lock.updateLock().unlock(); } void writeLock() { lock.writeLock().lock(); } void writeUnlock() { lock.writeLock().unlock(); } @Override public BlockImpl getLastBlock() { return lastBlock.get(); } void setLastBlock(BlockImpl block) { lastBlock.set(block); } void setLastBlock(BlockImpl previousBlock, BlockImpl block) { if (! lastBlock.compareAndSet(previousBlock, block)) { throw new IllegalStateException("Last block is no longer previous block"); } } @Override public int getHeight() { BlockImpl last = lastBlock.get(); return last == null ? 0 : last.getHeight(); } @Override public int getLastBlockTimestamp() { BlockImpl last = lastBlock.get(); return last == null ? 0 : last.getTimestamp(); } @Override public BlockImpl getLastBlock(int timestamp) { BlockImpl block = lastBlock.get(); if (timestamp >= block.getTimestamp()) { return block; } return BlockDb.findLastBlock(timestamp); } @Override public BlockImpl getBlock(long blockId) { BlockImpl block = lastBlock.get(); if (block.getId() == blockId) { return block; } return BlockDb.findBlock(blockId); } @Override public boolean hasBlock(long blockId) { return lastBlock.get().getId() == blockId || BlockDb.hasBlock(blockId); } @Override public DbIterator<BlockImpl> getAllBlocks() { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block ORDER BY db_id ASC"); return getBlocks(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<BlockImpl> getBlocks(int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block WHERE height <= ? AND height >= ? ORDER BY height DESC"); int blockchainHeight = getHeight(); pstmt.setInt(1, blockchainHeight - from); pstmt.setInt(2, blockchainHeight - to); return getBlocks(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<BlockImpl> getBlocks(long accountId, int timestamp) { return getBlocks(accountId, timestamp, 0, -1); } @Override public DbIterator<BlockImpl> getBlocks(long accountId, int timestamp, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block WHERE generator_id = ? " + (timestamp > 0 ? " AND timestamp >= ? " : " ") + "ORDER BY height DESC" + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setLong(++i, accountId); if (timestamp > 0) { pstmt.setInt(++i, timestamp); } DbUtils.setLimits(++i, pstmt, from, to); return getBlocks(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public int getBlockCount(long accountId) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT COUNT(*) FROM block WHERE generator_id = ?")) { pstmt.setLong(1, accountId); try (ResultSet rs = pstmt.executeQuery()) { rs.next(); return rs.getInt(1); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<BlockImpl> getBlocks(Connection con, PreparedStatement pstmt) { return new DbIterator<>(con, pstmt, BlockDb::loadBlock); } @Override public List<Long> getBlockIdsAfter(long blockId, int limit) { // Check the block cache List<Long> result = new ArrayList<>(BlockDb.BLOCK_CACHE_SIZE); synchronized(BlockDb.blockCache) { BlockImpl block = BlockDb.blockCache.get(blockId); if (block != null) { Collection<BlockImpl> cacheMap = BlockDb.heightMap.tailMap(block.getHeight() + 1).values(); for (BlockImpl cacheBlock : cacheMap) { if (result.size() >= limit) { break; } result.add(cacheBlock.getId()); } return result; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT id FROM block " + "WHERE db_id > (SELECT db_id FROM block WHERE id = ?) " + "ORDER BY db_id ASC LIMIT ?")) { pstmt.setLong(1, blockId); pstmt.setInt(2, limit); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { result.add(rs.getLong("id")); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } return result; } @Override public List<BlockImpl> getBlocksAfter(long blockId, int limit) { if (limit <= 0) { return Collections.emptyList(); } // Check the block cache List<BlockImpl> result = new ArrayList<>(BlockDb.BLOCK_CACHE_SIZE); synchronized(BlockDb.blockCache) { BlockImpl block = BlockDb.blockCache.get(blockId); if (block != null) { Collection<BlockImpl> cacheMap = BlockDb.heightMap.tailMap(block.getHeight() + 1).values(); for (BlockImpl cacheBlock : cacheMap) { if (result.size() >= limit) { break; } result.add(cacheBlock); } return result; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block " + "WHERE db_id > (SELECT db_id FROM block WHERE id = ?) " + "ORDER BY db_id ASC LIMIT ?")) { pstmt.setLong(1, blockId); pstmt.setInt(2, limit); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { result.add(BlockDb.loadBlock(con, rs, true)); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } return result; } @Override public List<BlockImpl> getBlocksAfter(long blockId, List<Long> blockList) { if (blockList.isEmpty()) { return Collections.emptyList(); } // Check the block cache List<BlockImpl> result = new ArrayList<>(BlockDb.BLOCK_CACHE_SIZE); synchronized(BlockDb.blockCache) { BlockImpl block = BlockDb.blockCache.get(blockId); if (block != null) { Collection<BlockImpl> cacheMap = BlockDb.heightMap.tailMap(block.getHeight() + 1).values(); int index = 0; for (BlockImpl cacheBlock : cacheMap) { if (result.size() >= blockList.size() || cacheBlock.getId() != blockList.get(index++)) { break; } result.add(cacheBlock); } return result; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block " + "WHERE db_id > (SELECT db_id FROM block WHERE id = ?) " + "ORDER BY db_id ASC LIMIT ?")) { pstmt.setLong(1, blockId); pstmt.setInt(2, blockList.size()); try (ResultSet rs = pstmt.executeQuery()) { int index = 0; while (rs.next()) { BlockImpl block = BlockDb.loadBlock(con, rs, true); if (block.getId() != blockList.get(index++)) { break; } result.add(block); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } return result; } @Override public long getBlockIdAtHeight(int height) { Block block = lastBlock.get(); if (height > block.getHeight()) { throw new IllegalArgumentException("Invalid height " + height + ", current blockchain is at " + block.getHeight()); } if (height == block.getHeight()) { return block.getId(); } return BlockDb.findBlockIdAtHeight(height); } @Override public BlockImpl getBlockAtHeight(int height) { BlockImpl block = lastBlock.get(); if (height > block.getHeight()) { throw new IllegalArgumentException("Invalid height " + height + ", current blockchain is at " + block.getHeight()); } if (height == block.getHeight()) { return block; } return BlockDb.findBlockAtHeight(height); } @Override public TransactionImpl getTransaction(long transactionId) { return TransactionDb.findTransaction(transactionId); } @Override public TransactionImpl getTransactionByFullHash(String fullHash) { return TransactionDb.findTransactionByFullHash(Convert.parseHexString(fullHash)); } @Override public boolean hasTransaction(long transactionId) { return TransactionDb.hasTransaction(transactionId); } @Override public boolean hasTransactionByFullHash(String fullHash) { return TransactionDb.hasTransactionByFullHash(Convert.parseHexString(fullHash)); } @Override public int getTransactionCount() { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT COUNT(*) FROM transaction"); ResultSet rs = pstmt.executeQuery()) { rs.next(); return rs.getInt(1); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<TransactionImpl> getAllTransactions() { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM transaction ORDER BY db_id ASC"); return getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<TransactionImpl> getTransactions(long accountId, byte type, byte subtype, int blockTimestamp, boolean includeExpiredPrunable) { return getTransactions(accountId, 0, type, subtype, blockTimestamp, false, false, false, 0, -1, includeExpiredPrunable, false); } @Override public DbIterator<TransactionImpl> getTransactions(long accountId, int numberOfConfirmations, byte type, byte subtype, int blockTimestamp, boolean withMessage, boolean phasedOnly, boolean nonPhasedOnly, int from, int to, boolean includeExpiredPrunable, boolean executedOnly) { if (phasedOnly && nonPhasedOnly) { throw new IllegalArgumentException("At least one of phasedOnly or nonPhasedOnly must be false"); } int height = numberOfConfirmations > 0 ? getHeight() - numberOfConfirmations : Integer.MAX_VALUE; if (height < 0) { throw new IllegalArgumentException("Number of confirmations required " + numberOfConfirmations + " exceeds current blockchain height " + getHeight()); } Connection con = null; try { StringBuilder buf = new StringBuilder(); buf.append("SELECT transaction.* FROM transaction "); if (executedOnly && !nonPhasedOnly) { buf.append(" LEFT JOIN phasing_poll_result ON transaction.id = phasing_poll_result.id "); } buf.append("WHERE recipient_id = ? AND sender_id <> ? "); if (blockTimestamp > 0) { buf.append("AND block_timestamp >= ? "); } if (type >= 0) { buf.append("AND type = ? "); if (subtype >= 0) { buf.append("AND subtype = ? "); } } if (height < Integer.MAX_VALUE) { buf.append("AND height <= ? "); } if (withMessage) { buf.append("AND (has_message = TRUE OR has_encrypted_message = TRUE "); buf.append("OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) "); } if (phasedOnly) { buf.append("AND phased = TRUE "); } else if (nonPhasedOnly) { buf.append("AND phased = FALSE "); } if (executedOnly && !nonPhasedOnly) { buf.append("AND (phased = FALSE OR approved = TRUE) "); } buf.append("UNION ALL SELECT transaction.* FROM transaction "); if (executedOnly && !nonPhasedOnly) { buf.append(" LEFT JOIN phasing_poll_result ON transaction.id = phasing_poll_result.id "); } buf.append("WHERE sender_id = ? "); if (blockTimestamp > 0) { buf.append("AND block_timestamp >= ? "); } if (type >= 0) { buf.append("AND type = ? "); if (subtype >= 0) { buf.append("AND subtype = ? "); } } if (height < Integer.MAX_VALUE) { buf.append("AND height <= ? "); } if (withMessage) { buf.append("AND (has_message = TRUE OR has_encrypted_message = TRUE OR has_encrypttoself_message = TRUE "); buf.append("OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) "); } if (phasedOnly) { buf.append("AND phased = TRUE "); } else if (nonPhasedOnly) { buf.append("AND phased = FALSE "); } if (executedOnly && !nonPhasedOnly) { buf.append("AND (phased = FALSE OR approved = TRUE) "); } buf.append("ORDER BY block_timestamp DESC, transaction_index DESC"); buf.append(DbUtils.limitsClause(from, to)); con = Db.db.getConnection(); PreparedStatement pstmt; int i = 0; pstmt = con.prepareStatement(buf.toString()); pstmt.setLong(++i, accountId); pstmt.setLong(++i, accountId); if (blockTimestamp > 0) { pstmt.setInt(++i, blockTimestamp); } if (type >= 0) { pstmt.setByte(++i, type); if (subtype >= 0) { pstmt.setByte(++i, subtype); } } if (height < Integer.MAX_VALUE) { pstmt.setInt(++i, height); } int prunableExpiration = Math.max(0, Constants.INCLUDE_EXPIRED_PRUNABLE && includeExpiredPrunable ? Nxt.getEpochTime() - Constants.MAX_PRUNABLE_LIFETIME : Nxt.getEpochTime() - Constants.MIN_PRUNABLE_LIFETIME); if (withMessage) { pstmt.setInt(++i, prunableExpiration); } pstmt.setLong(++i, accountId); if (blockTimestamp > 0) { pstmt.setInt(++i, blockTimestamp); } if (type >= 0) { pstmt.setByte(++i, type); if (subtype >= 0) { pstmt.setByte(++i, subtype); } } if (height < Integer.MAX_VALUE) { pstmt.setInt(++i, height); } if (withMessage) { pstmt.setInt(++i, prunableExpiration); } DbUtils.setLimits(++i, pstmt, from, to); return getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<TransactionImpl> getReferencingTransactions(long transactionId, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT transaction.* FROM transaction, referenced_transaction " + "WHERE referenced_transaction.referenced_transaction_id = ? " + "AND referenced_transaction.transaction_id = transaction.id " + "ORDER BY transaction.block_timestamp DESC, transaction.transaction_index DESC " + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setLong(++i, transactionId); DbUtils.setLimits(++i, pstmt, from, to); return getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } @Override public DbIterator<TransactionImpl> getTransactions(Connection con, PreparedStatement pstmt) { return new DbIterator<>(con, pstmt, TransactionDb::loadTransaction); } @Override public List<TransactionImpl> getExpectedTransactions(Filter<Transaction> filter) { Map<TransactionType, Map<String, Integer>> duplicates = new HashMap<>(); BlockchainProcessorImpl blockchainProcessor = BlockchainProcessorImpl.getInstance(); List<TransactionImpl> result = new ArrayList<>(); readLock(); try { if (getHeight() >= Constants.PHASING_BLOCK) { try (DbIterator<TransactionImpl> phasedTransactions = PhasingPoll.getFinishingTransactions(getHeight() + 1)) { for (TransactionImpl phasedTransaction : phasedTransactions) { try { phasedTransaction.validate(); if (!phasedTransaction.attachmentIsDuplicate(duplicates, false) && filter.ok(phasedTransaction)) { result.add(phasedTransaction); } } catch (NxtException.ValidationException ignore) { } } } } blockchainProcessor.selectUnconfirmedTransactions(duplicates, getLastBlock(), -1).forEach( unconfirmedTransaction -> { TransactionImpl transaction = unconfirmedTransaction.getTransaction(); if (transaction.getPhasing() == null && filter.ok(transaction)) { result.add(transaction); } } ); } finally { readUnlock(); } return result; } }