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

convex.peer.Server Maven / Gradle / Ivy

package convex.peer;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

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

import convex.api.Convex;
import convex.core.Belief;
import convex.core.Constants;
import convex.core.ErrorCodes;
import convex.core.Order;
import convex.core.Peer;
import convex.core.Result;
import convex.core.State;
import convex.core.crypto.AKeyPair;
import convex.core.data.ACell;
import convex.core.data.AMap;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.AccountStatus;
import convex.core.data.Address;
import convex.core.data.Hash;
import convex.core.data.Keyword;
import convex.core.data.Keywords;
import convex.core.data.Maps;
import convex.core.data.Ref;
import convex.core.data.SignedData;
import convex.core.data.Strings;
import convex.core.data.Vectors;
import convex.core.data.prim.CVMLong;
import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.InvalidDataException;
import convex.core.exceptions.MissingDataException;
import convex.core.init.Init;
import convex.core.lang.RT;
import convex.core.store.AStore;
import convex.core.store.Stores;
import convex.core.util.Counters;
import convex.core.util.Shutdown;
import convex.core.util.Utils;
import convex.net.Message;
import convex.net.MessageType;
import convex.net.NIOServer;


/**
 * A self contained Peer Server that can be launched with a config.
 * 
 * The primary role for the Server is to respond to incoming messages and maintain
 * network consensus.
 *
 * Components contained within the Server handle specific tasks, e.g:
 * - Client transaction handling
 * - CPoS Belief merges
 * - Belief Propagation
 * - CVM Execution
 *
 * "Programming is a science dressed up as art, because most of us don't
 * understand the physics of software and it's rarely, if ever, taught. The
 * physics of software is not algorithms, data structures, languages, and
 * abstractions. These are just tools we make, use, and throw away. The real
 * physics of software is the physics of people. Specifically, it's about our
 * limitations when it comes to complexity and our desire to work together to
 * solve large problems in pieces. This is the science of programming: make
 * building blocks that people can understand and use easily, and people will
 * work together to solve the very largest problems." ― Pieter Hintjens
 *
 */
public class Server implements Closeable {
	public static final int DEFAULT_PORT = Constants.DEFAULT_PEER_PORT;
	
	static final Logger log = LoggerFactory.getLogger(Server.class.getName());

	private Consumer messageReceiveObserver=null;

	/**
	 * Message Consumer that simply enqueues received client messages received by this peer
	 * Called on NIO thread: should never block
	 */
	Consumer receiveAction = m->{
		observeMessageReceived(m);
		processMessage(m);
	};

	/**
	 * Connection manager instance.
	 */
	protected final ConnectionManager manager = new ConnectionManager(this);
	
	/**
	 * Connection manager instance.
	 */
	protected final BeliefPropagator propagator=new BeliefPropagator(this);
	
	/**
	 * Transaction handler instance.
	 */
	protected final TransactionHandler transactionHandler=new TransactionHandler(this);
	
	/**
	 * Transaction handler instance.
	 */
	protected final CVMExecutor executor=new CVMExecutor(this);

	/**
	 * Query handler instance.
	 */
	protected final QueryHandler queryHandler=new QueryHandler(this);

	/**
	 * Store to use for all threads associated with this server instance
	 */
	private final AStore store;

	/**
	 * Configuration
	 */

	private final HashMap config;

	private final ACell rootKey;


	/**
	 * NIO Server instance
	 */
	private NIOServer nio = NIOServer.create(this);

	private Server(HashMap config) throws ConfigException, InterruptedException {
		this.config = config;
		final AStore savedStore=Stores.current();

		AStore configStore = Config.ensureStore(config);
		this.store=configStore;
		// this.store = (configStore == null) ? savedStore : configStore;
		
		// Switch to use the configured store for setup
		try {
			Stores.setCurrent(store);

			// Establish Peer state
			Peer peer = establishPeer();

			// Set up root key for Peer persistence. Default is Peer Account Key
			ACell rk=RT.cvm(config.get(Keywords.ROOT_KEY));
			if (rk==null) rk=peer.getPeerKey();
			rootKey=rk;

			// Ensure Peer is stored in executor and initially persisted prior to launch
			executor.setPeer(peer);
			executor.persistPeerData();
			
			establishController();
		} catch (TimeoutException e) {
			throw new ConfigException("Timeout trying to configure peer",e);
		} catch (IOException e) {
			throw new ConfigException("IO Error while trying to configure peer",e);
		} finally {
			Stores.setCurrent(savedStore);
		}
	}

	/**
	 * Establish the controller Account for this Peer.
	 */
	private void establishController() {
		Peer peer=getPeer();
		Address controlAddress=RT.toAddress(getConfig().get(Keywords.CONTROLLER));
		if (controlAddress==null) {
			controlAddress=peer.getController();
			if (controlAddress==null) {
				throw new IllegalStateException("Peer Controller account does not exist for Peer Key: "+peer.getPeerKey());
			}
		}
		AccountStatus as=peer.getConsensusState().getAccount(controlAddress);
		if (as==null) {
			log.warn("Peer Controller Account does not currently exist (perhaps pending sync?): "+controlAddress);	
		} else if (!Utils.equals(as.getAccountKey(),getKeyPair().getAccountKey())) {
			// TODO: not a problem?
			log.warn("Server keypair does not match keypair for control account: "+controlAddress);
		}
	}

	private Peer establishPeer() throws ConfigException, TimeoutException, IOException, InterruptedException {
		log.debug("Establishing Peer with store: {}",Stores.current());
		AKeyPair keyPair = Config.ensurePeerKey(config);
		if (keyPair==null) {
			log.warn("No keypair provided for Server, deafulting to generated keypair for testing purposes");
			keyPair=AKeyPair.generate();
			config.put(Keywords.KEYPAIR,keyPair);
			log.warn("Generated keypair with public key: "+keyPair.getAccountKey());
		}

		// TODO: should probably move acquisition to launch phase?
		Object source=getConfig().get(Keywords.SOURCE);
		if (Utils.bool(source)) {
			InetSocketAddress sourceAddr=Utils.toInetSocketAddress(source);
			if (sourceAddr==null) throw new ConfigException("Bad SOURCE for peer sync, should be an internet socket address: "+source);
			return syncPeer(keyPair,sourceAddr);

		} else if (Utils.bool(getConfig().get(Keywords.RESTORE))) {
			ACell rk=RT.cvm(config.get(Keywords.ROOT_KEY));
			if (rk==null) rk=keyPair.getAccountKey();

			Peer peer = Peer.restorePeer(store, keyPair, rk);
			if (peer != null) {
				log.info("Restored Peer with root data hash: {}",store.getRootHash());
				return peer;
			}
		}
		State genesisState = (State) config.get(Keywords.STATE);
		if (genesisState!=null) {
			log.debug("Defaulting to standard Peer startup with genesis state: "+genesisState.getHash());
		} else {
			AccountKey peerKey=keyPair.getAccountKey();
			genesisState=Init.createState(List.of(peerKey));
			log.debug("Created new genesis state: "+genesisState.getHash()+ " with initial peer: "+peerKey);
		}
		return Peer.createGenesisPeer(keyPair,genesisState);
	}

	private Peer syncPeer(AKeyPair keyPair, InetSocketAddress sourceAddr) throws IOException, TimeoutException, InterruptedException {
		// Peer sync case
		try {
			Convex convex = Convex.connect(sourceAddr);
			log.info("Attempting Peer Sync with: "+sourceAddr);
			long timeout = establishTimeout();
			
			// Sync status and genesis state
			Result result = convex.requestStatusSync(timeout);
			AVector status = result.getValue();
			if (status == null || status.count()!=Config.STATUS_COUNT) {
				throw new Error("Bad status message from remote Peer");
			}
			Hash beliefHash=RT.ensureHash(status.get(0));
			AccountKey remoteKey=RT.ensureAccountKey(status.get(3));
			Hash genesisHash=RT.ensureHash(status.get(2));
			Hash stateHash=RT.ensureHash(status.get(4));
			log.debug("Attempting to sync remote state: "+stateHash + " on network: "+genesisHash);
			State genF=(State) convex.acquire(genesisHash).get(timeout,TimeUnit.MILLISECONDS);
			log.debug("Retrieved Genesis State: "+genesisHash);
			
			// Belief acquisition
			log.debug("Attempting to obtain peer Belief: "+beliefHash);
			Belief belF=null;
			long timeElapsed=0;
			while (belF==null) {
				try {
					belF=(Belief) convex.acquire(beliefHash).get(timeout,TimeUnit.MILLISECONDS);
				} catch (TimeoutException te) {
					timeElapsed+=timeout;
					log.info("Still waiting for Belief sync after "+timeElapsed/1000+"s");
				}
			}
			log.info("Retrieved Peer Belief: "+beliefHash+ " with memory size: "+belF.getMemorySize());
	
			convex.close();
			SignedData peerOrder=belF.getOrders().get(remoteKey);
			if (peerOrder!=null) {
				SignedData newOrder=keyPair.signData(peerOrder.getValue());
				belF=belF.withOrders(belF.getOrders().assoc(keyPair.getAccountKey(),newOrder));
			} else {
				log.warn("Remote peer Belief missing it's own Order?");
			}
			Peer peer=Peer.create(keyPair, genF, belF);
			return peer;
		} catch (ExecutionException | InvalidDataException e) {
			throw Utils.sneakyThrow(e);
		}
	}

	private long establishTimeout() {
		Object maybeTimeout=getConfig().get(Keywords.TIMEOUT);
		if (maybeTimeout==null) return Config.PEER_SYNC_TIMEOUT;
		Utils.toInt(maybeTimeout);
		return 0;
	}

	/**
	 * Creates a new (unlaunched) Server with a given config.
	 *
	 * @param config Server configuration map. Will be defensively copied.
	 *
	 * @return New Server instance
	 * @throws ConfigException If Peer configuration failed, possible multiple causes
	 * @throws InterruptedException 
	 */
	public static Server create(HashMap config) throws ConfigException, InterruptedException {
		return new Server(new HashMap<>(config));
	}

	private void observeMessageReceived(Message m) {
		Consumer obs=messageReceiveObserver;
		if (obs!=null) {
			obs.accept(m);
		}
	}
	
	public void setMessageReceiveObserver(Consumer observer) {
		this.messageReceiveObserver=observer;
	}

	/**
	 * Gets the current Belief held by this Peer
	 *
	 * @return Current Belief
	 */
	public Belief getBelief() {
		return getPeer().getBelief();
	}

	/**
	 * Gets the current Peer data structure for this {@link Server}.
	 *
	 * @return Current Peer data
	 */
	public Peer getPeer() {
		return executor.getPeer();
	}

	/**
	 * Gets the desired host name for this Peer
	 * @return Hostname String
	 */
	public String getHostname() {
		return (String) config.get(Keywords.URL);
	}

	/**
	 * Launch the Peer Server, including all main server threads
	 * @throws InterruptedException 
	 */
	public void launch() throws IOException, InterruptedException {
		AStore savedStore=Stores.current();
		try {
			Stores.setCurrent(store);

			HashMap config = getConfig();

			Object p = config.get(Keywords.PORT);
			Integer port = (p == null) ? null : Utils.toInt(p);

			nio.launch((String)config.get(Keywords.BIND_ADDRESS), port);
			port = nio.getPort(); // Get the actual port (may be auto-allocated)

			// set running status now, so that loops don't immediately terminate
			isRunning = true;
			
			// Close server on shutdown, should be before Etch stores in priority
			Shutdown.addHook(Shutdown.SERVER, ()->close());
			
			// Start threaded components
			manager.start();
			queryHandler.start();
			propagator.start();
			transactionHandler.start();
			executor.start();

			// Connect to source peer if specified
			if (getConfig().containsKey(Keywords.SOURCE)) {
				Object s=getConfig().get(Keywords.SOURCE);
				InetSocketAddress sa=Utils.toInetSocketAddress(s);
				if (sa!=null) {
					if (manager.connectToPeer(sa)!=null) {
						log.debug("Automatically connected to :source peer at: {}",sa);
					} else {
						log.warn("Failed to connect to :source peer at: {}",sa);
					}
				} else {
					log.warn("Failed to parse :source peer address {}",s);
				}
			}

			goLive();
			log.info( "Peer server started on port "+nio.getPort()+" with peer key: {}",getPeerKey());
		} finally {
			Stores.setCurrent(savedStore);
		}
	}

	private void goLive() {
		isLive=true;
	}

	/**
	 * Process a message received from a peer or client. We know at this point that the
	 * message decoded successfully, not much else.....
	 * 
	 * SECURITY: Should anticipate malicious messages
	 *
	 * Runs on receiver thread, so we want to offload to a queue ASAP
	 *
	 * @param m
	 */
	protected void processMessage(Message m) {
		MessageType type = m.getType();
		AStore tempStore=Stores.current();
		try {
			Stores.setCurrent(this.store);
			switch (type) {
			case BELIEF:
				processBelief(m);
				break;
			case CHALLENGE:
				processChallenge(m);
				break;
			case RESPONSE:
				processResponse(m);
				break;
			case COMMAND:
				break;
			case DATA:
				processData(m);
				break;
			case REQUEST_DATA:
				processQuery(m); // goes on Query handler
				break;
			case QUERY:
				processQuery(m);
				break;
			case RESULT:
				break;
			case TRANSACT:
				processTransact(m);
				break;
			case GOODBYE:
				processClose(m);
				break;
			case STATUS:
				processStatus(m);
				break;
			default:
				Result r=Result.create(m.getID(), Strings.create("Bad Message Type: "+type), ErrorCodes.ARGUMENT);
				m.returnResult(r);
				break;
			}
		} catch (MissingDataException e) {
			Hash missingHash = e.getMissingHash();
			log.trace("Missing data: {} in message of type {}" , missingHash,type);
		} finally {
			Stores.setCurrent(tempStore);
		}
	}

	/**
	 * Respond to a request for missing data, on a best-efforts basis. Requests for
	 * missing data we do not hold are ignored.
	 *
	 * @param m
	 * @throws BadFormatException
	 */
	protected void handleDataRequest(Message m)  {
		// payload for a missing data request should be a valid Hash
		try {
			Message response=m.makeDataResponse(store);
			boolean sent = m.returnMessage(response);
			if (!sent) {
				log.trace("Can't send data request response due to full buffer");
			}
		} catch (BadFormatException e) {
			log.warn("Unable to deliver missing data due badly formatted DATA_REQUEST: {}", m);
		} catch (RuntimeException e) {
			log.warn("Unable to deliver missing data due to exception:", e);
		}
	}

	protected void processTransact(Message m) {
		boolean queued=transactionHandler.offerTransaction(m);
		
		if (queued) {
			// log.info("transaction queued");
		} else {
			// Failed to queue transaction
			Result r=Result.create(m.getID(), Strings.SERVER_LOADED, ErrorCodes.LOAD);
			m.returnResult(r);
		} 
	}

	/**
	 * Called by a remote peer to close connections to the remote peer.
	 *
	 */
	protected void processClose(Message m) {
		m.closeConnection();
	}

	/**
	 * Gets the number of belief broadcasts made by this Peer
	 * @return Count of broadcasts from this Server instance
	 */
	public long getBroadcastCount() {
		return propagator.getBeliefBroadcastCount();
	}
	
	/**
	 * Gets the number of beliefs received by this Peer
	 * @return Count of the beliefs received by this Server instance
	 */
	public long getBeliefReceivedCount() {
		return propagator.beliefReceivedCount;
	}



	/**
	 * Gets the Peer controller Address
	 * @return Peer controller Address
	 */
	public Address getPeerController() {
		return getPeer().getController();
	}

	/**
	 * Adds an event to the inbound server event queue. May block.
	 * @param event Signed event to add to inbound event queue
	 * @return True if Belief was successfullly queued, false otherwise
	 */
	public boolean queueBelief(Message event) {
		boolean offered=propagator.queueBelief(event);
		return offered;
	}
	
	protected void processStatus(Message m) {
		// We can ignore payload
		AVector reply = getStatusVector();
		Result r=Result.create(m.getID(), reply);
		m.returnResult(r);
	}

	/**
	 * Gets the status vector for the Peer
	 * 0 = latest belief hash
	 * 1 = states vector hash
	 * 2 = genesis state hash
	 * 3 = peer key
	 * 4 = consensus state
	 * 5 = consensus point
	 * 6 = proposal point
	 * 7 = ordering length
	 * 8 = consensus point vector
	 * @return Status vector
	 */
	public AVector getStatusVector() {
		Peer peer=getPeer();
		Belief belief=peer.getBelief();
		
		State state=peer.getConsensusState();
		
		Hash beliefHash=belief.getHash();
		Hash stateHash=state.getHash();
		Hash genesisHash=peer.getNetworkID();
		AccountKey peerKey=peer.getPeerKey();
		Hash consensusHash=state.getHash();
		
		Order order=belief.getOrder(peerKey);
		CVMLong cp = CVMLong.create(order.getConsensusPoint()) ;
		CVMLong pp = CVMLong.create(order.getProposalPoint()) ;
		CVMLong op = CVMLong.create(order.getBlockCount()) ;
		AVector cps = Vectors.of(Utils.toObjectArray(order.getConsensusPoints())) ;

		AVector reply=Vectors.of(beliefHash,stateHash,genesisHash,peerKey,consensusHash, cp,pp,op,cps);
		assert(reply.count()==Config.STATUS_COUNT);
		return reply;
	}

	private void processChallenge(Message m) {
		manager.processChallenge(m, getPeer());
	}

	protected void processResponse(Message m) {
		manager.processResponse(m, getPeer());
	}

	protected void processQuery(Message m) {
		boolean queued= queryHandler.offerQuery(m);
		if (!queued) {
			Result r=Result.create(m.getID(), Strings.SERVER_LOADED, ErrorCodes.LOAD);
			m.returnResult(r);
		} 
	}
	
	private void processData(Message m) {
		ACell payload;
		try {
			payload = m.getPayload();
			Counters.peerDataReceived++;
			
			// Note: partial messages are handled in Connection now
			Ref r = Ref.get(payload);
			if (r.isEmbedded()) {
				log.warn("DATA with embedded value: "+payload);
				return;
			}
			r = r.persistShallow();
		} catch (BadFormatException | IOException e) {
			log.debug("Error processing data: "+e.getMessage());
			m.closeConnection();
			return;
		}

	}

	/**
	 * Process an incoming message that represents a Belief
	 *
	 * @param m
	 */
	protected void processBelief(Message m) {
		if (!propagator.queueBelief(m)) {
			log.warn("Incoming belief queue full");
		}
	}

	/**
	 * Gets the port that this Server is currently accepting connections on
	 * @return Port number
	 */
	public int getPort() {
		return nio.getPort();
	}

	@Override
	public void finalize() {
		close();
	}

	/**
	 * Writes the Peer data to the configured store.
	 * 
	 * Note: Does not flush buffers to disk. 
	 *
	 * This will overwrite any previously persisted peer data.
	 * @return Updated Peer value with persisted data
	 * @throws IOException In case of any IO Error
	 */
	@SuppressWarnings("unchecked")
	public Peer persistPeerData() throws IOException {
		AStore tempStore = Stores.current();
		try {
			Stores.setCurrent(store);
			AMap peerData = getPeer().toData();

			Ref> rootRef = store.refForHash(store.getRootHash());
			AMap currentRootData = (rootRef == null)? Maps.empty() : rootRef.getValue();
			AMap newRootData = currentRootData.assoc(rootKey, peerData);

			newRootData=store.setRootData(newRootData).getValue();
			peerData=(AMap) newRootData.get(rootKey);
			log.debug( "Stored peer data with hash: {}", peerData.getHash().toHexString());
			return Peer.fromData(getKeyPair(), peerData);
		}  finally {
			Stores.setCurrent(tempStore);
		}
	}

	/**
	 * Future to complete with timestamp at time of shutdown
	 */
	private CompletableFuture shutdownFuture=new CompletableFuture(); 
	
	@Override
	public void close() {
		
		if (!isRunning) return; // already shut down
		log.debug("Peer shutdown starting for "+getPeerKey());
		isLive=false;
		isRunning = false;

		// Close manager, we don't want any management actions during shutdown!
		manager.close();

		// Shut down propagator, no point sending any more Beliefs
		propagator.close();
		
		
		queryHandler.close();
		transactionHandler.close();
		executor.close();
		
		Peer peer=getPeer();
		// persist peer state if necessary
		if ((peer != null) && !Boolean.FALSE.equals(getConfig().get(Keywords.PERSIST))) {
			try {
				persistPeerData();
			} catch (IOException e) {
				log.warn("Unable to persist Peer data: ",e);
			}
		}

		nio.close();
		// Note we don't do store.close(); because we don't own the store.
		log.info("Peer shutdown complete for "+getPeerKey());
		shutdownFuture.complete(Utils.getCurrentTimestamp());
	}

	/**
	 * Gets the host address for this Server (including port), or null if closed
	 *
	 * @return Host Address
	 */
	public InetSocketAddress getHostAddress() {
		return nio.getHostAddress();
	}

	/**
	 * Returns the Keypair for this peer server
	 *
	 * SECURITY: Be careful with this!
	 * @return Key pair for Peer
	 */
	public AKeyPair getKeyPair() {
		return getPeer().getKeyPair();
	}

	/**
	 * Gets the public key of the peer account
	 *
	 * @return AccountKey of this Peer
	 */
	public AccountKey getPeerKey() {
		AKeyPair kp = getKeyPair();
		if (kp == null) return null;
		return kp.getAccountKey();
	}

	/**
	 * Gets the Store configured for this Server. A server must consistently use the
	 * same store instance for all Server threads, as values may be shared.
	 *
	 * @return Store instance
	 */
	public AStore getStore() {
		return store;
	}

	public ConnectionManager getConnectionManager() {
		return manager;
	}

	public HashMap getConfig() {
		return config;
	}

	/**
	 * Gets the action to perform for an incoming client message
	 * @return Message consumer
	 */
	public Consumer getReceiveAction() {
		return receiveAction;
	}

	/**
	 * Sets the desired host name for this Server
	 * @param string Desired host name String, e.g. "my-domain.com:12345"
	 */
	public void setHostname(String string) {
		config.put(Keywords.URL, string);
	}


	/**
	 * Flag for a running server. Setting to false will terminate server threads.
	 */
	private volatile boolean isRunning = true;
	
	/**
	 * Flag for a live server. Live Server has synced with at least one other peer
	 */
	private volatile boolean isLive = false;
	
	/**
	 * Checks is the server is Live, i.e. currently syncing successfully with network
	 * @return True if live, false otherwise
	 */
	public boolean isLive() {
		return isLive;
	}
	
	/**
	 * Checks if the Server threads are running
	 * @return True if running, false otherwise
	 */
	public boolean isRunning() {
		return isRunning;
	}

	public TransactionHandler getTransactionHandler() {
		return transactionHandler;
	}

	public BeliefPropagator getBeliefPropagator() {
		return propagator;
	}
 
	/**
	 * Triggers CVM Executor Belief update
	 * @param belief New Belief
	 */
	public void updateBelief(Belief belief) {
		executor.queueUpdate(belief);
	}

	public CVMExecutor getCVMExecutor() {
		return executor;
	}

	public QueryHandler getQueryProcessor() {
		return queryHandler;
	}

	/**
	 * Shut down the Server, as gracefully as possible.
	 * @throws TimeoutException If shutdown attempt times out
	 * @throws IOException  In case of IO Error
	 */
	public void shutdown()  {
		try {
			AKeyPair kp= getKeyPair();
			AccountKey key=kp.getAccountKey();
			Convex convex=Convex.connect(this, getPeerController(),kp);
			Result r=convex.transactSync("(set-peer-stake "+key+" 0)");
			if (r.isError()) {
				log.warn("Unable to remove Peer stake: "+r);
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		} finally {
			isLive=false;
			close();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy