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

convex.peer.TransactionHandler Maven / Gradle / Ivy

package convex.peer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.io.IOException;

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

import convex.core.Block;
import convex.core.BlockResult;
import convex.core.Constants;
import convex.core.ErrorCodes;
import convex.core.Peer;
import convex.core.Result;
import convex.core.SourceCodes;
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.AccountStatus;
import convex.core.data.Address;
import convex.core.data.Cells;
import convex.core.data.Hash;
import convex.core.data.Keyword;
import convex.core.data.Keywords;
import convex.core.data.PeerStatus;
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.lang.RT;
import convex.core.lang.Reader;
import convex.core.transactions.ATransaction;
import convex.core.transactions.Invoke;
import convex.core.util.LoadMonitor;
import convex.core.util.Utils;
import convex.net.Message;

/**
 * Server component for handling client transactions and producing Blocks
 * 
 * Main loop for this component handles client transaction messages, validates them and 
 * prepares them for inclusion in a Block
 */
public class TransactionHandler extends AThreadedComponent {
	
	static final Logger log = LoggerFactory.getLogger(BeliefPropagator.class.getName());
	
	/**
	 * Default minimum delay between proposing own transactions as a peer
	 */
	private static final long OWN_BLOCK_DELAY=2000;

	/**
	 * Default minimum delay between proposing a block as a peer
	 */
	private static final long DEFAULT_MIN_BLOCK_TIME=10;
	
	/**
	 * Queue for incoming (unverified) transaction messages
	 */
	protected final ArrayBlockingQueue txMessageQueue;
	
	/**
	 * Queue for valid received Transactions submitted for clients of this Peer
	 */
	ArrayBlockingQueue> transactionQueue;
	
	public TransactionHandler(Server server) {
		super(server);	
		txMessageQueue= new ArrayBlockingQueue<>(Config.TRANSACTION_QUEUE_SIZE);
		transactionQueue=new ArrayBlockingQueue<>(Config.TRANSACTION_QUEUE_SIZE);	
	}
	
	/**
	 * Offer a transaction for handling
	 * @param m Message offered
	 * @return True if queued for handling, false otherwise
	 */
	public boolean offerTransaction(Message m) {
		return txMessageQueue.offer(m);
	}
	
	/**
	 * Register of client interests in receiving transaction responses
	 */
	private HashMap interests = new HashMap<>();

	public long clientTransactionCount=0;
	public long receivedTransactionCount=0;

	private Consumer> requestObserver;

	/**
	 * Register interest in receiving a result for a transaction
	 * @param signedTransactionHash
	 * @param m
	 */
	private void registerInterest(Hash signedTransactionHash, Message m) {
		interests.put(signedTransactionHash, m);
	}
	
	protected void processMessage(Message m) throws InterruptedException {
		try {
			this.receivedTransactionCount++;
			
			// Transaction is a vector [id , signed-object]
			AVector v = m.getPayload();
			@SuppressWarnings("unchecked")
			SignedData sd = (SignedData) v.get(1);
			
			// Check our transaction is valid and we want to process it
			Result error=checkTransaction(sd);
			if (error!=null) {
				m.returnResult(error.withSource(SourceCodes.PEER));
				return;
			}

			// Persist the signed transaction. Might throw MissingDataException?
			// If we already have the transaction persisted, will obtain signature status
			sd=Cells.persist(sd);
	
			// Put on Server's transaction queue. We are OK to block here
			LoadMonitor.down();
			transactionQueue.put(sd);
			observeTransactionRequest(sd);
			LoadMonitor.up();
			this.clientTransactionCount++;
			
			registerInterest(sd.getHash(), m);		
		} catch (BadFormatException | IOException e) {
			log.warn("Unandled exception in transaction handler",e);
			m.closeConnection();
		}
	}
	
	private Result checkTransaction(SignedData sd) {

		// TODO: throttle?
		ATransaction tx=RT.ensureTransaction(sd.getValue());
		
		// System.out.println("transact: "+v);
		if (tx==null) {
			return Result.error(ErrorCodes.FORMAT,Strings.BAD_FORMAT);
		}
		
		State s=server.getPeer().getConsensusState();
		AccountStatus as=s.getAccount(tx.getOrigin());
		if (as==null) {
			return Result.error(ErrorCodes.NOBODY, Strings.NO_SUCH_ACCOUNT);
		}
		
		if (tx.getSequence()<=as.getSequence()) {
			return Result.error(ErrorCodes.SEQUENCE, Strings.OLD_SEQUENCE);
		}
		
		AccountKey expectedKey=as.getAccountKey();
		if (expectedKey==null) {
			return Result.error(ErrorCodes.STATE, Strings.NO_TX_FOR_ACTOR);
		}
		
		AccountKey pubKey=sd.getAccountKey();
		if (!expectedKey.equals(pubKey)) {
			return Result.error(ErrorCodes.SIGNATURE, Strings.WRONG_KEY );
		}
		
		if (!sd.checkSignature()) {
			// SECURITY: Client tried to send a badly signed transaction!
			return Result.error(ErrorCodes.SIGNATURE, Strings.BAD_SIGNATURE);
		}

		// All checks passed OK!
		return null;
	}
	
	/**
	 * Sets a request observer, which will be called whenever the Peer
	 * processes a valid client transaction request
	 * @param observer Consumer to receive observed transaction
	 */
	public void setRequestObserver(Consumer> observer) {
		this.requestObserver=observer;
	}
	
	private void observeTransactionRequest(SignedData sd) {
		Consumer> observer=this.requestObserver;
		if (observer!=null) {
			observer.accept(sd);
		}
	}

	long reportedConsensusPoint;

	private BiConsumer, Result> responseObserver;

	public void maybeReportTransactions(Peer peer) {
		// Report transaction results
		long newConsensusPoint = peer.getFinalityPoint();
		if (newConsensusPoint > reportedConsensusPoint) {
			log.debug("Consensus point update from {} to {}" ,reportedConsensusPoint , newConsensusPoint);
			for (long i = reportedConsensusPoint; i < newConsensusPoint; i++) {
				SignedData block = peer.getPeerOrder().getBlock(i);
				// only report our own transactions!
				if (block.getAccountKey().equals(peer.getPeerKey())) {
					BlockResult br = peer.getBlockResult(i);
					reportTransactions(block.getValue(), br,i);
				}
			}
			reportedConsensusPoint=newConsensusPoint;
		}
	}
	
	private void reportTransactions(Block block, BlockResult br, long blockNum) {
		// TODO: consider culling old interests after some time period
		int nTrans = block.length();
		HashMap extInfo=new HashMap<>(5);
		for (long j = 0; j < nTrans; j++) {
			SignedData t = block.getTransactions().get(j);
			Hash h = t.getHash();
			Message m = interests.get(h);
			if (m != null) {
				ACell id = m.getID();
				log.trace("Returning transaction result ID {}", id);
				Result res = br.getResults().get(j);
				
				extInfo.put(Keywords.LOC,Vectors.of(blockNum,j));
				extInfo.put(Keywords.TX,t.getHash());
				
				res=res.withExtraInfo(extInfo);

				boolean reported = m.returnResult(res);
				if (!reported) {
					// ignore?
				}
				observeTransactionResponse(t,res);
				interests.remove(h);
			}
		}
	}
	
	/**
	 * Sets a request observer, which will be called whenever the Peer
	 * processes a valid client transaction request
	 * @param observer Consumer to receive observed transaction
	 */
	public void setResponseObserver(BiConsumer,Result> observer) {
		this.responseObserver=observer;
	}
	
	private void observeTransactionResponse(SignedData sd, Result r) {
		BiConsumer,Result> observer=this.responseObserver;
		if (observer!=null) {
			observer.accept(sd,r);
		}
	}
	
	/**
	 * Checks for pending transactions, and if found propose them as a new Block.
	 *
	 * @return New signed Block, or null if nothing to publish yet
	 */
	protected SignedData maybeGenerateBlock(Peer peer) {
		long timestamp=Utils.getCurrentTimestamp();

		if (!readyToPublish(peer)) return null;
		
		long minBlockTime=getMinBlockTime();
		
		if (timestamp>) newTransactions);
		newTransactions.clear();
		lastBlockPublishedTime=Utils.getCurrentTimestamp();
		SignedData signedBlock=peer.getKeyPair().signData(block);
		return signedBlock;
	}
	
	/**
	 * Gets the next Block for publication, or null if not yet ready
	 * @return New Block, or null if not yet produced
	 */
	public SignedData maybeGetBlock() {
		return maybeGenerateBlock(server.getPeer());
	}
	
	/**
	 * Checks if the Peer is ready to publish a Block
	 * @param peer Current Peer instance
	 * @return true if ready to publish, false otherwise
	 */
	private boolean readyToPublish(Peer peer) {
		return true;
	}

	Long minBlockTime=null;
	private long getMinBlockTime() {
		if (minBlockTime==null) {
			HashMap config = server.getConfig();
			CVMLong mbt=CVMLong.parse(config.get(Keywords.MIN_BLOCK_TIME));
			minBlockTime =(mbt==null)?DEFAULT_MIN_BLOCK_TIME:mbt.longValue();
		}
		return minBlockTime;
	}

	/**
	 * The list of new transactions to be added to the next Block. Accessed only in update loop
	 *
	 * Must all have been fully persisted.
	 */
	private ArrayList> newTransactions = new ArrayList<>();

	/**
	 * Last time at which the Peer's own transactions was submitted 
	 */
	private long lastOwnTransactionTimestamp=0L;

	/**
	 * Time at which last Block was published by this Peer
	 */
	protected long lastBlockPublishedTime=0L;


	
	/**
	 * Check if the Peer want to send any of its own transactions
	 * @param transactionList List of transactions to add to.
	 */
	void maybeGetOwnTransactions(Peer p) {
		long ts=Utils.getCurrentTimestamp();

		// If we already posted own transaction recently, don't try again
		if (ts<(lastOwnTransactionTimestamp+OWN_BLOCK_DELAY)) return;

		// NOTE: beyond this point we only execute stuff when AUTO_MANAGE is set
		if (!Utils.bool(server.getConfig().get(Keywords.AUTO_MANAGE))) return;

		State s=p.getConsensusState();
		String desiredHostname=server.getHostname(); // Intended hostname
		AccountKey peerKey=p.getPeerKey();
		PeerStatus ps=s.getPeer(peerKey);
		if (ps==null) return; // No peer record in consensus state?
		
		AString chn=ps.getHostname();
		String currentHostname=(chn==null)?null:chn.toString();
		
		// Try to set hostname if not correctly set
		trySetHostname:
		if (!Utils.equals(desiredHostname, currentHostname)) {
			log.debug("Trying to update own hostname from: {} to {}",currentHostname,desiredHostname);
			Address address=ps.getController();
			if (address==null) break trySetHostname;
			AccountStatus as=s.getAccount(address);
			if (as==null) break trySetHostname;
			if (!Cells.equals(peerKey, as.getAccountKey())) break trySetHostname;

			String code;
			if (desiredHostname==null) {
				code = String.format("(set-peer-data %s {:url nil})", peerKey);
			} else {
				code = String.format("(set-peer-data %s {:url \"%s\"})", peerKey, desiredHostname);
			}
			ACell message = Reader.read(code);
			ATransaction transaction = Invoke.create(address, as.getSequence()+1, message);
			newTransactions.add(p.getKeyPair().signData(transaction));
			lastOwnTransactionTimestamp=ts; // mark this timestamp
		}
	}

	public void close() {
		super.close();
	}

	public void start() {
		this.reportedConsensusPoint=server.getPeer().getFinalityPoint();
		super.start();

	}

	public boolean isAwaitingResults() {
		return interests.size()>0;
	}

	public int countInterests() {
		return interests.size();
	}
	
	ArrayList messages=new ArrayList<>();

	/**
	 * Loops for handling incoming client transactions
	 */
	@Override
	protected void loop() throws InterruptedException {
		long BLOCKTIME=getMinBlockTime();
		try {
			LoadMonitor.down();
			Message m = txMessageQueue.poll(BLOCKTIME, TimeUnit.MILLISECONDS);
			LoadMonitor.up();
			if (m==null) return;
			
			// We have at least one transaction to handle, drain queue to get the rest
			messages.add(m);
			txMessageQueue.drainTo(messages);
			
			// Process transaction messages
			// This might block if we aren't generating blocks fast enough
			// Which is OK, since we transfer backpressure to clients
			for (Message msg: messages) {
				processMessage(msg);
			}
		} finally {
			messages.clear();
		}
	}

	@Override
	protected String getThreadName() {
		return "Transaction handler on port: "+server.getPort();
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy