/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.net.server;
import org.apache.commons.collections4.map.LRUMap;
import org.ethereum.config.NodeFilter;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.Block;
import org.ethereum.core.BlockWrapper;
import org.ethereum.core.PendingState;
import org.ethereum.core.Transaction;
import org.ethereum.db.ByteArrayWrapper;
import org.ethereum.facade.Ethereum;
import org.ethereum.net.message.ReasonCode;
import org.ethereum.net.rlpx.Node;
import org.ethereum.sync.SyncManager;
import org.ethereum.sync.SyncPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.*;
import static org.ethereum.net.message.ReasonCode.DUPLICATE_PEER;
import static org.ethereum.net.message.ReasonCode.TOO_MANY_PEERS;
/**
* @author Roman Mandeleil
* @since 11.11.2014
*/
@Component
public class ChannelManager {
private static final Logger logger = LoggerFactory.getLogger("net");
// If the inbound peer connection was dropped by us with a reason message
// then we ban that peer IP on any connections for some time to protect from
// too active peers
private static final int inboundConnectionBanTimeout = 10 * 1000;
private List<Channel> newPeers = new CopyOnWriteArrayList<>();
private final Map<ByteArrayWrapper, Channel> activePeers = new ConcurrentHashMap<>();
private ScheduledExecutorService mainWorker = Executors.newSingleThreadScheduledExecutor();
private int maxActivePeers;
private Map<InetAddress, Date> recentlyDisconnected = Collections.synchronizedMap(new LRUMap<InetAddress, Date>(500));
private NodeFilter trustedPeers;
/**
* Queue with new blocks from other peers
*/
private BlockingQueue<BlockWrapper> newForeignBlocks = new LinkedBlockingQueue<>();
/**
* Queue with new peers used for after channel init tasks
*/
private BlockingQueue<Channel> newActivePeers = new LinkedBlockingQueue<>();
private Thread blockDistributeThread;
private Thread txDistributeThread;
Random rnd = new Random(); // Used for distributing new blocks / hashes logic
@Autowired
SyncPool syncPool;
@Autowired
private Ethereum ethereum;
@Autowired
private PendingState pendingState;
private SystemProperties config;
private SyncManager syncManager;
private PeerServer peerServer;
@Autowired
private ChannelManager(final SystemProperties config, final SyncManager syncManager,
final PeerServer peerServer) {
this.config = config;
this.syncManager = syncManager;
this.peerServer = peerServer;
maxActivePeers = config.maxActivePeers();
trustedPeers = config.peerTrusted();
mainWorker.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
processNewPeers();
} catch (Throwable t) {
logger.error("Error", t);
}
}
}, 0, 1, TimeUnit.SECONDS);
if (config.listenPort() > 0) {
new Thread(new Runnable() {
public void run() {
peerServer.start(config.listenPort());
}
},
"PeerServerThread").start();
}
// Resending new blocks to network in loop
this.blockDistributeThread = new Thread(new Runnable() {
@Override
public void run() {
newBlocksDistributeLoop();
}
}, "NewSyncThreadBlocks");
this.blockDistributeThread.start();
// Resending pending txs to newly connected peers
this.txDistributeThread = new Thread(new Runnable() {
@Override
public void run() {
newTxDistributeLoop();
}
}, "NewPeersThread");
this.txDistributeThread.start();
}
public void connect(Node node) {
if (logger.isTraceEnabled()) logger.trace(
"Peer {}: initiate connection",
node.getHexIdShort()
);
if (nodesInUse().contains(node.getHexId())) {
if (logger.isTraceEnabled()) logger.trace(
"Peer {}: connection already initiated",
node.getHexIdShort()
);
return;
}
ethereum.connect(node);
}
public Set<String> nodesInUse() {
Set<String> ids = new HashSet<>();
for (Channel peer : getActivePeers()) {
ids.add(peer.getPeerId());
}
for (Channel peer : newPeers) {
ids.add(peer.getPeerId());
}
return ids;
}
private void processNewPeers() {
if (newPeers.isEmpty()) return;
List<Channel> processed = new ArrayList<>();
int addCnt = 0;
for(Channel peer : newPeers) {
logger.debug("Processing new peer: " + peer);
if(peer.isProtocolsInitialized()) {
logger.debug("Protocols initialized");
if (!activePeers.containsKey(peer.getNodeIdWrapper())) {
if (!peer.isActive() &&
activePeers.size() >= maxActivePeers &&
!trustedPeers.accept(peer.getNode())) {
// restricting inbound connections unless this is a trusted peer
disconnect(peer, TOO_MANY_PEERS);
} else {
process(peer);
addCnt++;
}
} else {
disconnect(peer, DUPLICATE_PEER);
}
processed.add(peer);
}
}
if (addCnt > 0) {
logger.info("New peers processed: " + processed + ", active peers added: " + addCnt + ", total active peers: " + activePeers.size());
}
newPeers.removeAll(processed);
}
private void disconnect(Channel peer, ReasonCode reason) {
logger.debug("Disconnecting peer with reason " + reason + ": " + peer);
peer.disconnect(reason);
recentlyDisconnected.put(peer.getInetSocketAddress().getAddress(), new Date());
}
public boolean isRecentlyDisconnected(InetAddress peerAddr) {
Date disconnectTime = recentlyDisconnected.get(peerAddr);
if (disconnectTime != null &&
System.currentTimeMillis() - disconnectTime.getTime() < inboundConnectionBanTimeout) {
return true;
} else {
recentlyDisconnected.remove(peerAddr);
return false;
}
}
private void process(Channel peer) {
if(peer.hasEthStatusSucceeded()) {
// prohibit transactions processing until main sync is done
if (syncManager.isSyncDone()) {
peer.onSyncDone(true);
// So we could perform some tasks on recently connected peer
newActivePeers.add(peer);
}
activePeers.put(peer.getNodeIdWrapper(), peer);
}
}
/**
* Propagates the transactions message across active peers with exclusion of
* 'receivedFrom' peer.
* @param tx transactions to be sent
* @param receivedFrom the peer which sent original message or null if
* the transactions were originated by this peer
*/
public void sendTransaction(List<Transaction> tx, Channel receivedFrom) {
for (Channel channel : activePeers.values()) {
if (channel != receivedFrom) {
channel.sendTransaction(tx);
}
}
}
/**
* Propagates the new block message across active peers
* Suitable only for self-mined blocks
* Use {@link #sendNewBlock(Block, Channel)} for sending blocks received from net
* @param block new Block to be sent
*/
public void sendNewBlock(Block block) {
for (Channel channel : activePeers.values()) {
channel.sendNewBlock(block);
}
}
/**
* Called on new blocks received from other peers
* @param blockWrapper Block with additional info
*/
public void onNewForeignBlock(BlockWrapper blockWrapper) {
newForeignBlocks.add(blockWrapper);
}
/**
* Processing new blocks received from other peers from queue
*/
private void newBlocksDistributeLoop() {
while (!Thread.currentThread().isInterrupted()) {
BlockWrapper wrapper = null;
try {
wrapper = newForeignBlocks.take();
Channel receivedFrom = getActivePeer(wrapper.getNodeId());
sendNewBlock(wrapper.getBlock(), receivedFrom);
} catch (InterruptedException e) {
break;
} catch (Throwable e) {
if (wrapper != null) {
logger.error("Error broadcasting new block {}: ", wrapper.getBlock().getShortDescr(), e);
logger.error("Block dump: {}", wrapper.getBlock());
} else {
logger.error("Error broadcasting unknown block", e);
}
}
}
}
/**
* Sends all pending txs to new active peers
*/
private void newTxDistributeLoop() {
while (!Thread.currentThread().isInterrupted()) {
Channel channel = null;
try {
channel = newActivePeers.take();
List<Transaction> pendingTransactions = pendingState.getPendingTransactions();
if (!pendingTransactions.isEmpty()) {
channel.sendTransaction(pendingTransactions);
}
} catch (InterruptedException e) {
break;
} catch (Throwable e) {
if (channel != null) {
logger.error("Error sending transactions to peer {}: ", channel.getNode().getHexIdShort(), e);
} else {
logger.error("Unknown error when sending transactions to new peer", e);
}
}
}
}
/**
* Propagates the new block message across active peers with exclusion of
* 'receivedFrom' peer.
* Distributes full block to 30% of peers and only its hash to remains
* @param block new Block to be sent
* @param receivedFrom the peer which sent original message
*/
private void sendNewBlock(Block block, Channel receivedFrom) {
for (Channel channel : activePeers.values()) {
if (channel == receivedFrom) continue;
if (rnd.nextInt(10) < 3) { // 30%
channel.sendNewBlock(block);
} else { // 70%
channel.sendNewBlockHashes(block);
}
}
}
public void add(Channel peer) {
logger.debug("New peer in ChannelManager {}", peer);
newPeers.add(peer);
}
public void notifyDisconnect(Channel channel) {
logger.debug("Peer {}: notifies about disconnect", channel);
channel.onDisconnect();
syncPool.onDisconnect(channel);
activePeers.values().remove(channel);
newPeers.remove(channel);
}
public void onSyncDone(boolean done) {
for (Channel channel : activePeers.values())
channel.onSyncDone(done);
}
public Collection<Channel> getActivePeers() {
return new ArrayList<>(activePeers.values());
}
public Channel getActivePeer(byte[] nodeId) {
return activePeers.get(new ByteArrayWrapper(nodeId));
}
public void close() {
try {
logger.info("Shutting down block and tx distribute threads...");
if (blockDistributeThread != null) blockDistributeThread.interrupt();
if (txDistributeThread != null) txDistributeThread.interrupt();
logger.info("Shutting down ChannelManager worker thread...");
mainWorker.shutdownNow();
mainWorker.awaitTermination(5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.warn("Problems shutting down", e);
}
peerServer.close();
ArrayList<Channel> allPeers = new ArrayList<>(activePeers.values());
allPeers.addAll(newPeers);
for (Channel channel : allPeers) {
try {
channel.dropConnection();
} catch (Exception e) {
logger.warn("Problems disconnecting channel " + channel, e);
}
}
}
}