package controller; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.logging.Logger; import org.mapdb.Fun.Tuple2; import api.ApiService; import qora.BlockChain; import qora.BlockGenerator; import qora.Synchronizer; import qora.TransactionCreator; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.assets.Asset; import qora.assets.Order; import qora.assets.Trade; import qora.block.Block; import qora.crypto.Ed25519; import qora.naming.Name; import qora.naming.NameSale; import qora.payment.Payment; import qora.transaction.Transaction; import qora.voting.Poll; import qora.voting.PollOption; import qora.wallet.Wallet; import settings.Settings; import utils.ObserverMessage; import utils.Pair; import database.DBSet; import database.SortableList; import network.Network; import network.Peer; import network.message.BlockMessage; import network.message.GetBlockMessage; import network.message.GetSignaturesMessage; import network.message.Message; import network.message.MessageFactory; import network.message.TransactionMessage; import network.message.VersionMessage; public class Controller extends Observable { public static final int STATUS_NO_CONNECTIONS = 0; public static final int STATUS_SYNCHRONIZING = 1; public static final int STATUS_OKE = 2; private int status; private Network network; private ApiService rpcService; private BlockChain blockChain; private BlockGenerator blockGenerator; private Wallet wallet; private Synchronizer synchronizer; private TransactionCreator transactionCreator; private Map<Peer, Integer> peerHeight; private static Controller instance; public Map<Peer, Integer> getPeerHeights() { return peerHeight; } public static Controller getInstance() { if(instance == null) { instance = new Controller(); } return instance; } public int getStatus() { return this.status; } public void start(boolean disableRpc) throws Exception { //CHECK NETWORK PORT AVAILABLE if(!Network.isPortAvailable(Network.PORT)) { throw new Exception("Network port " + Network.PORT + " already in use!"); } //CHECK RPC PORT AVAILABLE if(!disableRpc) { if(!Network.isPortAvailable(Settings.getInstance().getRpcPort())) { throw new Exception("Rpc port " + Settings.getInstance().getRpcPort() + " already in use!"); } } //LOAD NATIVE LIBRARIES if(!Ed25519.load()) { throw new Exception("Failed to load native libraries!"); } this.peerHeight = new LinkedHashMap<Peer, Integer>(); //LINKED TO PRESERVE ORDER WHEN SYNCHRONIZING (PRIORITIZE SYNCHRONIZING FROM LONGEST CONNECTION ALIVE) this.status = STATUS_NO_CONNECTIONS; this.transactionCreator = new TransactionCreator(); //OPENING DATABASES DBSet.getInstance(); if(DBSet.getInstance().getBlockMap().isProcessing()) { throw new Exception("The application was not closed correctly!"); } //CREATE SYNCHRONIZOR this.synchronizer = new Synchronizer(); //CREATE BLOCKCHAIN this.blockChain = new BlockChain(); //CREATE BLOCKGENERATOR this.blockGenerator = new BlockGenerator(); if(!disableRpc) { this.rpcService = new ApiService(); this.rpcService.start(); } //CREATE WALLET this.wallet = new Wallet(); //START BLOCKGENERATOR this.blockGenerator.start(); //CREATE NETWORK this.network = new Network(); //CLOSE ON UNEXPECTED SHUTDOWN Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { stopAll(); } }); //REGISTER DATABASE OBSERVER this.addObserver(DBSet.getInstance().getTransactionMap()); this.addObserver(DBSet.getInstance()); } @Override public void addObserver(Observer o) { //ADD OBSERVER TO SYNCHRONIZER //this.synchronizer.addObserver(o); DBSet.getInstance().getBlockMap().addObserver(o); //ADD OBSERVER TO BLOCKGENERATOR //this.blockGenerator.addObserver(o); DBSet.getInstance().getTransactionMap().addObserver(o); //ADD OBSERVER TO NAMESALES DBSet.getInstance().getNameExchangeMap().addObserver(o); //ADD OBSERVER TO POLLS DBSet.getInstance().getPollMap().addObserver(o); //ADD OBSERVER TO ASSETS DBSet.getInstance().getAssetMap().addObserver(o); //ADD OBSERVER TO ORDERS DBSet.getInstance().getOrderMap().addObserver(o); //ADD OBSERVER TO TRADES DBSet.getInstance().getTradeMap().addObserver(o); //ADD OBSERVER TO BALANCES DBSet.getInstance().getBalanceMap().addObserver(o); //ADD OBSERVER TO CONTROLLER super.addObserver(o); o.update(this, new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } @Override public void deleteObserver(Observer o) { DBSet.getInstance().getBlockMap().deleteObserver(o); super.deleteObserver(o); } public void deleteWalletObserver(Observer o) { this.wallet.deleteObserver(o); } private boolean isStopping = false; public void stopAll() { //PREVENT MULTIPLE CALLS if(!this.isStopping) { this.isStopping = true; //STOP MESSAGE PROCESSOR Logger.getGlobal().info("Stopping message processor"); this.network.stop(); //STOP BLOCK PROCESSOR Logger.getGlobal().info("Stopping block processor"); this.synchronizer.stop(); //CLOSE DATABABASE Logger.getGlobal().info("Closing database"); DBSet.getInstance().close(); //CLOSE WALLET Logger.getGlobal().info("Closing wallet"); this.wallet.close(); //FORCE CLOSE System.exit(0); } } //NETWORK public List<Peer> getActivePeers() { //GET ACTIVE PEERS return this.network.getActiveConnections(); } public void onConnect(Peer peer) { //GET HEIGHT int height = this.blockChain.getHeight(); //SEND VERSION MESSAGE peer.sendMessage(MessageFactory.getInstance().createVersionMessage(height)); if(this.status == STATUS_NO_CONNECTIONS) { //UPDATE STATUS this.status = STATUS_OKE; //NOTIFY this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } } public void onDisconnect(Peer peer) { synchronized(this.peerHeight) { this.peerHeight.remove(peer); if(this.peerHeight.size() == 0) { //UPDATE STATUS this.status = STATUS_NO_CONNECTIONS; //NOTIFY this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } } } public void onError(Peer peer) { this.onDisconnect(peer); } //SYNCHRONIZED DO NOT PROCESSS MESSAGES SIMULTANEOUSLY public void onMessage(Message message) { Message response; Block block; synchronized(this) { switch(message.getType()) { case Message.PING_TYPE: //CREATE PING response = MessageFactory.getInstance().createPingMessage(); //SET ID response.setId(message.getId()); //SEND BACK TO SENDER message.getSender().sendMessage(response); break; case Message.VERSION_TYPE: VersionMessage versionMessage = (VersionMessage) message; //ADD TO LIST synchronized(this.peerHeight) { this.peerHeight.put(versionMessage.getSender(), versionMessage.getHeight()); } break; case Message.GET_SIGNATURES_TYPE: GetSignaturesMessage getHeadersMessage = (GetSignaturesMessage) message; //ASK SIGNATURES FROM BLOCKCHAIN List<byte[]> headers = this.blockChain.getSignatures(getHeadersMessage.getParent()); //CREATE RESPONSE WITH SAME ID response = MessageFactory.getInstance().createHeadersMessage(headers); response.setId(message.getId()); //SEND RESPONSE BACK WITH SAME ID message.getSender().sendMessage(response); break; case Message.GET_BLOCK_TYPE: GetBlockMessage getBlockMessage = (GetBlockMessage) message; //ASK BLOCK FROM BLOCKCHAIN block = this.blockChain.getBlock(getBlockMessage.getSignature()); //CREATE RESPONSE WITH SAME ID response = MessageFactory.getInstance().createBlockMessage(block); response.setId(message.getId()); //SEND RESPONSE BACK WITH SAME ID message.getSender().sendMessage(response); break; case Message.BLOCK_TYPE: BlockMessage blockMessage = (BlockMessage) message; //ASK BLOCK FROM BLOCKCHAIN block = blockMessage.getBlock(); //CHECK IF VALID if(this.blockChain.isNewBlockValid(block)) { Logger.getGlobal().info("received new valid block"); //PROCESS this.synchronizer.process(block); //BROADCAST List<Peer> excludes = new ArrayList<Peer>(); excludes.add(message.getSender()); this.network.broadcast(message, excludes); //UPDATE ALL PEER HEIGHTS TO OUR HEIGHT /*synchronized(this.peerHeight) { for(Peer peer: this.peerHeight.keySet()) { this.peerHeight.put(peer, this.blockChain.getHeight()); } }*/ } else { synchronized(this.peerHeight) { //UPDATE SENDER HEIGHT + 1 this.peerHeight.put(message.getSender(), blockMessage.getHeight()); } } break; case Message.TRANSACTION_TYPE: TransactionMessage transactionMessage = (TransactionMessage) message; //GET TRANSACTION Transaction transaction = transactionMessage.getTransaction(); //CHECK IF SIGNATURE IS VALID OR GENESIS TRANSACTION if(!transaction.isSignatureValid() || transaction.getType() == Transaction.GENESIS_TRANSACTION) { //DISHONEST PEER this.network.onError(message.getSender()); return; } //CHECK IF TRANSACTION HAS MINIMUM FEE AND MINIMUM FEE PER BYTE if(transaction.hasMinimumFee() && transaction.hasMinimumFeePerByte()) { //ADD TO UNCONFIRMED TRANSACTIONS this.blockGenerator.addUnconfirmedTransaction(transaction); //NOTIFY OBSERVERS //this.setChanged(); //this.notifyObservers(new ObserverMessage(ObserverMessage.LIST_TRANSACTION_TYPE, DatabaseSet.getInstance().getTransactionsDatabase().getTransactions())); this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.ADD_TRANSACTION_TYPE, transaction)); //BROADCAST List<Peer> excludes = new ArrayList<Peer>(); excludes.add(message.getSender()); this.network.broadcast(message, excludes); } break; } } } public void addActivePeersObserver(Observer o) { this.network.addObserver(o); } public void removeActivePeersObserver(Observer o) { this.network.deleteObserver(o); } private void broadcastBlock(Block newBlock) { //CREATE MESSAGE Message message = MessageFactory.getInstance().createBlockMessage(newBlock); //BROADCAST MESSAGE List<Peer> excludes = new ArrayList<Peer>(); this.network.broadcast(message, excludes); } private void broadcastTransaction(Transaction transaction) { //CREATE MESSAGE Message message = MessageFactory.getInstance().createTransactionMessage(transaction); //BROADCAST MESSAGE List<Peer> excludes = new ArrayList<Peer>(); this.network.broadcast(message, excludes); } //SYNCHRONIZE public boolean isUpToDate() { if(this.peerHeight.size() == 0) { return true; } int maxPeerHeight = this.getMaxPeerHeight(); int chainHeight = this.blockChain.getHeight(); return maxPeerHeight <= chainHeight; } public void update() { //UPDATE STATUS this.status = STATUS_SYNCHRONIZING; //NOTIFY this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); Peer peer = null; try { //WHILE NOT UPTODATE while(!this.isUpToDate()) { //START UPDATE FROM HIGHEST HEIGHT PEER peer = this.getMaxHeightPeer(); //SYNCHRONIZE FROM PEER this.synchronizer.synchronize(peer); } } catch (Exception e) { e.printStackTrace(); if(peer != null) { //DISHONEST PEER this.network.onError(peer); } } if(this.peerHeight.size() == 0) { //UPDATE STATUS this.status = STATUS_NO_CONNECTIONS; //NOTIFY this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } else { //UPDATE STATUS this.status = STATUS_OKE; //NOTIFY this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } } private Peer getMaxHeightPeer() { Peer highestPeer = null; int height = 0; try { synchronized(this.peerHeight) { for(Peer peer: this.peerHeight.keySet()) { if(peer == null) { highestPeer = peer; } else { //IF HEIGHT IS BIGGER if(height < this.peerHeight.get(peer)) { highestPeer = peer; height = this.peerHeight.get(peer); } //IF HEIGHT IS SAME if(height == this.peerHeight.get(peer)) { //CHECK IF PING OF PEER IS BETTER if(peer.getPing() < highestPeer.getPing()) { highestPeer = peer; } } } } } } catch(Exception e) { //PEER REMOVED WHILE ITERATING } return highestPeer; } private int getMaxPeerHeight() { int height = 0; try { synchronized(this.peerHeight) { for(Peer peer: this.peerHeight.keySet()) { if(height < this.peerHeight.get(peer)) { height = this.peerHeight.get(peer); } } } } catch(Exception e) { //PEER REMOVED WHILE ITERATING } return height; } //WALLET public boolean doesWalletExists() { //CHECK IF WALLET EXISTS return this.wallet.exists(); } public boolean createWallet(byte[] seed, String password, int amount) { //IF NEW WALLET CREADED return this.wallet.create(seed, password, amount, false); } public boolean recoverWallet(byte[] seed, String password, int amount) { return this.wallet.create(seed, password, amount, true); } public List<Account> getAccounts() { return this.wallet.getAccounts(); } public List<PrivateKeyAccount> getPrivateKeyAccounts() { return this.wallet.getprivateKeyAccounts(); } public String generateNewAccount() { return this.wallet.generateNewAccount(); } public PrivateKeyAccount getPrivateKeyAccountByAddress(String address) { return this.wallet.getPrivateKeyAccount(address); } public Account getAccountByAddress(String address) { return this.wallet.getAccount(address); } public BigDecimal getUnconfirmedBalance(String address) { return this.wallet.getUnconfirmedBalance(address); } public void addWalletListener(Observer o) { this.wallet.addObserver(o); } public String importAccountSeed(byte[] accountSeed) { return this.wallet.importAccountSeed(accountSeed); } public byte[] exportAccountSeed(String address) { return this.wallet.exportAccountSeed(address); } public byte[] exportSeed() { return this.wallet.exportSeed(); } public boolean deleteAccount(PrivateKeyAccount account) { return this.wallet.deleteAccount(account); } public void synchronizeWallet() { this.wallet.synchronize(); } public boolean isWalletUnlocked() { return this.wallet.isUnlocked(); } public boolean lockWallet() { return this.wallet.lock(); } public boolean unlockWallet(String password) { return this.wallet.unlock(password); } public List<Pair<Account, Transaction>> getLastTransactions(int limit) { return this.wallet.getLastTransactions(limit); } public Transaction getTransaction(byte[] signature) { //CHECK IF IN BLOCK Block block = DBSet.getInstance().getTransactionParentMap().getParent(signature); if(block != null) { return block.getTransaction(signature); } //CHECK IF IN TRANSACTION DATABASE return DBSet.getInstance().getTransactionMap().get(signature); } public List<Transaction> getLastTransactions(Account account, int limit) { return this.wallet.getLastTransactions(account, limit); } public List<Pair<Account, Block>> getLastBlocks() { return this.wallet.getLastBlocks(); } public List<Block> getLastBlocks(Account account) { return this.wallet.getLastBlocks(account); } public List<Pair<Account, Name>> getNames() { return this.wallet.getNames(); } public List<Name> getNames(Account account) { return this.wallet.getNames(account); } public List<Pair<Account, NameSale>> getNameSales() { return this.wallet.getNameSales(); } public List<NameSale> getNameSales(Account account) { return this.wallet.getNameSales(account); } public List<NameSale> getAllNameSales() { return DBSet.getInstance().getNameExchangeMap().getNameSales(); } public List<Pair<Account, Poll>> getPolls() { return this.wallet.getPolls(); } public List<Poll> getPolls(Account account) { return this.wallet.getPolls(account); } public void addAssetFavorite(Asset asset) { this.wallet.addAssetFavorite(asset); } public void removeAssetFavorite(Asset asset) { this.wallet.removeAssetFavorite(asset); } public boolean isAssetFavorite(Asset asset) { return this.wallet.isAssetFavorite(asset); } public Collection<Poll> getAllPolls() { return DBSet.getInstance().getPollMap().getValues(); } public void onDatabaseCommit() { this.wallet.commit(); } //BLOCKCHAIN public int getHeight() { return this.blockChain.getHeight(); } public Block getLastBlock() { return this.blockChain.getLastBlock(); } public Block getBlock(byte[] header) { return this.blockChain.getBlock(header); } public Pair<Block, List<Transaction>> scanTransactions(Block block, int blockLimit, int transactionLimit, int type, int service, Account account) { return this.blockChain.scanTransactions(block, blockLimit, transactionLimit, type, service, account); } public long getNextBlockGeneratingBalance() { return BlockGenerator.getNextBlockGeneratingBalance(DBSet.getInstance(), DBSet.getInstance().getBlockMap().getLastBlock()); } public long getNextBlockGeneratingBalance(Block parent) { return BlockGenerator.getNextBlockGeneratingBalance(DBSet.getInstance(), parent); } //FORGE public void newBlockGenerated(Block newBlock) { //ADD TO BLOCKCHAIN this.synchronizer.process(newBlock); //BROADCAST this.broadcastBlock(newBlock); } public List<Transaction> getUnconfirmedTransactions() { return this.blockGenerator.getUnconfirmedTransactions(); } //BALANCES public SortableList<Tuple2<String, Long>, BigDecimal> getBalances(long key) { return DBSet.getInstance().getBalanceMap().getBalancesSortableList(key); } public SortableList<Tuple2<String, Long>, BigDecimal> getBalances(Account account) { return DBSet.getInstance().getBalanceMap().getBalancesSortableList(account); } //NAMES public Name getName(String nameName) { return DBSet.getInstance().getNameMap().get(nameName); } public NameSale getNameSale(String nameName) { return DBSet.getInstance().getNameExchangeMap().getNameSale(nameName); } //POLLS public Poll getPoll(String name) { return DBSet.getInstance().getPollMap().get(name); } //ASSETS public Asset getQoraAsset() { return DBSet.getInstance().getAssetMap().get(0l); } public Asset getAsset(long key) { return DBSet.getInstance().getAssetMap().get(key); } public SortableList<BigInteger, Order> getOrders(Asset have, Asset want) { return DBSet.getInstance().getOrderMap().getOrdersSortableList(have.getKey(), want.getKey()); } public SortableList<Tuple2<BigInteger, BigInteger>, Trade> getTrades(Asset have, Asset want) { return DBSet.getInstance().getTradeMap().getTradesSortableList(have.getKey(), want.getKey()); } public SortableList<Tuple2<BigInteger, BigInteger>, Trade> getTrades(Order order) { return DBSet.getInstance().getTradeMap().getTrades(order); } //TRANSACTIONS public void onTransactionCreate(Transaction transaction) { //ADD TO UNCONFIRMED TRANSACTIONS this.blockGenerator.addUnconfirmedTransaction(transaction); //NOTIFY OBSERVERS this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.LIST_TRANSACTION_TYPE, DBSet.getInstance().getTransactionMap().getValues())); this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.ADD_TRANSACTION_TYPE, transaction)); //BROADCAST this.broadcastTransaction(transaction); } public Pair<Transaction, Integer> sendPayment(PrivateKeyAccount sender, Account recipient, BigDecimal amount, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createPayment(sender, recipient, amount, fee); } } public Pair<Transaction, Integer> registerName(PrivateKeyAccount registrant, Account owner, String name, String value, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createNameRegistration(registrant, new Name(owner, name, value), fee); } } public Pair<Transaction, Integer> updateName(PrivateKeyAccount owner, Account newOwner, String name, String value, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createNameUpdate(owner, new Name(newOwner, name, value), fee); } } public Pair<Transaction, Integer> sellName(PrivateKeyAccount owner, String name, BigDecimal amount, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createNameSale(owner, new NameSale(name, amount), fee); } } public Pair<Transaction, Integer> cancelSellName(PrivateKeyAccount owner, NameSale nameSale, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createCancelNameSale(owner, nameSale, fee); } } public Pair<Transaction, Integer> BuyName(PrivateKeyAccount buyer, NameSale nameSale, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createNamePurchase(buyer, nameSale, fee); } } public Pair<Transaction, Integer> createPoll(PrivateKeyAccount creator, String name, String description, List<String> options, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { //CREATE POLL OPTIONS List<PollOption> pollOptions = new ArrayList<PollOption>(); for(String option: options) { pollOptions.add(new PollOption(option)); } //CREATE POLL Poll poll = new Poll(creator, name, description, pollOptions); return this.transactionCreator.createPollCreation(creator, poll, fee); } } public Pair<Transaction, Integer> createPollVote(PrivateKeyAccount creator, Poll poll, PollOption option, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { //GET OPTION INDEX int optionIndex = poll.getOptions().indexOf(option); return this.transactionCreator.createPollVote(creator, poll.getName(), optionIndex, fee); } } public Pair<Transaction, Integer> createArbitraryTransaction(PrivateKeyAccount creator, int service, byte[] data, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createArbitraryTransaction(creator, service, data, fee); } } public Pair<Transaction, Integer> issueAsset(PrivateKeyAccount creator, String name, String description, long quantity, boolean divisible, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createIssueAssetTransaction(creator, name, description, quantity, divisible, fee); } } public Pair<Transaction, Integer> createOrder(PrivateKeyAccount creator, Asset have, Asset want, BigDecimal amount, BigDecimal price, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createOrderTransaction(creator, have, want, amount, price, fee); } } public Pair<Transaction, Integer> cancelOrder(PrivateKeyAccount creator, Order order, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createCancelOrderTransaction(creator, order, fee); } } public Pair<Transaction, Integer> transferAsset(PrivateKeyAccount sender, Account recipient, Asset asset, BigDecimal amount, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.createAssetTransfer(sender, recipient, asset, amount, fee); } } public Pair<Transaction, Integer> sendMultiPayment(PrivateKeyAccount sender, List<Payment> payments, BigDecimal fee) { //CREATE ONLY ONE TRANSACTION AT A TIME synchronized(this.transactionCreator) { return this.transactionCreator.sendMultiPayment(sender, payments, fee); } } }