package org.ripple.power.txns.btc;
import java.io.EOFException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class NetworkMessageListener extends AbstractMessageListener {
/** Alert listeners */
private final List<AlertListener> alertListeners = new ArrayList<>();
/**
* Registers an alert listener
*
* @param listener Alert listener
*/
public void addListener(AlertListener listener) {
alertListeners.add(listener);
}
/**
* Handle an inventory request
*
* <p>This method is called when a 'getdata' message is received. The application
* should send the inventory items to the requesting peer. A 'notfound' message
* should be returned to the requesting peer if one or more items cannot be sent.</p>
*
* @param msg Message
* @param invList Inventory item list
*/
@Override
public void sendInventory(Message msg, List<InventoryItem> invList) {
Peer peer = msg.getPeer();
BTCLoader.debug(String.format("Processing 'getdata' from %s", peer.getAddress()));
//
// If this is a request restart, we need to skip over the items that have already
// been processed as indicated by the restart index contained in the message. Otherwise,
// start with the first inventory item.
//
List<InventoryItem> notFound = new ArrayList<>(invList.size());
int restart = msg.getRestartIndex();
msg.setRestartIndex(0);
int blocksSent = 0;
for (int i=restart; i<invList.size(); i++) {
//
// Defer the request if we have sent 25 blocks in the current batch. We will
// restart at this point after the current batch has been sent to the peer.
//
if (blocksSent == 25) {
msg.setRestartIndex(i);
break;
}
InventoryItem item = invList.get(i);
switch (item.getType()) {
case InventoryItem.INV_TX:
//
// Send a transaction from the memory pool
//
StoredTransaction tx;
synchronized(BTCLoader.txMap) {
tx = BTCLoader.txMap.get(item.getHash());
}
if (tx != null) {
Message txMsg = TransactionMessage.buildTransactionMessage(peer, tx.getBytes());
BTCLoader.networkHandler.sendMessage(txMsg);
BTCLoader.txSent.incrementAndGet();
BTCLoader.debug(String.format("Sent tx %s", tx.getHash()));
} else {
notFound.add(item);
}
break;
case InventoryItem.INV_BLOCK:
//
// Send a block from the database
//
try {
Block block = BTCLoader.blockStore.getBlock(item.getHash());
if (block != null) {
blocksSent++;
Message blockMsg = BlockMessage.buildBlockMessage(peer, block.getBytes());
BTCLoader.networkHandler.sendMessage(blockMsg);
BTCLoader.blocksSent.incrementAndGet();
BTCLoader.debug(String.format("Sent block %s", block.getHash()));
} else {
notFound.add(item);
}
} catch (BlockStoreException exc) {
notFound.add(item);
} catch (Throwable exc) {
BTCLoader.error("Unable to build message", exc);
notFound.add(item);
}
break;
case InventoryItem.INV_FILTERED_BLOCK:
//
// Send a filtered block if the peer has loaded a Bloom filter. The request
// will be ignored if there is no Bloom filter.
//
BloomFilter filter = peer.getBloomFilter();
if (filter != null) {
//
// Get the block from the database and locate any matching transactions.
// Send the Merkle block followed by the matching transactions (if any)
//
try {
Block block = BTCLoader.blockStore.getBlock(item.getHash());
if (block != null) {
List<Sha256Hash> matches = filter.findMatches(block);
sendMatchedTransactions(peer, block, matches);
BTCLoader.debug(String.format("Sent filtered block %s", block.getHash()));
} else {
notFound.add(item);
}
} catch (BlockStoreException exc) {
notFound.add(item);
} catch (Throwable exc) {
BTCLoader.error("Unable to build filtered block message", exc);
notFound.add(item);
}
}
break;
default:
notFound.add(item);
}
}
//
// Create a 'notfound' response if we didn't find all of the requested items
//
if (!notFound.isEmpty()) {
Message invMsg = NotFoundMessage.buildNotFoundMessage(peer, notFound);
BTCLoader.networkHandler.sendMessage(invMsg);
}
//
// Set up the restart if we didn't process all of the items
//
if (peer.getDeferredMessage() == null && msg.getRestartIndex() != 0) {
ByteBuffer msgBuffer = msg.getBuffer();
msgBuffer.rewind();
msg.setRestartBuffer(msgBuffer);
synchronized(peer) {
peer.setDeferredMessage(msg);
}
}
//
// Send an 'inv' message for the current chain head to restart
// the peer download if the previous 'getblocks' was not able
// to return all of the blocks leading to the chain head
//
if (peer.isIncomplete() && peer.getDeferredMessage()==null) {
peer.setIncomplete(false);
List<InventoryItem> blockList = new ArrayList<>(1);
blockList.add(new InventoryItem(InventoryItem.INV_BLOCK, BTCLoader.blockStore.getChainHead()));
Message invMessage = InventoryMessage.buildInventoryMessage(peer, blockList);
BTCLoader.networkHandler.sendMessage(invMessage);
}
}
/**
* Handle an inventory item available notification
*
* <p>This method is called when an 'inv' message is received. The application
* should request any needed inventory items from the peer.</p>
*
* @param msg Message
* @param invList Inventory item list
*/
@Override
public void requestInventory(Message msg, List<InventoryItem> invList) {
Peer peer = msg.getPeer();
int txRequests = 0;
//
// Process the inventory items
//
for (InventoryItem item : invList) {
PeerRequest request = new PeerRequest(item.getHash(), item.getType(), peer);
switch (item.getType()) {
case InventoryItem.INV_TX:
//
// Ignore large transaction broadcasts to avoid running out of storage
//
if (txRequests >= 50) {
BTCLoader.warn(String.format("More than 50 tx entries in 'inv' message from %s - ignoring",
peer.getAddress()));
continue;
}
//
// Skip the transaction if we have already seen it
//
boolean newTx;
synchronized(BTCLoader.txMap) {
newTx = (BTCLoader.recentTxMap.get(item.getHash()) == null);
}
if (!newTx)
continue;
//
// Ignore transactions if we are down-level since they will be orphaned
// until we catch up to the rest of the network
//
if (BTCLoader.blockStore.getChainHeight() < BTCLoader.networkChainHeight-5)
continue;
//
// Request the transaction if it is not in the memory pool and has not
// been requested. We add the request at the front of the queue so it
// does not get stuck behind pending block requests.
//
try {
if (BTCLoader.blockStore.isNewTransaction(item.getHash())) {
synchronized(BTCLoader.pendingRequests) {
if (BTCLoader.recentTxMap.get(item.getHash()) == null &&
!BTCLoader.pendingRequests.contains(request) &&
!BTCLoader.processedRequests.contains(request)) {
BTCLoader.pendingRequests.add(0, request);
txRequests++;
}
}
}
} catch (BlockStoreException exc) {
// Unable to check database - wait for another inventory broadcast
}
break;
case InventoryItem.INV_BLOCK:
//
// Request the block if it is not in the database and has not been requested.
// Block requests are added to the end of the queue so that we don't hold
// up transaction requests while we update the block chain.
//
try {
if (BTCLoader.blockStore.isNewBlock(item.getHash())) {
synchronized(BTCLoader.pendingRequests) {
if (!BTCLoader.pendingRequests.contains(request) &&
!BTCLoader.processedRequests.contains(request))
BTCLoader.pendingRequests.add(request);
}
}
} catch (BlockStoreException exc) {
// Unable to check database - wait for another inventory broadcast
}
break;
}
}
}
/**
* Handle a request not found
*
* <p>This method is called when a 'notfound' message is received. It notifies the
* application that an inventory request cannot be completed because the item was
* not found. The request can be discarded or retried by sending it to a different
* peer.</p>
*
* @param msg Message
* @param invList Inventory item list
*/
@Override
public void requestNotFound(Message msg, List<InventoryItem> invList) {
for (InventoryItem item : invList) {
synchronized(BTCLoader.pendingRequests) {
//
// Remove the request from the processedRequests list and put it
// back on the pendingRequests list. The network handler will
// then send the request to a different peer or discard it if
// all of the available peers have been contacted.
//
Iterator<PeerRequest> it = BTCLoader.processedRequests.iterator();
while (it.hasNext()) {
PeerRequest request = it.next();
if (request.getType()==item.getType() && request.getHash().equals(item.getHash())) {
it.remove();
BTCLoader.pendingRequests.add(request);
break;
}
}
}
}
}
/**
* Handle a request for the transaction memory pool
*
* <p>This method is called when a 'mempool' message is received. The application
* should return an 'inv' message listing the transactions in the memory pool.</p>
*
* @param msg Message
*/
@Override
public void requestMemoryPool(Message msg) {
//
// Get the list of transaction identifiers in the memory pool. We will send a maximum
// of 1000 transaction identifiers.
//
List<InventoryItem> invList;
synchronized(BTCLoader.txMap) {
Set<Sha256Hash> txSet = BTCLoader.txMap.keySet();
invList = new ArrayList<>(txSet.size());
Iterator<Sha256Hash> it = txSet.iterator();
while (it.hasNext() && invList.size() < 1000)
invList.add(new InventoryItem(InventoryItem.INV_TX, it.next()));
}
//
// Send the 'inv' message
//
Message invMsg = InventoryMessage.buildInventoryMessage(msg.getPeer(), invList);
BTCLoader.networkHandler.sendMessage(invMsg);
}
/**
* Process a peer address list
*
* <p>This method is called when an 'addr' message is received.</p>
*
* @param msg Message
* @param addresses Peer address list
*/
@Override
public void processAddresses(Message msg, List<PeerAddress> addresses) {
long oldestTime = System.currentTimeMillis()/1000 - BTCLoader.MAX_PEER_ADDRESS_AGE;
//
// Add new addresses to the peer address list and update the timestamp and services
// for existing entries. We will not include peers that provide no services or peers
// that that are too old. The peer list is sorted by timestamp from newest to oldest.
// Existing entries are updated in-place.
//
for(PeerAddress addr:addresses){
if(addr.getServices()!=0 && addr.getTimeStamp()>oldestTime &&
!addr.getAddress().isAnyLocalAddress() &&
!addr.getAddress().isLoopbackAddress() &&
addr.getPort()>0 && addr.getPort()<65536 &&
!addr.equals(BTCLoader.listenAddress)){
long timeStamp = addr.getTimeStamp();
synchronized(BTCLoader.peerAddresses) {
PeerAddress mapAddress = BTCLoader.peerMap.get(addr);
if (mapAddress == null) {
int index, lowIndex, highIndex;
int lastElem = BTCLoader.peerAddresses.size()-1;
if (lastElem < 0) {
BTCLoader.peerAddresses.add(addr);
} else if (BTCLoader.peerAddresses.get(lastElem).getTimeStamp() >= timeStamp) {
BTCLoader.peerAddresses.add(addr);
} else {
lowIndex = -1;
highIndex = lastElem;
while (highIndex-lowIndex > 1) {
index = (highIndex-lowIndex)/2+lowIndex;
if (BTCLoader.peerAddresses.get(index).getTimeStamp() < timeStamp)
highIndex = index;
else
lowIndex = index;
}
BTCLoader.peerAddresses.add(highIndex, addr);
}
BTCLoader.peerMap.put(addr, addr);
} else {
mapAddress.setTimeStamp(Math.max(mapAddress.getTimeStamp(), timeStamp));
mapAddress.setServices(addr.getServices());
}
}
}
}
}
/**
* Process an alert
*
* <p>This method is called when an 'alert' message is received</p>
*
* @param msg Message
* @param alert Alert
*/
@Override
public void processAlert(Message msg, Alert alert) {
//
// Store a new alert in our database
//
try {
if (BTCLoader.blockStore.isNewAlert(alert.getID())) {
//
// Store the alert in our database
//
BTCLoader.blockStore.storeAlert(alert);
//
// Process alert cancels
//
int cancelID = alert.getCancelID();
if (cancelID != 0)
BTCLoader.blockStore.cancelAlert(cancelID);
List<Integer> cancelSet = alert.getCancelSet();
for (Integer id : cancelSet)
BTCLoader.blockStore.cancelAlert(id);
//
// Broadcast the alert to our peers
//
if (alert.getRelayTime() > System.currentTimeMillis()/1000) {
Message alertMsg = AlertMessage.buildAlertMessage(null, alert);
BTCLoader.networkHandler.broadcastMessage(alertMsg);
}
}
} catch (BlockStoreException exc) {
// Can't store the alert - let it go
}
//
// Notify alert listeners
//
synchronized(BTCLoader.alerts) {
BTCLoader.alerts.add(alert);
}
for(AlertListener listener:alertListeners){
listener.alertReceived(alert);
}
}
/**
* Process a block
*
* <p>This method is called when a 'block' message is received</p>
*
* @param msg Message
* @param block Block
*/
@Override
public void processBlock(Message msg, Block block) {
//
// Indicate the block is being processed so the block request won't be rebroadcast
//
synchronized(BTCLoader.pendingRequests) {
for (PeerRequest chkRequest : BTCLoader.processedRequests) {
if (chkRequest.getType()==InventoryItem.INV_BLOCK && chkRequest.getHash().equals(block.getHash())) {
chkRequest.setProcessing(true);
break;
}
}
}
BTCLoader.blocksReceived.incrementAndGet();
//
// Add the block to the database handler queue
//
try {
BTCLoader.databaseQueue.put(block);
} catch (InterruptedException exc) {
// We should never block since the queue is backed by a linked list
}
}
/**
* Process a Bloom filter clear request
*
* <p>This method is called when a 'filterclear' message is received. The peer
* Bloom filter has been cleared before this method is called.</p>
*
* @param msg Message
* @param oldFilter Previous bloom filter
*/
@Override
public void processFilterClear(Message msg, BloomFilter oldFilter) {
synchronized(BTCLoader.bloomFilters) {
BTCLoader.bloomFilters.remove(oldFilter);
}
}
/**
* Process a Bloom filter load request
*
* <p>This method is called when a 'filterload' message is received. The peer bloom
* filter has been updated before this method is called.</p>
*
* @param msg Message
* @param oldFilter Previous bloom filter
* @param newFilter New bloom filter
*/
@Override
public void processFilterLoad(Message msg, BloomFilter oldFilter, BloomFilter newFilter) {
synchronized(BTCLoader.bloomFilters) {
if (oldFilter != null)
BTCLoader.bloomFilters.remove(oldFilter);
BTCLoader.bloomFilters.add(newFilter);
}
}
/**
* Process a get address request
*
* <p>This method is called when a 'getaddr' message is received. The application should
* call AddressMessage.buildAddressMessage() to build the response message.</p>
*
* @param msg Message
*/
@Override
public void processGetAddress(Message msg) {
List<PeerAddress> addressList;
synchronized(BTCLoader.peerAddresses) {
addressList = new ArrayList<>(BTCLoader.peerAddresses);
}
Message addrMsg = AddressMessage.buildAddressMessage(msg.getPeer(), addressList, BTCLoader.listenAddress);
BTCLoader.networkHandler.sendMessage(addrMsg);
}
/**
* Process a request for the latest blocks
*
* <p>This method is called when a 'getblocks' message is received. The application should
* use the locator block list to find the latest common block and then send an 'inv'
* message to the peer for the blocks following the common block.</p>
*
* @param msg Message
* @param version Negotiated version
* @param blockList Locator block list
* @param stopBlock Stop block (Sha256Hash.ZERO_HASH if all blocks should be sent)
*/
@Override
public void processGetBlocks(Message msg, int version, List<Sha256Hash> blockList, Sha256Hash stopBlock) {
Peer peer = msg.getPeer();
BTCLoader.debug(String.format("Processing 'getblocks' from %s", peer.getAddress()));
//
// We will ignore a 'getblocks' message if we are still processing a prior request
//
if (peer.getDeferredMessage() != null)
return;
try {
//
// Check each locator until we find one that is on the main chain
//
boolean foundJunction = false;
Sha256Hash startBlock = null;
for (Sha256Hash blockHash : blockList) {
if (BTCLoader.blockStore.isOnChain(blockHash)) {
startBlock = blockHash;
foundJunction = true;
BTCLoader.debug(String.format("Found junction block %s", startBlock));
break;
}
}
//
// We go back to the genesis block if none of the supplied locators are on the main chain
//
if (!foundJunction)
startBlock = new Sha256Hash(NetParams.GENESIS_BLOCK_HASH);
//
// Get the chain list
//
List<InventoryItem> chainList = BTCLoader.blockStore.getChainList(startBlock, stopBlock);
if (chainList.size() >= 500)
peer.setIncomplete(true);
//
// Build the 'inv' response
//
BTCLoader.debug(String.format("Returning %d inventory blocks", chainList.size()));
Message invMsg = InventoryMessage.buildInventoryMessage(peer, chainList);
BTCLoader.networkHandler.sendMessage(invMsg);
} catch (BlockStoreException exc) {
// Can't access the database, so just ignore the 'getblocks' request
}
}
/**
* Process a request for the latest headers
*
* <p>This method is called when a 'getheaders' message is received. The application should
* use the locator block list to find the latest common block and then send a 'headers'
* message to the peer for the blocks following the common block.</p>
*
* @param msg Message
* @param version Negotiated version
* @param blockList Locator block list
* @param stopBlock Stop block (Sha256Hash.ZERO_HASH if all blocks should be sent)
*/
@Override
public void processGetHeaders(Message msg, int version, List<Sha256Hash> blockList, Sha256Hash stopBlock) {
Peer peer = msg.getPeer();
BTCLoader.debug(String.format("Processing 'getheaders' from %s", peer.getAddress()));
//
// Check each locator until we find one that is on the main chain
//
try {
boolean foundJunction = false;
Sha256Hash startBlock = null;
for (Sha256Hash blockHash : blockList) {
if (BTCLoader.blockStore.isOnChain(blockHash)) {
foundJunction = true;
startBlock = blockHash;
BTCLoader.debug(String.format("Found junction block %s", startBlock));
break;
}
}
//
// We go back to the genesis block if none of the supplied locators are on the main chain
//
if (!foundJunction)
startBlock = new Sha256Hash(NetParams.GENESIS_BLOCK_HASH);
//
// Get the chain list
//
List<BlockHeader> chainList = BTCLoader.blockStore.getHeaderList(startBlock, stopBlock);
//
// Build the 'headers' response
//
BTCLoader.debug(String.format("Returning %d headers", chainList.size()));
Message hdrMsg = HeadersMessage.buildHeadersMessage(peer, chainList);
BTCLoader.networkHandler.sendMessage(hdrMsg);
} catch (BlockStoreException exc) {
// Can't access the database, so just ignore the 'getheaders' request
}
}
/**
* Process a ping
*
* <p>This method is called when a 'ping' message is received. The application should
* return a 'pong' message to the sender. This method will not be called if the sender
* has not implemented BIP0031.</p>
*
* @param msg Message
* @param nonce Nonce
*/
@Override
public void processPing(Message msg, long nonce) {
//
// Send the 'pong' response
//
Message pongMsg = PongMessage.buildPongMessage(msg.getPeer(), nonce);
BTCLoader.networkHandler.sendMessage(pongMsg);
}
/**
* Process a pong
*
* <p>This method is called when a 'pong' message is received.</p>
*
* @param msg Message
* @param nonce Nonce
*/
@Override
public void processPong(Message msg, long nonce) {
msg.getPeer().setPing(false);
BTCLoader.info(String.format("'pong' response received from %s", msg.getPeer().getAddress()));
}
/**
* Process a message rejection
*
* <p>This method is called when a 'reject' message is received.</p>
*
* @param msg Message
* @param cmd Failing message command
* @param reasonCode Failure reason code
* @param description Description of the failure
* @param hash Item hash or Sha256Hash.ZERO_HASH
*/
@Override
public void processReject(Message msg, String cmd, int reasonCode, String description, Sha256Hash hash) {
//
// Log the message
//
String reason = RejectMessage.reasonCodes.get(reasonCode);
if (reason == null)
reason = Integer.toString(reasonCode, 16);
BTCLoader.error(String.format("Message rejected by %s\n Command %s, Reason %s - %s\n %s",
msg.getPeer().getAddress(), cmd, reason, description, hash));
}
/**
* Process a transaction
*
* <p>This method is called when a 'tx' message is received.</p>
*
* @param msg Message
* @param tx Transaction
*/
@Override
public void processTransaction(Message msg, Transaction tx) {
Peer peer = msg.getPeer();
Sha256Hash txHash = tx.getHash();
int reasonCode = 0;
//
// Remove the request from the processedRequests list
//
synchronized(BTCLoader.pendingRequests) {
Iterator<PeerRequest> it = BTCLoader.processedRequests.iterator();
while (it.hasNext()) {
PeerRequest request = it.next();
if (request.getType()==InventoryItem.INV_TX && request.getHash().equals(txHash)) {
it.remove();
break;
}
}
}
//
// Ignore the transaction if we have already seen it. Otherwise, add it to
// the recent transaction list
//
boolean duplicateTx = false;
synchronized(BTCLoader.txMap) {
if (BTCLoader.recentTxMap.get(txHash) != null) {
duplicateTx = true;
} else {
BTCLoader.recentTxMap.put(txHash, txHash);
}
}
if (duplicateTx)
return;
try {
//
// Don't relay the transaction if the version is not 1 (BIP0034)
//
if (tx.getVersion() != 1)
throw new VerificationException(String.format("Transaction version %d is not valid",
tx.getVersion()),
RejectMessage.REJECT_NONSTANDARD, txHash);
//
// Verify the transaction
//
tx.verify(true);
//
// Coinbase transactions cannot be relayed
//
if (tx.isCoinBase())
throw new VerificationException("Coinbase transaction cannot be relayed",
RejectMessage.REJECT_INVALID, txHash);
//
// Validate the transaction
//
if (!validateTx(tx))
return;
//
// Broadcast the transaction to our peers
//
broadcastTx(tx);
//
// Process orphan transactions that were waiting on this transaction
//
List<StoredTransaction> orphanTxList;
synchronized(BTCLoader.txMap) {
orphanTxList = BTCLoader.orphanTxMap.remove(txHash);
}
if (orphanTxList != null) {
for (StoredTransaction orphanStoredTx : orphanTxList) {
Transaction orphanTx = orphanStoredTx.getTransaction();
if (validateTx(orphanTx))
broadcastTx(orphanTx);
}
}
//
// Clean up the transaction pools
//
synchronized(BTCLoader.txMap) {
// Clean up the transaction memory pool
if (BTCLoader.txMap.size() > 5000) {
Set<Sha256Hash> txSet = BTCLoader.txMap.keySet();
Iterator<Sha256Hash> it = txSet.iterator();
do {
it.next();
it.remove();
} while (txSet.size() > 4000);
}
// Clean up the recent transaction list
if (BTCLoader.recentTxMap.size() > 5000) {
Set<Sha256Hash> txSet = BTCLoader.recentTxMap.keySet();
Iterator<Sha256Hash> it = txSet.iterator();
do {
it.next();
it.remove();
} while (txSet.size() > 4000);
}
// Clean up the spent outputs list
if (BTCLoader.spentOutputsMap.size() > 25000) {
Set<OutPoint> txSet = BTCLoader.spentOutputsMap.keySet();
Iterator<OutPoint> it = txSet.iterator();
do {
it.next();
it.remove();
} while (txSet.size() > 20000);
}
// Clean up the orphan transactions list
if (BTCLoader.orphanTxMap.size() > 5000) {
Set<Sha256Hash> txSet = BTCLoader.orphanTxMap.keySet();
Iterator<Sha256Hash> it = txSet.iterator();
do {
it.next();
it.remove();
} while (txSet.size() > 4000);
}
}
} catch (EOFException exc) {
BTCLoader.error(String.format("End-of-data while processing 'tx' message from %s", peer.getAddress()));
reasonCode = RejectMessage.REJECT_MALFORMED;
BTCLoader.txRejected.incrementAndGet();
if (peer.getVersion() >= 70002) {
Message rejectMsg = RejectMessage.buildRejectMessage(peer, "tx", reasonCode, exc.getMessage());
BTCLoader.networkHandler.sendMessage(rejectMsg);
}
} catch (VerificationException exc) {
BTCLoader.error(String.format("Message verification failed for 'tx' message from %s\n %s\n %s",
peer.getAddress(), exc.getMessage(), exc.getHash()));
reasonCode = exc.getReason();
BTCLoader.txRejected.incrementAndGet();
if (peer.getVersion() >= 70002) {
Message rejectMsg = RejectMessage.buildRejectMessage(peer, "tx", reasonCode,
exc.getMessage(), exc.getHash());
BTCLoader.networkHandler.sendMessage(rejectMsg);
}
}
//
// Increment the banscore for the peer if this is an invalid and malformed transaction
//
synchronized(peer) {
if (reasonCode == RejectMessage.REJECT_MALFORMED || reasonCode == RejectMessage.REJECT_INVALID) {
int banScore = peer.getBanScore() + 5;
peer.setBanScore(banScore);
if (banScore >= BTCLoader.MAX_BAN_SCORE)
peer.setDisconnect(true);
}
}
}
/**
* Process a version message
*
* <p>This method is called when a 'version' message is received. The application
* should return a 'verack' message to the sender if the connection is accepted.</p>
*
* @param msg Message
* @param localAddress Local address as seen by the peer
*/
@Override
public void processVersion(Message msg, PeerAddress localAddress) {
Peer peer = msg.getPeer();
peer.incVersionCount();
BTCLoader.info(String.format("Peer %s: Protocol level %d, Services %d, Agent %s, Height %d, "+
"Relay blocks %s, Relay tx %s",
peer.getAddress(), peer.getVersion(), peer.getServices(),
peer.getUserAgent(), peer.getHeight(),
peer.shouldRelayBlocks()?"Yes":"No",
peer.shouldRelayTx()?"Yes":"No"));
Message ackMsg = VersionAckMessage.buildVersionAckMessage(peer);
BTCLoader.networkHandler.sendMessage(ackMsg);
//
// Set our local address from the Version message if it hasn't been set yet
//
if (BTCLoader.listenAddress == null)
BTCLoader.listenAddress = localAddress;
}
/**
* Process a version acknowledgment
*
* <p>This method is called when a 'verack' message is received.</p>
*
* @param msg Message
*/
@Override
public void processVersionAck(Message msg) {
msg.getPeer().incVersionCount();
}
/**
* Sends a 'merkleblock' message followed by 'tx' messages for the matched transactions
*
* @param peer Destination peer
* @param block Block containing the transactions
* @param matches List of matching transactions
*/
public void sendMatchedTransactions(Peer peer, Block block, List<Sha256Hash> matches) {
//
// Build the index list for the matching transactions
//
List<Integer> txIndexes;
List<Transaction> txList = block.getTransactions();
if (matches.isEmpty()) {
txIndexes = new ArrayList<>();
} else {
txIndexes = new ArrayList<>(matches.size());
int index = 0;
for (Transaction tx : txList) {
if (matches.contains(tx.getHash()))
txIndexes.add(index);
index++;
}
}
//
// Build and send the 'merkleblock' message
//
Message blockMsg = MerkleBlockMessage.buildMerkleBlockMessage(peer, block, txIndexes);
BTCLoader.networkHandler.sendMessage(blockMsg);
BTCLoader.filteredBlocksSent.incrementAndGet();
//
// Send a 'tx' message for each matching transaction
//
for(Integer txIndex:txIndexes){
Transaction tx = txList.get(txIndex);
Message txMsg = TransactionMessage.buildTransactionMessage(peer, tx);
BTCLoader.networkHandler.sendMessage(txMsg);
BTCLoader.txSent.incrementAndGet();
}
}
/**
* Retry an orphan transaction
*
* @param tx Transaction
*/
public void retryOrphanTransaction(Transaction tx) {
try {
if (validateTx(tx))
broadcastTx(tx);
} catch (EOFException | VerificationException exc) {
// Ignore the transaction since it is no longer valid
}
}
/**
* Validates the transaction
*
* @param tx Transaction
* @return TRUE if the transaction is valid
* @throws EOFException End-of-data processing script
* @throws VerificationException Transaction validation failed
*/
private boolean validateTx(Transaction tx) throws EOFException, VerificationException {
Sha256Hash txHash = tx.getHash();
BigInteger totalInput = BigInteger.ZERO;
BigInteger totalOutput = BigInteger.ZERO;
boolean nonFinalTx = false;
boolean nonFinalTxInput = false;
//
// The transaction must be final. If the transaction lock time is specified as a block height,
// the block height must not be greater than the current chain height+1. The reference code
// does not perform a check if a timestamp is used instead of a block height.
//
if (tx.getLockTime()<=500000000L && (int)tx.getLockTime()>BTCLoader.networkChainHeight+1)
nonFinalTx = true;
//
// Validate the transaction outputs
//
List<TransactionOutput> outputs = tx.getOutputs();
for (TransactionOutput output : outputs) {
// Dust transactions are not relayed - a dust transaction is one where the minimum
// relay fee is greater than 1/3 of the output value, assuming a single 148-byte input
// to spend the output
BigInteger chkValue = output.getValue().multiply(BigInteger.valueOf(1000)).divide(
BigInteger.valueOf(3*(output.getScriptBytes().length+9+148)));
if (chkValue.compareTo(BTCLoader.MIN_TX_RELAY_FEE) < 0)
throw new VerificationException("Dust transactions are not relayed",
RejectMessage.REJECT_DUST, tx.getHash());
// Non-standard payment types are not relayed
int paymentType = Script.getPaymentType(output.getScriptBytes());
if (paymentType != ScriptOpCodes.PAY_TO_PUBKEY_HASH &&
paymentType != ScriptOpCodes.PAY_TO_PUBKEY &&
paymentType != ScriptOpCodes.PAY_TO_SCRIPT_HASH &&
paymentType != ScriptOpCodes.PAY_TO_MULTISIG &&
paymentType != ScriptOpCodes.PAY_TO_NOBODY) {
BTCLoader.dumpData("Failing Script", output.getScriptBytes());
throw new VerificationException("Non-standard payment types are not relayed",
RejectMessage.REJECT_NONSTANDARD, txHash);
}
// Add the output value to the total output value for the transaction
totalOutput = totalOutput.add(output.getValue());
}
//
// Validate the transaction inputs
//
List<OutPoint> spentOutputs = new ArrayList<>();
List<TransactionInput> inputs = tx.getInputs();
boolean orphanTx = false;
boolean duplicateTx = false;
Sha256Hash orphanHash = null;
for (TransactionInput input : inputs) {
// A transaction input is non-final if the sequence number is not -1
if (input.getSeqNumber() != -1)
nonFinalTxInput = true;
// Script size must not exceed 500 bytes
if (input.getScriptBytes().length > 500)
throw new VerificationException("Input script size greater than 500 bytes",
RejectMessage.REJECT_NONSTANDARD, txHash);
// Connected output must not be spent
OutPoint outPoint = input.getOutPoint();
StoredOutput output = null;
Sha256Hash spendHash;
boolean outputSpent = false;
synchronized(BTCLoader.txMap) {
spendHash = BTCLoader.spentOutputsMap.get(outPoint);
}
if (spendHash == null) {
// Connected output is not in the recently spent list, check the memory pool
StoredTransaction outTx;
synchronized(BTCLoader.txMap) {
outTx = BTCLoader.txMap.get(outPoint.getHash());
}
if (outTx != null) {
// Transaction is in the memory pool, get the connected output
Transaction poolTx = outTx.getTransaction();
List<TransactionOutput> txOutputs = poolTx.getOutputs();
for (TransactionOutput txOutput : txOutputs) {
if (txOutput.getIndex() == outPoint.getIndex()) {
totalInput = totalInput.add(txOutput.getValue());
output = new StoredOutput(txOutput.getIndex(), txOutput.getValue(),
txOutput.getScriptBytes(), poolTx.isCoinBase());
break;
}
}
if (output == null)
throw new VerificationException(String.format(
"Transaction references non-existent output\n Tx %s",
txHash), RejectMessage.REJECT_INVALID, txHash);
} else {
// Transaction is not in the memory pool, check the database
try {
output = BTCLoader.blockStore.getTxOutput(outPoint);
if (output == null) {
orphanTx = true;
orphanHash = outPoint.getHash();
} else if (output.isSpent()) {
outputSpent = true;
} else {
totalInput = totalInput.add(output.getValue());
}
} catch (BlockStoreException exc) {
orphanTx = true;
orphanHash = outPoint.getHash();
}
}
} else if (!spendHash.equals(txHash)) {
outputSpent = true;
} else {
duplicateTx = true;
}
// Stop now if we have a problem
if (duplicateTx || orphanTx)
break;
// Error if the output has been spent
if (outputSpent)
throw new VerificationException("Input already spent", RejectMessage.REJECT_DUPLICATE, txHash);
// Check for immature coinbase transaction
if (output.isCoinBase()) {
try {
int txDepth = BTCLoader.blockStore.getTxDepth(outPoint.getHash());
txDepth += BTCLoader.networkChainHeight - BTCLoader.blockStore.getChainHeight();
if (txDepth < BTCLoader.COINBASE_MATURITY)
throw new VerificationException("Spending immature coinbase output",
RejectMessage.REJECT_INVALID, txHash);
} catch (BlockStoreException exc) {
// Can't check transaction depth - let it go
}
}
// Check for canonical signatures and public keys
int paymentType = Script.getPaymentType(output.getScriptBytes());
List<byte[]> dataList = Script.getData(input.getScriptBytes());
int canonicalType = 0;
switch (paymentType) {
case ScriptOpCodes.PAY_TO_PUBKEY:
// First data element is signature
if (dataList.isEmpty() || !ECKey.isSignatureCanonical(dataList.get(0)))
canonicalType = 1;
break;
case ScriptOpCodes.PAY_TO_PUBKEY_HASH:
// First data element is signature, second data element is public key
if (dataList.isEmpty() || !ECKey.isSignatureCanonical(dataList.get(0)))
canonicalType = 1;
else if (dataList.size() < 2 || !ECKey.isPubKeyCanonical(dataList.get(1)))
canonicalType = 2;
break;
case ScriptOpCodes.PAY_TO_MULTISIG:
// All data elements are public keys
for (byte[] sigBytes : dataList) {
if (!ECKey.isSignatureCanonical(sigBytes)) {
canonicalType = 1;
break;
}
}
}
if (canonicalType == 1)
throw new VerificationException("Non-canonical signature", RejectMessage.REJECT_NONSTANDARD, txHash);
if (canonicalType == 2)
throw new VerificationException("Non-canonical public key", RejectMessage.REJECT_NONSTANDARD, txHash);
// Add the output to the spent outputs list
spentOutputs.add(outPoint);
}
//
// Don't relay a non-final transaction
//
if (nonFinalTx && nonFinalTxInput)
throw new VerificationException("Non-final transactions are not relayed");
//
// Ignore a duplicate transaction (race condition among message handler threads)
//
if (duplicateTx)
return false;
//
// Save an orphan transaction for later
//
if (orphanTx) {
StoredTransaction storedTx = new StoredTransaction(tx);
storedTx.setParent(orphanHash);
synchronized(BTCLoader.txMap) {
List<StoredTransaction> orphanList = BTCLoader.orphanTxMap.get(orphanHash);
if (orphanList == null) {
orphanList = new ArrayList<>();
orphanList.add(storedTx);
BTCLoader.orphanTxMap.put(orphanHash, orphanList);
} else {
orphanList.add(storedTx);
}
}
return false;
}
//
// Check for insufficient transaction fee
//
BigInteger totalFee = totalInput.subtract(totalOutput);
if (totalFee.signum() < 0){
throw new VerificationException("Transaction output value exceeds transaction input value",
RejectMessage.REJECT_INVALID, txHash);
}
int txLength = tx.getBytes().length;
int feeMultiplier = txLength/1000;
if (txLength > BTCLoader.MAX_FREE_TX_SIZE) {
BigInteger minFee = BTCLoader.MIN_TX_RELAY_FEE.multiply(BigInteger.valueOf(feeMultiplier+1));
if (totalFee.compareTo(minFee) < 0){
throw new VerificationException("Insufficient transaction fee",
RejectMessage.REJECT_INSUFFICIENT_FEE, txHash);
}
}
//
// Store the transaction in the memory pool (maximum size we will store is 50KB)
//
if (txLength <= 50*1024) {
StoredTransaction storedTx = new StoredTransaction(tx);
synchronized(BTCLoader.txMap) {
if (BTCLoader.txMap.get(txHash) == null) {
BTCLoader.txMap.put(txHash, storedTx);
BTCLoader.txReceived.incrementAndGet();
for(OutPoint outPoint:spentOutputs){
BTCLoader.spentOutputsMap.put(outPoint, txHash);
}
}
}
}
return true;
}
/**
* Broadcast the transaction
*
* @param tx Transaction
* @throws EOFException End-of-data processing script
*/
private void broadcastTx(Transaction tx) throws EOFException {
Sha256Hash txHash = tx.getHash();
//
// Send an 'inv' message to the broadcast peers (full nodes)
//
List<InventoryItem> invList = new ArrayList<>(1);
invList.add(new InventoryItem(InventoryItem.INV_TX, txHash));
Message invMsg = InventoryMessage.buildInventoryMessage(null, invList);
invMsg.setInventoryType(InventoryItem.INV_TX);
BTCLoader.networkHandler.broadcastMessage(invMsg);
//
// Copy the current list of Bloom filters
//
List<BloomFilter> filters;
synchronized(BTCLoader.bloomFilters) {
filters = new ArrayList<>(BTCLoader.bloomFilters);
}
//
// Check each filter for a match and notify the SPV peer
//
for (BloomFilter filter : filters) {
Peer peer = filter.getPeer();
//
// Remove the filter if the peer is no longer connected
//
if (!peer.isConnected()) {
synchronized(BTCLoader.bloomFilters) {
BTCLoader.bloomFilters.remove(filter);
}
continue;
}
//
// Check the transaction against the filter and send an 'inv' message if it is a match
//
if (filter.checkTransaction(tx)) {
invMsg = InventoryMessage.buildInventoryMessage(peer, invList);
BTCLoader.networkHandler.sendMessage(invMsg);
}
}
}
}