|
|
@ -143,11 +143,11 @@ public class Controller extends Thread { |
|
|
|
/** Whether we can mint new blocks, as reported by BlockMinter. */ |
|
|
|
/** Whether we can mint new blocks, as reported by BlockMinter. */ |
|
|
|
private volatile boolean isMintingPossible = false; |
|
|
|
private volatile boolean isMintingPossible = false; |
|
|
|
|
|
|
|
|
|
|
|
/** List of incoming transaction that are in the import queue */ |
|
|
|
/** Map of incoming transaction that are in the import queue. Key is transaction data, value is whether signature has been validated. */ |
|
|
|
private List<TransactionData> incomingTransactions = Collections.synchronizedList(new ArrayList<>()); |
|
|
|
private final Map<TransactionData, Boolean> incomingTransactions = Collections.synchronizedMap(new HashMap<>()); |
|
|
|
|
|
|
|
|
|
|
|
/** List of recent invalid unconfirmed transactions */ |
|
|
|
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */ |
|
|
|
private Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>()); |
|
|
|
private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>()); |
|
|
|
|
|
|
|
|
|
|
|
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */ |
|
|
|
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */ |
|
|
|
private final ReentrantLock blockchainLock = new ReentrantLock(); |
|
|
|
private final ReentrantLock blockchainLock = new ReentrantLock(); |
|
|
@ -837,17 +837,17 @@ public class Controller extends Thread { |
|
|
|
|
|
|
|
|
|
|
|
private boolean incomingTransactionQueueContains(byte[] signature) { |
|
|
|
private boolean incomingTransactionQueueContains(byte[] signature) { |
|
|
|
synchronized (incomingTransactions) { |
|
|
|
synchronized (incomingTransactions) { |
|
|
|
return incomingTransactions.stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature)); |
|
|
|
return incomingTransactions.keySet().stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void removeIncomingTransaction(byte[] signature) { |
|
|
|
private void removeIncomingTransaction(byte[] signature) { |
|
|
|
incomingTransactions.removeIf(t -> Arrays.equals(t.getSignature(), signature)); |
|
|
|
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void processIncomingTransactionsQueue() { |
|
|
|
private void processIncomingTransactionsQueue() { |
|
|
|
if (this.incomingTransactions.size() == 0) { |
|
|
|
if (this.incomingTransactions.isEmpty()) { |
|
|
|
// Don't bother locking if there are no new transactions to process
|
|
|
|
// Nothing to do?
|
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -856,87 +856,145 @@ public class Controller extends Thread { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try (final Repository repository = RepositoryManager.getRepository()) { |
|
|
|
|
|
|
|
// Take a snapshot of incomingTransactions, so we don't need to lock it while processing
|
|
|
|
|
|
|
|
Map<TransactionData, Boolean> incomingTransactionsCopy = Map.copyOf(this.incomingTransactions); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOGGER.debug("Processing incoming transactions queue (size {})...", incomingTransactionsCopy.size()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
List<Transaction> sigValidTransactions = new ArrayList<>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Signature validation round - does not require blockchain lock
|
|
|
|
|
|
|
|
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) { |
|
|
|
|
|
|
|
// Quick exit?
|
|
|
|
|
|
|
|
if (isStopping) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (Synchronizer.getInstance().isSyncRequestPending()) { |
|
|
|
|
|
|
|
LOGGER.debug("Breaking out of transaction signature validation with {} remaining, because a sync request is pending", incomingTransactionsCopy.size()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fall-through to importing, or we could not even attempt to import by changing following line to 'return'
|
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TransactionData transactionData = transactionEntry.getKey(); |
|
|
|
|
|
|
|
Transaction transaction = Transaction.fromData(repository, transactionData); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only validate signature if we haven't already done so
|
|
|
|
|
|
|
|
Boolean isSigValid = transactionEntry.getValue(); |
|
|
|
|
|
|
|
if (!Boolean.TRUE.equals(isSigValid)) { |
|
|
|
|
|
|
|
if (!transaction.isSignatureValid()) { |
|
|
|
|
|
|
|
String signature58 = Base58.encode(transactionData.getSignature()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOGGER.trace("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58); |
|
|
|
|
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Also add to invalidIncomingTransactions map
|
|
|
|
|
|
|
|
Long now = NTP.getTime(); |
|
|
|
|
|
|
|
if (now != null) { |
|
|
|
|
|
|
|
Long expiry = now + INVALID_TRANSACTION_RECHECK_INTERVAL; |
|
|
|
|
|
|
|
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58); |
|
|
|
|
|
|
|
// Add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
|
|
|
|
|
|
|
invalidUnconfirmedTransactions.put(signature58, expiry); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add mark signature as valid if transaction still exists in import queue
|
|
|
|
|
|
|
|
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
LOGGER.trace(() -> String.format("Transaction %s known to have valid signature", Base58.encode(transactionData.getSignature()))); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Signature valid - add to shortlist
|
|
|
|
|
|
|
|
sigValidTransactions.add(transaction); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (sigValidTransactions.isEmpty()) { |
|
|
|
|
|
|
|
// Don't bother locking if there are no new transactions to process
|
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); |
|
|
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); |
|
|
|
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) { |
|
|
|
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) { |
|
|
|
LOGGER.trace(() -> String.format("Too busy to process incoming transactions queue")); |
|
|
|
// This is not great if we've just spent a while doing mem-PoW during signature validation round above
|
|
|
|
|
|
|
|
LOGGER.debug("Too busy to process incoming transactions queue"); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (InterruptedException e) { |
|
|
|
} catch (InterruptedException e) { |
|
|
|
LOGGER.info("Interrupted when trying to acquire blockchain lock"); |
|
|
|
LOGGER.debug("Interrupted when trying to acquire blockchain lock"); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try (final Repository repository = RepositoryManager.getRepository()) { |
|
|
|
// Import transactions with valid signatures
|
|
|
|
LOGGER.debug("Processing incoming transactions queue (size {})...", this.incomingTransactions.size()); |
|
|
|
try { |
|
|
|
|
|
|
|
for (int i = 0; i < sigValidTransactions.size(); ++i) { |
|
|
|
// Take a copy of incomingTransactions so we can release the lock
|
|
|
|
|
|
|
|
List<TransactionData>incomingTransactionsCopy = new ArrayList<>(this.incomingTransactions); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Iterate through incoming transactions list
|
|
|
|
|
|
|
|
Iterator iterator = incomingTransactionsCopy.iterator(); |
|
|
|
|
|
|
|
while (iterator.hasNext()) { |
|
|
|
|
|
|
|
if (isStopping) { |
|
|
|
if (isStopping) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (Synchronizer.getInstance().isSyncRequestPending()) { |
|
|
|
if (Synchronizer.getInstance().isSyncRequestPending()) { |
|
|
|
LOGGER.debug("Breaking out of transaction processing loop with {} remaining, because a sync request is pending", incomingTransactionsCopy.size()); |
|
|
|
LOGGER.debug("Breaking out of transaction processing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
TransactionData transactionData = (TransactionData) iterator.next(); |
|
|
|
Transaction transaction = sigValidTransactions.get(i); |
|
|
|
Transaction transaction = Transaction.fromData(repository, transactionData); |
|
|
|
TransactionData transactionData = transaction.getTransactionData(); |
|
|
|
|
|
|
|
|
|
|
|
// Check signature
|
|
|
|
|
|
|
|
if (!transaction.isSignatureValid()) { |
|
|
|
|
|
|
|
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature", transactionData.getType().name(), Base58.encode(transactionData.getSignature()))); |
|
|
|
|
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ValidationResult validationResult = transaction.importAsUnconfirmed(); |
|
|
|
ValidationResult validationResult = transaction.importAsUnconfirmed(); |
|
|
|
|
|
|
|
|
|
|
|
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) { |
|
|
|
switch (validationResult) { |
|
|
|
|
|
|
|
case TRANSACTION_ALREADY_EXISTS: { |
|
|
|
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature()))); |
|
|
|
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature()))); |
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
break; |
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) { |
|
|
|
case NO_BLOCKCHAIN_LOCK: { |
|
|
|
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction", Base58.encode(transactionData.getSignature()))); |
|
|
|
// Is this even possible considering we acquired blockchain lock above?
|
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction %s", Base58.encode(transactionData.getSignature()))); |
|
|
|
continue; |
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case OK: { |
|
|
|
|
|
|
|
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()))); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (validationResult != ValidationResult.OK) { |
|
|
|
// All other invalid cases:
|
|
|
|
|
|
|
|
default: { |
|
|
|
final String signature58 = Base58.encode(transactionData.getSignature()); |
|
|
|
final String signature58 = Base58.encode(transactionData.getSignature()); |
|
|
|
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58)); |
|
|
|
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58)); |
|
|
|
|
|
|
|
|
|
|
|
Long now = NTP.getTime(); |
|
|
|
Long now = NTP.getTime(); |
|
|
|
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) { |
|
|
|
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) { |
|
|
|
Long expiryLength = INVALID_TRANSACTION_RECHECK_INTERVAL; |
|
|
|
Long expiryLength = INVALID_TRANSACTION_RECHECK_INTERVAL; |
|
|
|
|
|
|
|
|
|
|
|
if (validationResult == ValidationResult.TIMESTAMP_TOO_OLD) { |
|
|
|
if (validationResult == ValidationResult.TIMESTAMP_TOO_OLD) { |
|
|
|
// Use shorter recheck interval for expired transactions
|
|
|
|
// Use shorter recheck interval for expired transactions
|
|
|
|
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL; |
|
|
|
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Long expiry = now + expiryLength; |
|
|
|
Long expiry = now + expiryLength; |
|
|
|
LOGGER.debug("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58); |
|
|
|
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58); |
|
|
|
// Invalid, unconfirmed transaction has become stale - add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
|
|
|
// Invalid, unconfirmed transaction has become stale - add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
|
|
|
invalidUnconfirmedTransactions.put(signature58, expiry); |
|
|
|
invalidUnconfirmedTransactions.put(signature58, expiry); |
|
|
|
} |
|
|
|
} |
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
} |
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()))); |
|
|
|
// Transaction has been processed, even if only to reject it
|
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
removeIncomingTransaction(transactionData.getSignature()); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (DataException e) { |
|
|
|
|
|
|
|
LOGGER.error(String.format("Repository issue while processing incoming transactions", e)); |
|
|
|
|
|
|
|
} finally { |
|
|
|
} finally { |
|
|
|
LOGGER.debug("Finished processing incoming transactions queue"); |
|
|
|
LOGGER.debug("Finished processing incoming transactions queue"); |
|
|
|
blockchainLock.unlock(); |
|
|
|
blockchainLock.unlock(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch (DataException e) { |
|
|
|
|
|
|
|
LOGGER.error("Repository issue while processing incoming transactions", e); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void cleanupInvalidTransactionsList(Long now) { |
|
|
|
private void cleanupInvalidTransactionsList(Long now) { |
|
|
@ -1437,9 +1495,12 @@ public class Controller extends Thread { |
|
|
|
private void onNetworkTransactionMessage(Peer peer, Message message) { |
|
|
|
private void onNetworkTransactionMessage(Peer peer, Message message) { |
|
|
|
TransactionMessage transactionMessage = (TransactionMessage) message; |
|
|
|
TransactionMessage transactionMessage = (TransactionMessage) message; |
|
|
|
TransactionData transactionData = transactionMessage.getTransactionData(); |
|
|
|
TransactionData transactionData = transactionMessage.getTransactionData(); |
|
|
|
|
|
|
|
|
|
|
|
if (this.incomingTransactions.size() < MAX_INCOMING_TRANSACTIONS) { |
|
|
|
if (this.incomingTransactions.size() < MAX_INCOMING_TRANSACTIONS) { |
|
|
|
if (!this.incomingTransactions.contains(transactionData)) { |
|
|
|
synchronized (this.incomingTransactions) { |
|
|
|
this.incomingTransactions.add(transactionData); |
|
|
|
if (!incomingTransactionQueueContains(transactionData.getSignature())) { |
|
|
|
|
|
|
|
this.incomingTransactions.put(transactionData, Boolean.FALSE); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|