All Downloads are FREE. Search and download functionalities are using the official Maven repository.

convex.peer.ConnectionManager Maven / Gradle / Ivy

There is a newer version: 0.7.15
Show newest version
package convex.peer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.UnresolvedAddressException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import convex.api.Convex;
import convex.core.Belief;
import convex.core.Constants;
import convex.core.Peer;
import convex.core.Result;
import convex.core.State;
import convex.core.data.ACell;
import convex.core.data.AString;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.Hash;
import convex.core.data.Keywords;
import convex.core.data.PeerStatus;
import convex.core.data.SignedData;
import convex.core.data.Vectors;
import convex.core.lang.RT;
import convex.core.store.Stores;
import convex.core.util.Utils;
import convex.net.Connection;
import convex.net.message.Message;
import convex.net.message.MessageRemote;

/**
 * Class for managing the outbound connections from a Peer Server.
 *
 * Outbound connections need special handling: - Should be trusted connections
 * to known peers - Should be targets for broadcast of belief updates - Should
 * be limited in number
 */
public class ConnectionManager {

	private static final Logger log = LoggerFactory.getLogger(ConnectionManager.class.getName());

	/**
	 * Pause for each iteration of Server connection loop.
	 */
	static final long SERVER_CONNECTION_PAUSE = 1000;

	/**
	 * Default pause for each iteration of Server connection loop.
	 */
	static final long SERVER_POLL_DELAY = 2000;

	/**
	 * How long to wait for a belief poll request of status.
	 */
	static final long POLL_TIMEOUT_MILLIS = 2000;

	/**
	 * How long to wait for a complete acquire of a belief.
	 */
	static final long POLL_ACQUIRE_TIMEOUT_MILLIS = 10000;

	protected final Server server;
	private final HashMap connections = new HashMap<>();

	/**
	 * Planned future connections for this Peer
	 */
	private final HashSet plannedConnections = new HashSet<>();

	/**
	 * The list of outgoing challenges that are being made to remote peers
	 */
	private HashMap challengeList = new HashMap<>();

	private Thread connectionThread = null;

	private SecureRandom random = new SecureRandom();

	private long pollDelay;

	/**
	 * Timstamp for the last execution of the Connection Manager update loop.
	 */
	private long lastUpdate = Utils.getCurrentTimestamp();

	/*
	 * Runnable loop for managing server connections
	 */
	private Runnable connectionLoop = new Runnable() {
		@Override
		public void run() {
			Stores.setCurrent(server.getStore()); // ensure the loop uses this Server's store
			try {
				lastUpdate = Utils.getCurrentTimestamp();
				while (server.isLive()) {
					Thread.sleep(ConnectionManager.SERVER_CONNECTION_PAUSE);
					makePlannedConnections();
					maintainConnections();
					pollBelief();
					lastUpdate = Utils.getCurrentTimestamp();
				}
			} catch (InterruptedException e) {
				/* OK? Close the thread normally */
			} catch (Throwable e) {
				log.error("Unexpected exception, Terminating Server connection loop");
				e.printStackTrace();
			} finally {
				connectionThread = null;
				closeAllConnections(); // shut down everything gracefully if we can
			}
		}
	};

	/**
	 * Celled by the connection manager to ensure we are tracking latest Beliefs on the network
	 */
	private void pollBelief() {
		try {
			// Poll if no recent consensus updates
			long lastConsensus = server.getPeer().getConsensusState().getTimeStamp().longValue();
			if (lastConsensus + pollDelay >= lastUpdate) return;

			ArrayList conns = new ArrayList<>(connections.values());
			if (conns.size() == 0) {
				// Nothing to do
				// log.debug("No connections available to poll!");
				return;
			}
			
			// TODO: probably shouldn't make a new connection?
			// Maybe use Convex instance instead of Connection?
			Connection c = conns.get(random.nextInt(conns.size()));

			if (c.isClosed()) return;
			Convex convex = Convex.connect(c.getRemoteAddress());
			try {
				// use requestStatusSync to auto acquire hash of the status instead of the value
				Result result=convex.requestStatusSync(POLL_TIMEOUT_MILLIS);
				AVector status = result.getValue();

				Hash h=RT.ensureHash(status.get(0));
				@SuppressWarnings("unchecked")

				SignedData sb=(SignedData) convex.acquire(h).get(POLL_ACQUIRE_TIMEOUT_MILLIS,TimeUnit.MILLISECONDS);

				server.queueEvent(sb);
			} finally {
				convex.close();
			}
		} catch (Throwable t) {
			if (server.isLive()) log.warn("Polling failed: {}",t);
		}
	}

	private void makePlannedConnections() {
		synchronized(plannedConnections) {
			for (InetSocketAddress a: plannedConnections) {
				Connection c=connectToPeer(a);
				if (c==null) {
					log.warn( "Planned Connection failed to {}",a);
				} else {
					log.info("Planned Connection made to {}",a);
				}
			}
			plannedConnections.clear();
		}
	}


	protected void maintainConnections() {
		State s=server.getPeer().getConsensusState();

		long millisSinceLastUpdate=Math.max(0,Utils.getCurrentTimestamp()-lastUpdate);

		int targetPeerCount=getTargetPeerCount();
		int currentPeerCount=connections.size();
		double totalStake=s.computeStakes().get(null);

		AccountKey[] peers = connections.keySet().toArray(new AccountKey[currentPeerCount]);
		for (AccountKey p: peers) {
			Connection conn=connections.get(p);

			// Remove closed connections. No point keeping these
			if ((conn==null)||(conn.isClosed())) {
				closeConnection(p);
				currentPeerCount--;
				continue;
			}

			/*
			 *  Always remove Peers not staked in consensus. This should eliminate Peers that have
			 *  withdrawn or are slashed from current consideration.
			 */
			PeerStatus ps=s.getPeer(p);
			if ((ps==null)||(ps.getTotalStake()<=Constants.MINIMUM_EFFECTIVE_STAKE)) {
				closeConnection(p);
				currentPeerCount--;
				continue;
			}

			/* Drop Peers randomly if they have a small stake
			 * This ensure that new peers will get picked up occasionally and
			 * the distribution of peers tends towards the level of stake over time
			 */
			if ((millisSinceLastUpdate>0)&&(currentPeerCount>=targetPeerCount)) {
				double prop=ps.getTotalStake()/totalStake; // proportion of stake represented by this Peer
				// Very low chance of dropping a Peer with high stake (more than
				double keepChance=Math.min(1.0, prop*targetPeerCount);

				if (keepChance<1.0) {

					double dropRate=millisSinceLastUpdate/(double)Constants.PEER_CONNECTION_DROP_TIME;
					if (random.nextDouble()<(dropRate*(1.0-keepChance))) {
						closeConnection(p);
						currentPeerCount--;
						continue;
					}
				}
			}

			// send request for a trusted peer connection if necessary
			// TODO: need to find out why the response message is not being received by the peers
			requestChallenge(p, conn, server.getPeer());
		}

		// refresh peers list
		currentPeerCount=connections.size();
		peers = connections.keySet().toArray(new AccountKey[currentPeerCount]);
		if (peers.length potentialPeers=s.getPeers().keySet();
			InetSocketAddress target=null;
			double accStake=0.0;
			for (ACell c:potentialPeers) {
				AccountKey peerKey=RT.ensureAccountKey(c);
				if (connections.containsKey(peerKey)) continue; // skip if already connected

				if (server.getPeerKey().equals(peerKey)) continue; // don't connect to self!!

				PeerStatus ps=s.getPeers().get(peerKey);
				if (ps==null) continue; // skip
				AString hostName=ps.getHostname();
				if (hostName==null) continue;
				InetSocketAddress maybeAddress=Utils.toInetSocketAddress(hostName.toString());
				if (maybeAddress==null) continue;
				long peerStake=ps.getTotalStake();
				if (peerStake>0) {
					double t=random.nextDouble()*(accStake+peerStake);
					if (t>=accStake) {
						target=maybeAddress;
					}
					accStake+=peerStake;
				}
			}

			if (target!=null) {
				// Try to connect to Peer. If it fails, no worry, will retry another peer next time
				connectToPeer(target);
			}
		}
	}

	/**
	 * Gets the desired number of outgoing connections
	 * @return
	 */
	private int getTargetPeerCount() {
		Integer target;
		try {
			target = Utils.toInt(server.getConfig().get(Keywords.OUTGOING_CONNECTIONS));
		} catch (Exception ex) {
			target=null;
		}
		if (target==null) target=Constants.DEFAULT_OUTGOING_CONNECTION_COUNT;
		return target;
	}


	public ConnectionManager(Server server) {
		this.server = server;

		Object _pollDelay = server.getConfig().get(Keywords.POLL_DELAY);
		this.pollDelay = (_pollDelay == null) ? ConnectionManager.SERVER_POLL_DELAY : Utils.toInt(_pollDelay);
	}

	public synchronized void setConnection(AccountKey peerKey, Connection peerConnection) {
		if (connections.containsKey(peerKey)) {
			connections.get(peerKey).close();
			connections.replace(peerKey, peerConnection);
		}
		else {
			connections.put(peerKey, peerConnection);
		}
	}

	/**
	 * Close and remove a connection
	 *
	 * @param peerKey Peer key linked to the connection to close and remove.
	 *
	 */
	public synchronized void closeConnection(AccountKey peerKey) {
		if (connections.containsKey(peerKey)) {
			Connection conn=connections.get(peerKey);
			if (conn!=null) {
				conn.close();
			}
			connections.remove(peerKey);
			server.raiseServerChange("connection");
		}
	}

	/**
	 * Close all outgoing connections from this Peer
	 */
	public synchronized void closeAllConnections() {
		for (Connection conn:connections.values()) {
			if (conn!=null) conn.close();
		}
		connections.clear();
	}

	/**
	 * Gets the current set of outbound peer connections from this server
	 *
	 * @return Set of connections
	 */
	public HashMap getConnections() {
		return connections;
	}

	/**
	 * Return true if a specified Peer is connected
	 * @param peerKey Public Key of Peer
	 * @return True if connected
	 *
	 */
	public boolean isConnected(AccountKey peerKey) {
		return connections.containsKey(peerKey);
	}


	/**
	 * Gets a connection based on the peers public key
	 * @param peerKey Public key of Peer
	 *
	 * @return Connection instance, or null if not found
	 */
	public Connection getConnection(AccountKey peerKey) {
		if (!connections.containsKey(peerKey)) return null;
		return connections.get(peerKey);
	}

	/**
	 * Returns the number of active connections
	 * @return Number of connections
	 */
	public int getConnectionCount() {
		return connections.size();
	}

	/**
	 * Returns the number of trusted connections
	 * @return Number of trusted connections
	 *
	 */
	public int getTrustedConnectionCount() {
		int result = 0;
		for (Connection connection : connections.values()) {
			if (connection.isTrusted()) {
				result ++;
			}
		}
		return result;
	}

	public void processChallenge(Message m, Peer thisPeer) {
		try {
			SignedData> signedData = m.getPayload();
			if ( signedData == null) {
				log.debug( "challenge bad message data sent");
				return;
			}
			AVector challengeValues = signedData.getValue();

			if (challengeValues == null || challengeValues.size() != 3) {
				log.debug("challenge data incorrect number of items should be 3 not ",RT.count(challengeValues));
				return;
			}
			Connection pc = ((MessageRemote)m).getConnection();
			if ( pc == null) {
				log.warn( "No remote peer connection from challenge");
				return;
			}
			// log.log(LEVEL_CHALLENGE_RESPONSE, "Processing challenge request from: " + pc.getRemoteAddress());

			// get the token to respond with
			Hash token = RT.ensureHash(challengeValues.get(0));
			if (token == null) {
				log.warn( "no challenge token provided");
				return;
			}

			// check to see if we are both want to connect to the same network
			Hash networkId = RT.ensureHash(challengeValues.get(1));
			if (networkId == null) {
				log.warn( "challenge data has no networkId");
				return;
			}
			if ( !networkId.equals(thisPeer.getNetworkID())) {
				log.warn( "challenge data has incorrect networkId");
				return;
			}
			// check to see if the challenge is for this peer
			AccountKey toPeer = RT.ensureAccountKey(challengeValues.get(2));
			if (toPeer == null) {
				log.warn( "challenge data has no toPeer address");
				return;
			}
			if ( !toPeer.equals(thisPeer.getPeerKey())) {
				log.warn( "challenge data has incorrect addressed peer");
				return;
			}

			// get who sent this challenge
			AccountKey fromPeer = signedData.getAccountKey();

			// send the signed response back
			AVector responseValues = Vectors.of(token, thisPeer.getNetworkID(), fromPeer, signedData.getHash());

			SignedData response = thisPeer.sign(responseValues);
			// log.log(LEVEL_CHALLENGE_RESPONSE, "Sending response to "+ pc.getRemoteAddress());
			if (pc.sendResponse(response) == -1 ){
				log.warn("Failed sending response from challenge to ", pc.getRemoteAddress());
			}

		} catch (Throwable t) {
			log.error("Challenge Error: {}" ,t);
			// t.printStackTrace();
		}
	}

	AccountKey processResponse(Message m, Peer thisPeer) {
		try {
			SignedData signedData = m.getPayload();

			log.debug( "Processing response request from: {}",m.getOriginString());

			@SuppressWarnings("unchecked")
			AVector responseValues = (AVector) signedData.getValue();

			if (responseValues.size() != 4) {
				log.warn( "response data incorrect number of items should be 4 not {}",responseValues.size());
				return null;
			}


			// get the signed token
			Hash token = RT.ensureHash(responseValues.get(0));
			if (token == null) {
				log.warn( "no response token provided");
				return null;
			}

			// check to see if we are both want to connect to the same network
			Hash networkId = RT.ensureHash(responseValues.get(1));
			if ( networkId == null || !networkId.equals(thisPeer.getNetworkID())) {
				log.warn( "response data has incorrect networkId");
				return null;
			}
			// check to see if the challenge is for this peer
			AccountKey toPeer = RT.ensureAccountKey(responseValues.get(2));
			if ( toPeer == null || !toPeer.equals(thisPeer.getPeerKey())) {
				log.warn( "response data has incorrect addressed peer");
				return null;
			}

			// hash sent by the response
			Hash challengeHash = RT.ensureHash(responseValues.get(3));

			// get who sent this challenge
			AccountKey fromPeer = signedData.getAccountKey();


			if ( !challengeList.containsKey(fromPeer)) {
				log.warn( "response from an unkown challenge");
				return null;
			}
			synchronized(challengeList) {

				// get the challenge data we sent out for this peer
				ChallengeRequest challengeRequest = challengeList.get(fromPeer);

				Hash challengeToken = challengeRequest.getToken();
				if (!challengeToken.equals(token)) {
					log.warn( "invalid response token sent");
					return null;
				}

				AccountKey challengeFromPeer = challengeRequest.getPeerKey();
				if (!signedData.getAccountKey().equals(challengeFromPeer)) {
					log.warn("response key does not match requested key, sent from a different peer");
					return null;
				}

				// hash sent by this peer for the challenge
				Hash challengeSourceHash = challengeRequest.getSendHash();
				if ( !challengeHash.equals(challengeSourceHash)) {
					log.warn("response hash of the challenge does not match");
					return null;
				}
				// remove from list incase this fails, we can generate another challenge
				challengeList.remove(fromPeer);

				Connection connection = getConnection(fromPeer);
				if (connection != null) {
					connection.setTrustedPeerKey(fromPeer);
					server.raiseServerChange("trusted connection");
				}

				// return the trusted peer key
				return fromPeer;
			}

		} catch (Throwable t) {
			log.error("Response Error: {}",t);
		}
		return null;
	}



	/**
	 * Sends out a challenge to a connection that is not trusted.
	 * @param toPeerKey Peer key that we need to send the challenge too.
	 * @param connection untrusted connection
	 * @param thisPeer Source peer that the challenge is issued from
	 *
	 */
	public void requestChallenge(AccountKey toPeerKey, Connection connection, Peer thisPeer) {
		synchronized(challengeList) {
			if (connection.isTrusted()) {
				return;
			}
			// skip if a challenge is already being sent
			if (challengeList.containsKey(toPeerKey)) {
				if (!challengeList.get(toPeerKey).isTimedout()) {
					// not timed out, then continue to wait
					return;
				}
				// remove the old timed out request
				challengeList.remove(toPeerKey);
			}
			ChallengeRequest request = ChallengeRequest.create(toPeerKey);
			if (request.send(connection, thisPeer)>=0) {
				challengeList.put(toPeerKey, request);
			} else {
				// TODO: check OK to do nothing and send later?
			}
		}
	}

	/**
	 *
	 * @param msg Message to broadcast
	 *
	 * @param requireTrusted If true, only broadcast to trusted peers
	 *
	 */
	public synchronized void broadcast(Message msg, boolean requireTrusted) {
		synchronized(connections) {
			for (Connection pc : connections.values()) {
				try {
					if ( (requireTrusted && pc.isTrusted()) || !requireTrusted) {
						pc.sendMessage(msg);
					}
				} catch (IOException e) {
					log.error("Error in broadcast: ", e);
				}
			}
		}
	}

	/**
	 * Connects explicitly to a Peer at the given host address
	 * @param hostAddress Address to connect to
	 * @return new Connection, or null if attempt fails
	 */
	public Connection connectToPeer(InetSocketAddress hostAddress) {
		Connection newConn = null;
		try {
			// Temp client connection
			Convex convex=Convex.connect(hostAddress);
			Result result = convex.requestStatusSync(Constants.DEFAULT_CLIENT_TIMEOUT);
			AVector status = result.getValue();
			if (status == null || status.count()!=Constants.STATUS_COUNT) {
				throw new Error("Bad status message from remote Peer");
			}

			AccountKey peerKey =RT.ensureAccountKey(status.get(3));
			if (peerKey==null) return null;

			Connection existing=connections.get(peerKey);
			if ((existing!=null)&&!existing.isClosed()) return existing;
			// close the current connecton to Convex API
			convex.close();
			synchronized(connections) {
				// reopen with connection to the peer and handle server messages
				newConn = Connection.connect(hostAddress, server.peerReceiveAction, server.getStore(), null,Constants.SOCKET_PEER_BUFFER_SIZE,Constants.SOCKET_PEER_BUFFER_SIZE);
				connections.put(peerKey, newConn);
			}
			server.raiseServerChange("connection");
		} catch (IOException | TimeoutException e) {
			// ignore any errors from the peer connections
		} catch (UnresolvedAddressException e) {
			log.info("Unable to resolve host address: "+hostAddress);
		}
		return newConn;
	}

	/**
	 * Schedules a request to connect to a Peer at the given host address
	 * @param hostAddress Address to connect to
	 */
	public void connectToPeerAsync(InetSocketAddress hostAddress) {
		synchronized (plannedConnections) {
			plannedConnections.add(hostAddress);
		}
	}

	public void start() {
		// Set timestamp for connection updates
		lastUpdate=Utils.getCurrentTimestamp();

		// start connection thread
		connectionThread = new Thread(connectionLoop, "Connection Manager thread at "+server.getPort());
		connectionThread.setDaemon(true);
		connectionThread.start();

	}

	public void close() {
		if (connectionThread!=null) {
			connectionThread.interrupt();
		}
	}




}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy