/****************************************************************************** * 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.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.EntityDbTable; import nxt.peer.Peer; import nxt.peer.Peers; import nxt.util.Convert; import nxt.util.JSON; import nxt.util.Listener; import nxt.util.Listeners; import nxt.util.Logger; import nxt.util.ThreadPool; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; final class TransactionProcessorImpl implements TransactionProcessor { private static final boolean enableTransactionRebroadcasting = Nxt.getBooleanProperty("nxt.enableTransactionRebroadcasting"); private static final boolean testUnconfirmedTransactions = Nxt.getBooleanProperty("nxt.testUnconfirmedTransactions"); private static final int maxUnconfirmedTransactions; static { int n = Nxt.getIntProperty("nxt.maxUnconfirmedTransactions"); maxUnconfirmedTransactions = n <= 0 ? Integer.MAX_VALUE : n; } private static final TransactionProcessorImpl instance = new TransactionProcessorImpl(); static TransactionProcessorImpl getInstance() { return instance; } private final Map<Long, UnconfirmedTransaction> transactionCache = new HashMap<>(); private volatile boolean cacheInitialized = false; final DbKey.LongKeyFactory<UnconfirmedTransaction> unconfirmedTransactionDbKeyFactory = new DbKey.LongKeyFactory<UnconfirmedTransaction>("id") { @Override public DbKey newKey(UnconfirmedTransaction unconfirmedTransaction) { return unconfirmedTransaction.getTransaction().getDbKey(); } }; private final EntityDbTable<UnconfirmedTransaction> unconfirmedTransactionTable = new EntityDbTable<UnconfirmedTransaction>("unconfirmed_transaction", unconfirmedTransactionDbKeyFactory) { @Override protected UnconfirmedTransaction load(Connection con, ResultSet rs) throws SQLException { return new UnconfirmedTransaction(rs); } @Override protected void save(Connection con, UnconfirmedTransaction unconfirmedTransaction) throws SQLException { unconfirmedTransaction.save(con); if (transactionCache.size() < maxUnconfirmedTransactions) { transactionCache.put(unconfirmedTransaction.getId(), unconfirmedTransaction); } } @Override public void rollback(int height) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM unconfirmed_transaction WHERE height > ?")) { pstmt.setInt(1, height); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { UnconfirmedTransaction unconfirmedTransaction = load(con, rs); waitingTransactions.add(unconfirmedTransaction); transactionCache.remove(unconfirmedTransaction.getId()); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } super.rollback(height); unconfirmedDuplicates.clear(); } @Override public void truncate() { super.truncate(); clearCache(); } @Override protected String defaultSort() { return " ORDER BY transaction_height ASC, fee_per_byte DESC, arrival_timestamp ASC, id ASC "; } }; private final Set<TransactionImpl> broadcastedTransactions = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Listeners<List<? extends Transaction>,Event> transactionListeners = new Listeners<>(); private final PriorityQueue<UnconfirmedTransaction> waitingTransactions = new PriorityQueue<UnconfirmedTransaction>( (UnconfirmedTransaction o1, UnconfirmedTransaction o2) -> { int result; if ((result = Integer.compare(o2.getHeight(), o1.getHeight())) != 0) { return result; } if ((result = Boolean.compare(o2.getTransaction().referencedTransactionFullHash() != null, o1.getTransaction().referencedTransactionFullHash() != null)) != 0) { return result; } if ((result = Long.compare(o1.getFeePerByte(), o2.getFeePerByte())) != 0) { return result; } if ((result = Long.compare(o2.getArrivalTimestamp(), o1.getArrivalTimestamp())) != 0) { return result; } return Long.compare(o2.getId(), o1.getId()); }) { @Override public boolean add(UnconfirmedTransaction unconfirmedTransaction) { if (!super.add(unconfirmedTransaction)) { return false; } if (size() > maxUnconfirmedTransactions) { UnconfirmedTransaction removed = remove(); //Logger.logDebugMessage("Dropped unconfirmed transaction " + removed.getJSONObject().toJSONString()); } return true; } }; private final Map<TransactionType, Map<String, Integer>> unconfirmedDuplicates = new HashMap<>(); private final Runnable removeUnconfirmedTransactionsThread = () -> { try { try { if (Nxt.getBlockchainProcessor().isDownloading() && ! testUnconfirmedTransactions) { return; } List<UnconfirmedTransaction> expiredTransactions = new ArrayList<>(); try (DbIterator<UnconfirmedTransaction> iterator = unconfirmedTransactionTable.getManyBy( new DbClause.IntClause("expiration", DbClause.Op.LT, Nxt.getEpochTime()), 0, -1, "")) { while (iterator.hasNext()) { expiredTransactions.add(iterator.next()); } } if (expiredTransactions.size() > 0) { BlockchainImpl.getInstance().writeLock(); try { try { Db.db.beginTransaction(); for (UnconfirmedTransaction unconfirmedTransaction : expiredTransactions) { removeUnconfirmedTransaction(unconfirmedTransaction.getTransaction()); } Db.db.commitTransaction(); } catch (Exception e) { Logger.logErrorMessage(e.toString(), e); Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } } finally { BlockchainImpl.getInstance().writeUnlock(); } } } catch (Exception e) { Logger.logMessage("Error removing unconfirmed transactions", e); } } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString()); t.printStackTrace(); System.exit(1); } }; private final Runnable rebroadcastTransactionsThread = () -> { try { try { if (Nxt.getBlockchainProcessor().isDownloading() && ! testUnconfirmedTransactions) { return; } List<Transaction> transactionList = new ArrayList<>(); int curTime = Nxt.getEpochTime(); for (TransactionImpl transaction : broadcastedTransactions) { if (transaction.getExpiration() < curTime || TransactionDb.hasTransaction(transaction.getId())) { broadcastedTransactions.remove(transaction); } else if (transaction.getTimestamp() < curTime - 30) { transactionList.add(transaction); } } if (transactionList.size() > 0) { Peers.sendToSomePeers(transactionList); } } catch (Exception e) { Logger.logMessage("Error in transaction re-broadcasting thread", e); } } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString()); t.printStackTrace(); System.exit(1); } }; private final Runnable processTransactionsThread = () -> { try { try { if (Nxt.getBlockchainProcessor().isDownloading() && ! testUnconfirmedTransactions) { return; } Peer peer = Peers.getAnyPeer(Peer.State.CONNECTED, true); if (peer == null) { return; } JSONObject request = new JSONObject(); request.put("requestType", "getUnconfirmedTransactions"); JSONArray exclude = new JSONArray(); getAllUnconfirmedTransactionIds().forEach(transactionId -> exclude.add(Long.toUnsignedString(transactionId))); Collections.sort(exclude); request.put("exclude", exclude); JSONObject response = peer.send(JSON.prepareRequest(request), 10 * 1024 * 1024); if (response == null) { return; } JSONArray transactionsData = (JSONArray)response.get("unconfirmedTransactions"); if (transactionsData == null || transactionsData.size() == 0) { return; } try { processPeerTransactions(transactionsData); } catch (NxtException.ValidationException|RuntimeException e) { peer.blacklist(e); } } catch (Exception e) { Logger.logMessage("Error processing unconfirmed transactions", e); } } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString()); t.printStackTrace(); System.exit(1); } }; private final Runnable processWaitingTransactionsThread = () -> { try { try { if (Nxt.getBlockchainProcessor().isDownloading() && ! testUnconfirmedTransactions) { return; } processWaitingTransactions(); } catch (Exception e) { Logger.logMessage("Error processing waiting transactions", e); } } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString()); t.printStackTrace(); System.exit(1); } }; private TransactionProcessorImpl() { ThreadPool.scheduleThread("ProcessTransactions", processTransactionsThread, 5); ThreadPool.scheduleThread("RemoveUnconfirmedTransactions", removeUnconfirmedTransactionsThread, 1); ThreadPool.scheduleThread("ProcessWaitingTransactions", processWaitingTransactionsThread, 1); ThreadPool.runAfterStart(this::rebroadcastAllUnconfirmedTransactions); ThreadPool.scheduleThread("RebroadcastTransactions", rebroadcastTransactionsThread, 23); } @Override public boolean addListener(Listener<List<? extends Transaction>> listener, Event eventType) { return transactionListeners.addListener(listener, eventType); } @Override public boolean removeListener(Listener<List<? extends Transaction>> listener, Event eventType) { return transactionListeners.removeListener(listener, eventType); } void notifyListeners(List<? extends Transaction> transactions, Event eventType) { transactionListeners.notify(transactions, eventType); } @Override public DbIterator<UnconfirmedTransaction> getAllUnconfirmedTransactions() { return unconfirmedTransactionTable.getAll(0, -1); } @Override public DbIterator<UnconfirmedTransaction> getAllUnconfirmedTransactions(String sort) { return unconfirmedTransactionTable.getAll(0, -1, sort); } @Override public Transaction getUnconfirmedTransaction(long transactionId) { return unconfirmedTransactionTable.get(unconfirmedTransactionDbKeyFactory.newKey(transactionId)); } private List<Long> getAllUnconfirmedTransactionIds() { List<Long> result = new ArrayList<>(); try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT id FROM unconfirmed_transaction"); 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 UnconfirmedTransaction[] getAllWaitingTransactions() { UnconfirmedTransaction[] transactions; BlockchainImpl.getInstance().readLock(); try { transactions = waitingTransactions.toArray(new UnconfirmedTransaction[waitingTransactions.size()]); } finally { BlockchainImpl.getInstance().readUnlock(); } Arrays.sort(transactions, waitingTransactions.comparator()); return transactions; } Collection<UnconfirmedTransaction> getWaitingTransactions() { return Collections.unmodifiableCollection(waitingTransactions); } @Override public TransactionImpl[] getAllBroadcastedTransactions() { BlockchainImpl.getInstance().readLock(); try { return broadcastedTransactions.toArray(new TransactionImpl[broadcastedTransactions.size()]); } finally { BlockchainImpl.getInstance().readUnlock(); } } @Override public void broadcast(Transaction transaction) throws NxtException.ValidationException { BlockchainImpl.getInstance().writeLock(); try { if (TransactionDb.hasTransaction(transaction.getId())) { Logger.logMessage("Transaction " + transaction.getStringId() + " already in blockchain, will not broadcast again"); return; } if (unconfirmedTransactionTable.get(((TransactionImpl) transaction).getDbKey()) != null) { if (enableTransactionRebroadcasting) { broadcastedTransactions.add((TransactionImpl) transaction); Logger.logMessage("Transaction " + transaction.getStringId() + " already in unconfirmed pool, will re-broadcast"); } else { Logger.logMessage("Transaction " + transaction.getStringId() + " already in unconfirmed pool, will not broadcast again"); } return; } transaction.validate(); UnconfirmedTransaction unconfirmedTransaction = new UnconfirmedTransaction((TransactionImpl) transaction, System.currentTimeMillis()); boolean broadcastLater = BlockchainProcessorImpl.getInstance().isProcessingBlock(); if (broadcastLater) { waitingTransactions.add(unconfirmedTransaction); broadcastedTransactions.add((TransactionImpl) transaction); Logger.logDebugMessage("Will broadcast new transaction later " + transaction.getStringId()); } else { processTransaction(unconfirmedTransaction); Logger.logDebugMessage("Accepted new transaction " + transaction.getStringId()); List<Transaction> acceptedTransactions = Collections.singletonList(transaction); Peers.sendToSomePeers(acceptedTransactions); transactionListeners.notify(acceptedTransactions, Event.ADDED_UNCONFIRMED_TRANSACTIONS); if (enableTransactionRebroadcasting) { broadcastedTransactions.add((TransactionImpl) transaction); } } } finally { BlockchainImpl.getInstance().writeUnlock(); } } @Override public void processPeerTransactions(JSONObject request) throws NxtException.ValidationException { JSONArray transactionsData = (JSONArray)request.get("transactions"); processPeerTransactions(transactionsData); } @Override public void clearUnconfirmedTransactions() { BlockchainImpl.getInstance().writeLock(); try { List<Transaction> removed = new ArrayList<>(); try { Db.db.beginTransaction(); try (DbIterator<UnconfirmedTransaction> unconfirmedTransactions = getAllUnconfirmedTransactions()) { for (UnconfirmedTransaction unconfirmedTransaction : unconfirmedTransactions) { unconfirmedTransaction.getTransaction().undoUnconfirmed(); removed.add(unconfirmedTransaction.getTransaction()); } } unconfirmedTransactionTable.truncate(); Db.db.commitTransaction(); } catch (Exception e) { Logger.logErrorMessage(e.toString(), e); Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } unconfirmedDuplicates.clear(); waitingTransactions.clear(); broadcastedTransactions.clear(); transactionCache.clear(); transactionListeners.notify(removed, Event.REMOVED_UNCONFIRMED_TRANSACTIONS); } finally { BlockchainImpl.getInstance().writeUnlock(); } } @Override public void requeueAllUnconfirmedTransactions() { BlockchainImpl.getInstance().writeLock(); try { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); requeueAllUnconfirmedTransactions(); Db.db.commitTransaction(); } catch (Exception e) { Logger.logErrorMessage(e.toString(), e); Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } return; } List<Transaction> removed = new ArrayList<>(); try (DbIterator<UnconfirmedTransaction> unconfirmedTransactions = getAllUnconfirmedTransactions()) { for (UnconfirmedTransaction unconfirmedTransaction : unconfirmedTransactions) { unconfirmedTransaction.getTransaction().undoUnconfirmed(); if (removed.size() < maxUnconfirmedTransactions) { removed.add(unconfirmedTransaction.getTransaction()); } waitingTransactions.add(unconfirmedTransaction); } } unconfirmedTransactionTable.truncate(); unconfirmedDuplicates.clear(); transactionCache.clear(); transactionListeners.notify(removed, Event.REMOVED_UNCONFIRMED_TRANSACTIONS); } finally { BlockchainImpl.getInstance().writeUnlock(); } } @Override public void rebroadcastAllUnconfirmedTransactions() { BlockchainImpl.getInstance().writeLock(); try { try (DbIterator<UnconfirmedTransaction> oldNonBroadcastedTransactions = getAllUnconfirmedTransactions()) { for (UnconfirmedTransaction unconfirmedTransaction : oldNonBroadcastedTransactions) { if (unconfirmedTransaction.getTransaction().isUnconfirmedDuplicate(unconfirmedDuplicates)) { Logger.logDebugMessage("Skipping duplicate unconfirmed transaction " + unconfirmedTransaction.getTransaction().getJSONObject().toString()); } else if (enableTransactionRebroadcasting) { broadcastedTransactions.add(unconfirmedTransaction.getTransaction()); } } } } finally { BlockchainImpl.getInstance().writeUnlock(); } } void removeUnconfirmedTransaction(TransactionImpl transaction) { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); removeUnconfirmedTransaction(transaction); Db.db.commitTransaction(); } catch (Exception e) { Logger.logErrorMessage(e.toString(), e); Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } return; } try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("DELETE FROM unconfirmed_transaction WHERE id = ?")) { pstmt.setLong(1, transaction.getId()); int deleted = pstmt.executeUpdate(); if (deleted > 0) { transaction.undoUnconfirmed(); transactionCache.remove(transaction.getId()); transactionListeners.notify(Collections.singletonList(transaction), Event.REMOVED_UNCONFIRMED_TRANSACTIONS); } } catch (SQLException e) { Logger.logErrorMessage(e.toString(), e); throw new RuntimeException(e.toString(), e); } } @Override public void processLater(Collection<? extends Transaction> transactions) { long currentTime = System.currentTimeMillis(); BlockchainImpl.getInstance().writeLock(); try { for (Transaction transaction : transactions) { ((TransactionImpl)transaction).unsetBlock(); waitingTransactions.add(new UnconfirmedTransaction((TransactionImpl)transaction, Math.min(currentTime, Convert.fromEpochTime(transaction.getTimestamp())))); } } finally { BlockchainImpl.getInstance().writeUnlock(); } } void processWaitingTransactions() { BlockchainImpl.getInstance().writeLock(); try { if (waitingTransactions.size() > 0) { int currentTime = Nxt.getEpochTime(); List<Transaction> addedUnconfirmedTransactions = new ArrayList<>(); Iterator<UnconfirmedTransaction> iterator = waitingTransactions.iterator(); while (iterator.hasNext()) { UnconfirmedTransaction unconfirmedTransaction = iterator.next(); try { processTransaction(unconfirmedTransaction); iterator.remove(); addedUnconfirmedTransactions.add(unconfirmedTransaction.getTransaction()); } catch (NxtException.ExistingTransactionException e) { iterator.remove(); } catch (NxtException.NotCurrentlyValidException e) { if (unconfirmedTransaction.getExpiration() < currentTime || currentTime - Convert.toEpochTime(unconfirmedTransaction.getArrivalTimestamp()) > 3600) { iterator.remove(); } } catch (NxtException.ValidationException|RuntimeException e) { iterator.remove(); } } if (addedUnconfirmedTransactions.size() > 0) { transactionListeners.notify(addedUnconfirmedTransactions, Event.ADDED_UNCONFIRMED_TRANSACTIONS); } } } finally { BlockchainImpl.getInstance().writeUnlock(); } } private void processPeerTransactions(JSONArray transactionsData) throws NxtException.NotValidException { if (Nxt.getBlockchain().getHeight() <= Constants.LAST_CHECKSUM_BLOCK) { return; } if (transactionsData == null || transactionsData.isEmpty()) { return; } long arrivalTimestamp = System.currentTimeMillis(); List<TransactionImpl> receivedTransactions = new ArrayList<>(); List<TransactionImpl> sendToPeersTransactions = new ArrayList<>(); List<TransactionImpl> addedUnconfirmedTransactions = new ArrayList<>(); List<Exception> exceptions = new ArrayList<>(); for (Object transactionData : transactionsData) { try { TransactionImpl transaction = TransactionImpl.parseTransaction((JSONObject) transactionData); receivedTransactions.add(transaction); if (unconfirmedTransactionTable.get(transaction.getDbKey()) != null || TransactionDb.hasTransaction(transaction.getId())) { continue; } transaction.validate(); UnconfirmedTransaction unconfirmedTransaction = new UnconfirmedTransaction(transaction, arrivalTimestamp); processTransaction(unconfirmedTransaction); if (broadcastedTransactions.contains(transaction)) { Logger.logDebugMessage("Received back transaction " + transaction.getStringId() + " that we broadcasted, will not forward again to peers"); } else { sendToPeersTransactions.add(transaction); } addedUnconfirmedTransactions.add(transaction); } catch (NxtException.NotCurrentlyValidException ignore) { } catch (NxtException.ValidationException|RuntimeException e) { Logger.logDebugMessage(String.format("Invalid transaction from peer: %s", ((JSONObject) transactionData).toJSONString()), e); exceptions.add(e); } } if (sendToPeersTransactions.size() > 0) { Peers.sendToSomePeers(sendToPeersTransactions); } if (addedUnconfirmedTransactions.size() > 0) { transactionListeners.notify(addedUnconfirmedTransactions, Event.ADDED_UNCONFIRMED_TRANSACTIONS); } broadcastedTransactions.removeAll(receivedTransactions); if (!exceptions.isEmpty()) { throw new NxtException.NotValidException("Peer sends invalid transactions: " + exceptions.toString()); } } private void processTransaction(UnconfirmedTransaction unconfirmedTransaction) throws NxtException.ValidationException { TransactionImpl transaction = unconfirmedTransaction.getTransaction(); int curTime = Nxt.getEpochTime(); if (transaction.getTimestamp() > curTime + Constants.MAX_TIMEDRIFT || transaction.getExpiration() < curTime) { throw new NxtException.NotCurrentlyValidException("Invalid transaction timestamp"); } if (transaction.getVersion() < 1) { throw new NxtException.NotValidException("Invalid transaction version"); } if (transaction.getId() == 0L) { throw new NxtException.NotValidException("Invalid transaction id 0"); } BlockchainImpl.getInstance().writeLock(); try { try { Db.db.beginTransaction(); if (Nxt.getBlockchain().getHeight() <= Constants.LAST_CHECKSUM_BLOCK) { throw new NxtException.NotCurrentlyValidException("Blockchain not ready to accept transactions"); } if (unconfirmedTransactionTable.get(transaction.getDbKey()) != null || TransactionDb.hasTransaction(transaction.getId())) { throw new NxtException.ExistingTransactionException("Transaction already processed"); } if (! transaction.verifySignature()) { if (Account.getAccount(transaction.getSenderId()) != null) { throw new NxtException.NotValidException("Transaction signature verification failed"); } else { throw new NxtException.NotCurrentlyValidException("Unknown transaction sender"); } } if (! transaction.applyUnconfirmed()) { throw new NxtException.InsufficientBalanceException("Insufficient balance"); } if (transaction.isUnconfirmedDuplicate(unconfirmedDuplicates)) { throw new NxtException.NotCurrentlyValidException("Duplicate unconfirmed transaction"); } unconfirmedTransactionTable.insert(unconfirmedTransaction); Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } } finally { BlockchainImpl.getInstance().writeUnlock(); } } private static final Comparator<UnconfirmedTransaction> cachedUnconfirmedTransactionComparator = (UnconfirmedTransaction t1, UnconfirmedTransaction t2) -> { int compare; // Sort by transaction_height ASC compare = Integer.compare(t1.getHeight(), t2.getHeight()); if (compare != 0) return compare; // Sort by fee_per_byte DESC compare = Long.compare(t1.getFeePerByte(), t2.getFeePerByte()); if (compare != 0) return -compare; // Sort by arrival_timestamp ASC compare = Long.compare(t1.getArrivalTimestamp(), t2.getArrivalTimestamp()); if (compare != 0) return compare; // Sort by transaction ID ASC return Long.compare(t1.getId(), t2.getId()); }; /** * Get the cached unconfirmed transactions * * @param exclude List of transaction identifiers to exclude */ @Override public SortedSet<? extends Transaction> getCachedUnconfirmedTransactions(List<String> exclude) { SortedSet<UnconfirmedTransaction> transactionSet = new TreeSet<>(cachedUnconfirmedTransactionComparator); Nxt.getBlockchain().readLock(); try { // // Initialize the unconfirmed transaction cache if it hasn't been done yet // synchronized(transactionCache) { if (!cacheInitialized) { DbIterator<UnconfirmedTransaction> it = getAllUnconfirmedTransactions(); while (it.hasNext()) { UnconfirmedTransaction unconfirmedTransaction = it.next(); transactionCache.put(unconfirmedTransaction.getId(), unconfirmedTransaction); } cacheInitialized = true; } } // // Build the result set // transactionCache.values().forEach(transaction -> { if (Collections.binarySearch(exclude, transaction.getStringId()) < 0) { transactionSet.add(transaction); } }); } finally { Nxt.getBlockchain().readUnlock(); } return transactionSet; } /** * Restore expired prunable data * * @param transactions Transactions containing prunable data * @return Processed transactions * @throws NxtException.NotValidException Transaction is not valid */ @Override public List<Transaction> restorePrunableData(JSONArray transactions) throws NxtException.NotValidException { List<Transaction> processed = new ArrayList<>(); Nxt.getBlockchain().readLock(); try { Db.db.beginTransaction(); try { // // Check each transaction returned by the archive peer // for (Object transactionJSON : transactions) { TransactionImpl transaction = TransactionImpl.parseTransaction((JSONObject)transactionJSON); TransactionImpl myTransaction = TransactionDb.findTransactionByFullHash(transaction.fullHash()); if (myTransaction != null) { boolean foundAllData = true; // // Process each prunable appendage // appendageLoop: for (Appendix.AbstractAppendix appendage : transaction.getAppendages()) { if ((appendage instanceof Appendix.Prunable)) { // // Don't load the prunable data if we already have the data // for (Appendix.AbstractAppendix myAppendage : myTransaction.getAppendages()) { if (myAppendage.getClass() == appendage.getClass()) { myAppendage.loadPrunable(myTransaction, true); if (((Appendix.Prunable)myAppendage).hasPrunableData()) { Logger.logDebugMessage(String.format("Already have prunable data for transaction %s %s appendage", myTransaction.getStringId(), myAppendage.getAppendixName())); continue appendageLoop; } break; } } // // Load the prunable data // if (((Appendix.Prunable)appendage).hasPrunableData()) { Logger.logDebugMessage(String.format("Loading prunable data for transaction %s %s appendage", Long.toUnsignedString(transaction.getId()), appendage.getAppendixName())); ((Appendix.Prunable)appendage).restorePrunableData(transaction, myTransaction.getBlockTimestamp(), myTransaction.getHeight()); } else { foundAllData = false; } } } if (foundAllData) { processed.add(myTransaction); } Db.db.clearCache(); Db.db.commitTransaction(); } } Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); processed.clear(); throw e; } finally { Db.db.endTransaction(); } } finally { Nxt.getBlockchain().readUnlock(); } return processed; } }