/****************************************************************************** * 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.DbUtils; import nxt.util.Convert; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.List; final class TransactionDb { static TransactionImpl findTransaction(long transactionId) { return findTransaction(transactionId, Integer.MAX_VALUE); } static TransactionImpl findTransaction(long transactionId, int height) { // Check the block cache synchronized (BlockDb.blockCache) { TransactionImpl transaction = BlockDb.transactionCache.get(transactionId); if (transaction != null) { return transaction.getHeight() <= height ? transaction : null; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM transaction WHERE id = ?")) { pstmt.setLong(1, transactionId); try (ResultSet rs = pstmt.executeQuery()) { if (rs.next() && rs.getInt("height") <= height) { return loadTransaction(con, rs); } return null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } catch (NxtException.ValidationException e) { throw new RuntimeException("Transaction already in database, id = " + transactionId + ", does not pass validation!", e); } } static TransactionImpl findTransactionByFullHash(byte[] fullHash) { return findTransactionByFullHash(fullHash, Integer.MAX_VALUE); } static TransactionImpl findTransactionByFullHash(byte[] fullHash, int height) { long transactionId = Convert.fullHashToId(fullHash); // Check the cache synchronized(BlockDb.blockCache) { TransactionImpl transaction = BlockDb.transactionCache.get(transactionId); if (transaction != null) { return (transaction.getHeight() <= height && Arrays.equals(transaction.fullHash(), fullHash) ? transaction : null); } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM transaction WHERE id = ?")) { pstmt.setLong(1, transactionId); try (ResultSet rs = pstmt.executeQuery()) { if (rs.next() && Arrays.equals(rs.getBytes("full_hash"), fullHash) && rs.getInt("height") <= height) { return loadTransaction(con, rs); } return null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } catch (NxtException.ValidationException e) { throw new RuntimeException("Transaction already in database, full_hash = " + Convert.toHexString(fullHash) + ", does not pass validation!", e); } } static boolean hasTransaction(long transactionId) { return hasTransaction(transactionId, Integer.MAX_VALUE); } static boolean hasTransaction(long transactionId, int height) { // Check the block cache synchronized(BlockDb.blockCache) { TransactionImpl transaction = BlockDb.transactionCache.get(transactionId); if (transaction != null) { return (transaction.getHeight() <= height); } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT height FROM transaction WHERE id = ?")) { pstmt.setLong(1, transactionId); try (ResultSet rs = pstmt.executeQuery()) { return rs.next() && rs.getInt("height") <= height; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static boolean hasTransactionByFullHash(byte[] fullHash) { return Arrays.equals(fullHash, getFullHash(Convert.fullHashToId(fullHash))); } static boolean hasTransactionByFullHash(byte[] fullHash, int height) { long transactionId = Convert.fullHashToId(fullHash); // Check the block cache synchronized(BlockDb.blockCache) { TransactionImpl transaction = BlockDb.transactionCache.get(transactionId); if (transaction != null) { return (transaction.getHeight() <= height && Arrays.equals(transaction.fullHash(), fullHash)); } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT full_hash, height FROM transaction WHERE id = ?")) { pstmt.setLong(1, transactionId); try (ResultSet rs = pstmt.executeQuery()) { return rs.next() && Arrays.equals(rs.getBytes("full_hash"), fullHash) && rs.getInt("height") <= height; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static byte[] getFullHash(long transactionId) { // Check the block cache synchronized(BlockDb.blockCache) { TransactionImpl transaction = BlockDb.transactionCache.get(transactionId); if (transaction != null) { return transaction.fullHash(); } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT full_hash FROM transaction WHERE id = ?")) { pstmt.setLong(1, transactionId); try (ResultSet rs = pstmt.executeQuery()) { return rs.next() ? rs.getBytes("full_hash") : null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static TransactionImpl loadTransaction(Connection con, ResultSet rs) throws NxtException.NotValidException { try { byte type = rs.getByte("type"); byte subtype = rs.getByte("subtype"); int timestamp = rs.getInt("timestamp"); short deadline = rs.getShort("deadline"); long amountNQT = rs.getLong("amount"); long feeNQT = rs.getLong("fee"); byte[] referencedTransactionFullHash = rs.getBytes("referenced_transaction_full_hash"); int ecBlockHeight = rs.getInt("ec_block_height"); long ecBlockId = rs.getLong("ec_block_id"); byte[] signature = rs.getBytes("signature"); long blockId = rs.getLong("block_id"); int height = rs.getInt("height"); long id = rs.getLong("id"); long senderId = rs.getLong("sender_id"); byte[] attachmentBytes = rs.getBytes("attachment_bytes"); int blockTimestamp = rs.getInt("block_timestamp"); byte[] fullHash = rs.getBytes("full_hash"); byte version = rs.getByte("version"); short transactionIndex = rs.getShort("transaction_index"); ByteBuffer buffer = null; if (attachmentBytes != null) { buffer = ByteBuffer.wrap(attachmentBytes); buffer.order(ByteOrder.LITTLE_ENDIAN); } TransactionType transactionType = TransactionType.findTransactionType(type, subtype); TransactionImpl.BuilderImpl builder = new TransactionImpl.BuilderImpl(version, null, amountNQT, feeNQT, deadline, transactionType.parseAttachment(buffer, version)) .timestamp(timestamp) .referencedTransactionFullHash(referencedTransactionFullHash) .signature(signature) .blockId(blockId) .height(height) .id(id) .senderId(senderId) .blockTimestamp(blockTimestamp) .fullHash(fullHash) .ecBlockHeight(ecBlockHeight) .ecBlockId(ecBlockId) .index(transactionIndex); if (transactionType.canHaveRecipient()) { long recipientId = rs.getLong("recipient_id"); if (! rs.wasNull()) { builder.recipientId(recipientId); } } if (rs.getBoolean("has_message")) { builder.appendix(new Appendix.Message(buffer, version)); } if (rs.getBoolean("has_encrypted_message")) { builder.appendix(new Appendix.EncryptedMessage(buffer, version)); } if (rs.getBoolean("has_public_key_announcement")) { builder.appendix(new Appendix.PublicKeyAnnouncement(buffer, version)); } if (rs.getBoolean("has_encrypttoself_message")) { builder.appendix(new Appendix.EncryptToSelfMessage(buffer, version)); } if (rs.getBoolean("phased")) { builder.appendix(new Appendix.Phasing(buffer, version)); } if (rs.getBoolean("has_prunable_message")) { builder.appendix(new Appendix.PrunablePlainMessage(buffer, version)); } if (rs.getBoolean("has_prunable_encrypted_message")) { builder.appendix(new Appendix.PrunableEncryptedMessage(buffer, version)); } return builder.build(); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static List<TransactionImpl> findBlockTransactions(long blockId) { // Check the block cache synchronized(BlockDb.blockCache) { BlockImpl block = BlockDb.blockCache.get(blockId); if (block != null) { return block.getTransactions(); } } // Search the database try (Connection con = Db.db.getConnection()) { return findBlockTransactions(con, blockId); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static List<TransactionImpl> findBlockTransactions(Connection con, long blockId) { try (PreparedStatement pstmt = con.prepareStatement("SELECT * FROM transaction WHERE block_id = ? ORDER BY transaction_index")) { pstmt.setLong(1, blockId); pstmt.setFetchSize(50); try (ResultSet rs = pstmt.executeQuery()) { List<TransactionImpl> list = new ArrayList<>(); while (rs.next()) { list.add(loadTransaction(con, rs)); } return list; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } catch (NxtException.ValidationException e) { throw new RuntimeException("Transaction already in database for block_id = " + Long.toUnsignedString(blockId) + " does not pass validation!", e); } } static List<PrunableTransaction> findPrunableTransactions(Connection con, int minTimestamp, int maxTimestamp) { List<PrunableTransaction> result = new ArrayList<>(); try (PreparedStatement pstmt = con.prepareStatement("SELECT id, type, subtype, " + "has_prunable_attachment AS prunable_attachment, " + "has_prunable_message AS prunable_plain_message, " + "has_prunable_encrypted_message AS prunable_encrypted_message " + "FROM transaction WHERE (timestamp BETWEEN ? AND ?) AND " + "(has_prunable_attachment = TRUE OR has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE)")) { pstmt.setInt(1, minTimestamp); pstmt.setInt(2, maxTimestamp); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { long id = rs.getLong("id"); byte type = rs.getByte("type"); byte subtype = rs.getByte("subtype"); TransactionType transactionType = TransactionType.findTransactionType(type, subtype); result.add(new PrunableTransaction(id, transactionType, rs.getBoolean("prunable_attachment"), rs.getBoolean("prunable_plain_message"), rs.getBoolean("prunable_encrypted_message"))); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } return result; } static void saveTransactions(Connection con, List<TransactionImpl> transactions) { try { short index = 0; for (TransactionImpl transaction : transactions) { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO transaction (id, deadline, " + "recipient_id, amount, fee, referenced_transaction_full_hash, height, " + "block_id, signature, timestamp, type, subtype, sender_id, attachment_bytes, " + "block_timestamp, full_hash, version, has_message, has_encrypted_message, has_public_key_announcement, " + "has_encrypttoself_message, phased, has_prunable_message, has_prunable_encrypted_message, " + "has_prunable_attachment, ec_block_height, ec_block_id, transaction_index) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, transaction.getId()); pstmt.setShort(++i, transaction.getDeadline()); DbUtils.setLongZeroToNull(pstmt, ++i, transaction.getRecipientId()); pstmt.setLong(++i, transaction.getAmountNQT()); pstmt.setLong(++i, transaction.getFeeNQT()); DbUtils.setBytes(pstmt, ++i, transaction.referencedTransactionFullHash()); pstmt.setInt(++i, transaction.getHeight()); pstmt.setLong(++i, transaction.getBlockId()); pstmt.setBytes(++i, transaction.getSignature()); pstmt.setInt(++i, transaction.getTimestamp()); pstmt.setByte(++i, transaction.getType().getType()); pstmt.setByte(++i, transaction.getType().getSubtype()); pstmt.setLong(++i, transaction.getSenderId()); int bytesLength = 0; for (Appendix appendage : transaction.getAppendages()) { bytesLength += appendage.getSize(); } if (bytesLength == 0) { pstmt.setNull(++i, Types.VARBINARY); } else { ByteBuffer buffer = ByteBuffer.allocate(bytesLength); buffer.order(ByteOrder.LITTLE_ENDIAN); for (Appendix appendage : transaction.getAppendages()) { appendage.putBytes(buffer); } pstmt.setBytes(++i, buffer.array()); } pstmt.setInt(++i, transaction.getBlockTimestamp()); pstmt.setBytes(++i, transaction.fullHash()); pstmt.setByte(++i, transaction.getVersion()); pstmt.setBoolean(++i, transaction.getMessage() != null); pstmt.setBoolean(++i, transaction.getEncryptedMessage() != null); pstmt.setBoolean(++i, transaction.getPublicKeyAnnouncement() != null); pstmt.setBoolean(++i, transaction.getEncryptToSelfMessage() != null); pstmt.setBoolean(++i, transaction.getPhasing() != null); pstmt.setBoolean(++i, transaction.hasPrunablePlainMessage()); pstmt.setBoolean(++i, transaction.hasPrunableEncryptedMessage()); pstmt.setBoolean(++i, transaction.getAttachment() instanceof Appendix.Prunable); pstmt.setInt(++i, transaction.getECBlockHeight()); DbUtils.setLongZeroToNull(pstmt, ++i, transaction.getECBlockId()); pstmt.setShort(++i, index++); pstmt.executeUpdate(); } if (transaction.referencedTransactionFullHash() != null) { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO referenced_transaction " + "(transaction_id, referenced_transaction_id) VALUES (?, ?)")) { pstmt.setLong(1, transaction.getId()); pstmt.setLong(2, Convert.fullHashToId(transaction.referencedTransactionFullHash())); pstmt.executeUpdate(); } } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static class PrunableTransaction { private final long id; private final TransactionType transactionType; private final boolean prunableAttachment; private final boolean prunablePlainMessage; private final boolean prunableEncryptedMessage; public PrunableTransaction(long id, TransactionType transactionType, boolean prunableAttachment, boolean prunablePlainMessage, boolean prunableEncryptedMessage) { this.id = id; this.transactionType = transactionType; this.prunableAttachment = prunableAttachment; this.prunablePlainMessage = prunablePlainMessage; this.prunableEncryptedMessage = prunableEncryptedMessage; } public long getId() { return id; } public TransactionType getTransactionType() { return transactionType; } public boolean hasPrunableAttachment() { return prunableAttachment; } public boolean hasPrunablePlainMessage() { return prunablePlainMessage; } public boolean hasPrunableEncryptedMessage() { return prunableEncryptedMessage; } } }