From 8fa35752c0e5729afdffaeae814bfc8d09b2fdfe Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 25 Sep 2018 14:11:17 +0100 Subject: [PATCH 1/2] Fixed issues where forging a new block conflicts with synchronization leaving DB with two blocks at the same height. Changed BlockGenerator to repeatedly try to sync instead just one attempt, in case the syncing attempt fails due to ping timeout, etc. This also stops BlockGenerator from incorrectly changing status from syncing to OK. Controller no longer allows new blocks to forged if we're still syncing and nowhere near up to date. Added thread names to ConnectionCreator and ConnectionAcceptor. Don't process new incoming block if we're synchronizing! Fixed unnecessary error output when calling /blocks/byheight/{height} with a height that doesn't exist Attempt to avoid networking deadlock The networking layer can deadlock while processing an error condition, typically Controller, some ArrayList (e.g. Network.connectedPeers) and something else are all locked. One of these cases is where a peer's Pinger object is trying to join() after a ping failure, but can't because Network.connectedPeers is locked. To ease this case, when Pinger.stopPing() is called, don't bother to call join() after interrupt(). Waiting for the Pinger to exit isn't needed. No longer checks block signatures on arrival from peer as blocks may arrive out-of-order and depend on processing of previous blocks. (Checking will happen on in-order processing). In Controller.onDisconnect(), notifying observers now done outside of synchronized(this.peerHeight) to reduce deadlocks. Resend unconfirmed transactions on a new node connects When node receives a new block, it broadcasts new block to all peers except original sender. All peers except sender learn node's blockchain height, but sender does not. So now node's height is sent to sender (only height, not actual new block). synchronization: after a round of synchronization, a peer will broadcast its new height If remote peer sends new block but we have it already as blockchain tip, then still update our peer DB to reflect remote peer's new height. Send our height to random peer every 30 seconds instead of 5 minutes. Better checking of deleted entries from forked DBMaps. Generic code in DBMap classes to handle byte[] keys. Bumped version! --- Qora/src/api/QoraResource.java | 14 ++- Qora/src/controller/Controller.java | 119 +++++++++++++++++++---- Qora/src/database/DBListValueMap.java | 4 +- Qora/src/database/DBMap.java | 37 ++++++- Qora/src/database/DBMapValueMap.java | 4 +- Qora/src/network/ConnectionAcceptor.java | 2 + Qora/src/network/ConnectionCreator.java | 2 + Qora/src/network/Network.java | 18 ++-- Qora/src/network/Peer.java | 4 +- Qora/src/network/Pinger.java | 11 +-- Qora/src/qora/BlockBuffer.java | 4 + Qora/src/qora/BlockGenerator.java | 1 + Qora/src/qora/Synchronizer.java | 3 +- Qora/src/qora/block/Block.java | 4 + Qora/src/settings/Settings.java | 14 ++- Qora/src/test/CryptoTests.java | 36 +++++++ 16 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 Qora/src/test/CryptoTests.java diff --git a/Qora/src/api/QoraResource.java b/Qora/src/api/QoraResource.java index fd0ac75c..aa85066b 100644 --- a/Qora/src/api/QoraResource.java +++ b/Qora/src/api/QoraResource.java @@ -11,6 +11,8 @@ import org.json.simple.JSONValue; import controller.Controller; +import gui.ClosingDialog; +import gui.Gui; import lang.Lang; import settings.Settings; import utils.APIUtils; @@ -31,11 +33,15 @@ public String stop() if(Controller.getInstance().doesWalletExists() && !Controller.getInstance().isWalletUnlocked()) { throw ApiErrorFactory.getInstance().createError(ApiErrorFactory.ERROR_WALLET_LOCKED); } - + //STOP - Controller.getInstance().stopAll(); - System.exit(0); - + if (Gui.isGuiStarted()) + ClosingDialog.getInstance(); + else { + Controller.getInstance().stopAll(); + System.exit(0); + } + //RETURN return String.valueOf(true); } diff --git a/Qora/src/controller/Controller.java b/Qora/src/controller/Controller.java index 4f25c873..7b9bc71b 100644 --- a/Qora/src/controller/Controller.java +++ b/Qora/src/controller/Controller.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.LinkedHashMap; @@ -85,17 +86,18 @@ import utils.ObserverMessage; import utils.Pair; import utils.SysTray; +import utils.TransactionTimestampComparator; import utils.UpdateUtil; import webserver.WebService; public class Controller extends Observable { private static final Logger LOGGER = LogManager.getLogger(Controller.class); - private String version = "0.26.10"; - private String buildTime = "2018-06-13 07:57:00 UTC"; + private String version = "0.26.11"; + private String buildTime = "2018-09-25 12:29:00 UTC"; private long buildTimestamp; - public static final String releaseVersion = "0.26.10"; + public static final String releaseVersion = "0.26.11"; // TODO ENUM would be better here public static final int STATUS_NO_CONNECTIONS = 0; @@ -408,7 +410,7 @@ public void run() { } }); - // TIMER TO SEND HEIGHT TO NETWORK EVERY 5 MIN + // TIMER TO SEND HEIGHT TO RANDOM PEER this.timerPeerHeightUpdate.cancel(); this.timerPeerHeightUpdate = new Timer(); @@ -426,7 +428,7 @@ public void run() { } }; - this.timerPeerHeightUpdate.schedule(action, 5 * 60 * 1000, 5 * 60 * 1000); + this.timerPeerHeightUpdate.schedule(action, 30 * 1000, 30 * 1000); // REGISTER DATABASE OBSERVER this.addObserver(DBSet.getInstance().getTransactionMap()); @@ -649,6 +651,19 @@ public List getActivePeers() { } public void walletSyncStatusUpdate(int height) { + /* + * Prevent deadlock when a new block arrives from network while we're resyncing wallet. + * + * New block arrival locks Controller and wants Synchronizer, + * but it's possible Synchronizer is locked (e.g. by BlockGenerator) while performing a wallet sync + * and this.setChanged() would want a lock on Controller too, causing a deadlock. + * + * We avoid this by testing for block processing status and exiting early. + */ + + if (DBSet.getInstance().getBlockMap().isProcessing()) + return; + this.setChanged(); this.notifyObservers(new ObserverMessage(ObserverMessage.WALLET_SYNC_STATUS, height)); } @@ -671,9 +686,6 @@ public void onConnect(Peer peer) { if (DBSet.getInstance().isStopped()) return; - // GET HEIGHT - int height = this.blockChain.getHeight(); - if (NTP.getTime() >= Transaction.getPOWFIX_RELEASE()) { // SEND FOUNDMYSELF MESSAGE peer.sendMessage(MessageFactory.getInstance().createFindMyselfMessage(Controller.getInstance().getFoundMyselfID())); @@ -683,8 +695,19 @@ public void onConnect(Peer peer) { } // SEND HEIGHT MESSAGE - LOGGER.trace("Sending our height " + height + " to peer " + peer.getAddress()); - peer.sendMessage(MessageFactory.getInstance().createHeightMessage(height)); + getSendMyHeightToPeer(peer); + + // Resend any unconfirmed transactions + List transactions = DBSet.getInstance().getTransactionMap().getTransactions(); + + // Sort transactions chronologically + Collections.sort(transactions, new TransactionTimestampComparator()); + + // Send unconfirmed transactions + for (Transaction transaction : transactions) { + Message message = MessageFactory.getInstance().createTransactionMessage(transaction); + peer.sendMessage(message); + } if (this.status == STATUS_NO_CONNECTIONS) { // UPDATE STATUS @@ -743,12 +766,17 @@ public void onDisconnect(Peer peer) { // in case they attempt to access DB if (this.isStopping) return; - - // NOTIFY - this.setChanged(); - this.notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, this.status)); } } + + // NOTIFY, but in separate thread to avoid MapDB interrupt issue + new Thread() { + @Override + public void run() { + Controller.getInstance().setChanged(); + Controller.getInstance().notifyObservers(new ObserverMessage(ObserverMessage.NETWORK_STATUS, Controller.getInstance().status)); + } + }.start(); } public void onError(Peer peer) { @@ -830,10 +858,25 @@ public void onMessage(Message message) { case Message.BLOCK_TYPE: + // Don't process if we're synchronizing + if (this.status == STATUS_SYNCHRONIZING) + break; + BlockMessage blockMessage = (BlockMessage) message; - // ASK BLOCK FROM BLOCKCHAIN + // Get block from message block = blockMessage.getBlock(); + LOGGER.trace("Received block from peer " + message.getSender().getAddress()); + + // Compare to our blockchain tip + Block blockchainTip = this.blockChain.getLastBlock(); + if (blockchainTip.getHeight() == blockMessage.getHeight() && Arrays.equals(blockchainTip.getSignature(), block.getSignature())) { + // We have this block already but update our peer DB to reflect peer's height anyway + synchronized (this.peerHeight) { + this.peerHeight.put(message.getSender(), blockMessage.getHeight()); + } + break; + } boolean isNewBlockValid = this.blockChain.isNewBlockValid(block); @@ -846,6 +889,18 @@ public void onMessage(Message message) { if (this.isProcessingWalletSynchronize()) break; + /* + * Prevent deadlock when a new block arrives from network while we're resyncing wallet. + * + * New block arrival locks Controller and wants Synchronizer, + * but it's possible Synchronizer is locked (e.g. by BlockGenerator) while performing a wallet sync + * and this.setChanged() would want a lock on Controller too, causing a deadlock. + * + * We avoid this by testing for block processing status and exiting early. + */ + if (DBSet.getInstance().getBlockMap().isProcessing()) + break; + // CHECK IF VALID if (isNewBlockValid && this.synchronizer.process(block)) { LOGGER.info(Lang.getInstance().translate("received new valid block") + " " + block.getHeight()); @@ -858,6 +913,9 @@ public void onMessage(Message message) { excludes.add(message.getSender()); this.network.broadcast(message, excludes); + // Let sender know we've updated + this.getSendMyHeightToPeer(message.getSender()); + // UPDATE ALL PEER HEIGHTS TO OUR HEIGHT /* * synchronized(this.peerHeight) { for(Peer peer: this.peerHeight.keySet()) { this.peerHeight.put(peer, this.blockChain.getHeight()); } @@ -983,9 +1041,22 @@ public void update() { peer = this.getMaxHeightPeer(); if (peer != null) { + // Make a note of pre-sync height so we can tell if anything happened + int preSyncHeight = this.blockChain.getHeight(); + // SYNCHRONIZE FROM PEER LOGGER.info("Synchronizing using peer " + peer.getAddress().getHostAddress() + " with height " + peerHeight.get(peer) + " - ping " + peer.getPing() + "ms"); this.synchronizer.synchronize(peer); + + // If our height has changed, notify our peers + if (this.blockChain.getHeight() > preSyncHeight) { + Block blockchainTip = this.blockChain.getLastBlock(); + LOGGER.debug("Sending our new height " + blockchainTip.getHeight() + " to peers"); + Message message = MessageFactory.getInstance().createHeightMessage(blockchainTip.getHeight()); + + List excludes = new ArrayList(); + this.network.broadcast(message, excludes); + } } Thread.sleep(5 * 1000); @@ -999,6 +1070,8 @@ public void update() { // DISHONEST PEER this.network.onError(peer, e.getMessage()); } + + return; } if (this.peerHeight.size() == 0) { @@ -1354,10 +1427,17 @@ public long getNextBlockGeneratingBalance(Block parent) { // FORGE public void newBlockGenerated(Block newBlock) { - this.synchronizer.process(newBlock); + // Only process if we have connections and are not synchronizing + if (this.status == STATUS_OK) { + if (this.synchronizer.process(newBlock)) { + LOGGER.info("Forged new block " + newBlock.getHeight()); - // BROADCAST - this.broadcastBlock(newBlock); + // BROADCAST + this.broadcastBlock(newBlock); + } else { + LOGGER.info("Couldn't forge new block"); + } + } } public List getUnconfirmedTransactions() { @@ -1655,6 +1735,9 @@ public Pair sendMessage(PrivateKeyAccount sender, Account public Block getBlockByHeight(int parseInt) { byte[] b = DBSet.getInstance().getHeightMap().getBlockByHeight(parseInt); + if (b == null) + return null; + return DBSet.getInstance().getBlockMap().get(b); } diff --git a/Qora/src/database/DBListValueMap.java b/Qora/src/database/DBListValueMap.java index d02fa442..c2ed5e6a 100644 --- a/Qora/src/database/DBListValueMap.java +++ b/Qora/src/database/DBListValueMap.java @@ -78,7 +78,7 @@ protected L getListForAdd(K key) { L newList = this.newListValue(); // If we previous marked entry as deleted then we're essentially recreating a new list - if (this.deleted != null && this.deleted.contains(key)) + if (this.deletedContains(key)) return newList; // If the parent map contains a list for key then we need to duplicate its entries @@ -148,7 +148,7 @@ protected L getListForRemove(K key) { return this.map.get(key); // If we previously marked entry as deleted then we have no list - if (this.deleted != null && this.deleted.contains(key)) + if (this.deletedContains(key)) return null; // If we have no parent or parent has no entry then there's no list diff --git a/Qora/src/database/DBMap.java b/Qora/src/database/DBMap.java index cb4e8ab0..a512bf4a 100644 --- a/Qora/src/database/DBMap.java +++ b/Qora/src/database/DBMap.java @@ -1,6 +1,7 @@ package database; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -98,7 +99,7 @@ public U get(T key) { return this.map.get(key); // If we've deleted the entry for key, return default value - if (this.deleted != null && this.deleted.contains(key)) + if (this.deletedContains(key)) return this.getDefaultValue(); // We don't have any entry for key (deleted or otherwise) so defer to parent (if present) @@ -201,7 +202,7 @@ public boolean contains(T key) { return true; // Have we deleted the entry for the key? - if (this.deleted != null && this.deleted.contains(key)) + if (this.deletedContains(key)) return false; // deleted, so not present // Defer to parent map (if present) @@ -280,4 +281,36 @@ public void reset() { this.notifyObservers(new ObserverMessage(this.getObservableData().get(NOTIFY_LIST), list)); } } + + /** + * byte[]-aware version of this.deleted.contains(key) + *

+ * We can't use simplistic this.deleted.contains(key) as the underlying ArrayList implementation + * uses Object.equals() to compare as this doesn't work for byte[]. + *

+ * This is only needed for this.deleted as this.parent is a more complex MapDB object. + *

+ * @param key + * @return true if this.deleted list contains key, false otherwise + */ + protected boolean deletedContains(T key) { + if (this.deleted == null || key == null) + return false; + + if (!(key instanceof byte[])) + return this.deleted.contains(key); + + // byte[]-safe + @SuppressWarnings("unchecked") + List deletedKeys = (List) this.deleted; + + byte[] byteKey = (byte[]) key; + + for (byte[] deletedKey : deletedKeys) + if (Arrays.equals(byteKey, deletedKey)) + return true; + + return false; + } + } diff --git a/Qora/src/database/DBMapValueMap.java b/Qora/src/database/DBMapValueMap.java index 1a70be1b..5a803216 100644 --- a/Qora/src/database/DBMapValueMap.java +++ b/Qora/src/database/DBMapValueMap.java @@ -86,7 +86,7 @@ protected M getMapForAdd(K dbKey) { M newMap = this.newMapValue(); // If we previous marked entry as deleted then we're essentially recreating a new map - if (this.deleted != null && this.deleted.contains(dbKey)) + if (this.deletedContains(dbKey)) return newMap; // If the parent DBMap contains a map for dbKey then we need to duplicate its entries @@ -156,7 +156,7 @@ protected M getMapForRemove(K dbKey) { return this.map.get(dbKey); // If we previously marked entry as deleted then we have no map - if (this.deleted != null && this.deleted.contains(dbKey)) + if (this.deletedContains(dbKey)) return null; // If we have no parent or parent has no entry then there's no map diff --git a/Qora/src/network/ConnectionAcceptor.java b/Qora/src/network/ConnectionAcceptor.java index 95bace0d..7750f100 100644 --- a/Qora/src/network/ConnectionAcceptor.java +++ b/Qora/src/network/ConnectionAcceptor.java @@ -27,6 +27,8 @@ public ConnectionAcceptor(ConnectionCallback callback) { } public void run() { + Thread.currentThread().setName("ConnAcceptor"); + this.isRun = true; while (isRun) { diff --git a/Qora/src/network/ConnectionCreator.java b/Qora/src/network/ConnectionCreator.java index 76bb7bfc..929ed79f 100644 --- a/Qora/src/network/ConnectionCreator.java +++ b/Qora/src/network/ConnectionCreator.java @@ -24,6 +24,8 @@ public ConnectionCreator(ConnectionCallback callback) { } public void run() { + Thread.currentThread().setName("ConnCreator"); + this.isRun = true; while (isRun) { diff --git a/Qora/src/network/Network.java b/Qora/src/network/Network.java index f5ff6199..720a54f0 100644 --- a/Qora/src/network/Network.java +++ b/Qora/src/network/Network.java @@ -16,6 +16,7 @@ import network.message.FindMyselfMessage; import network.message.Message; import network.message.MessageFactory; +import settings.Settings; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -115,7 +116,7 @@ public void onError(Peer peer, String error) { } // ADD TO BLACKLIST - PeerManager.getInstance().blacklistPeer(peer); + // PeerManager.getInstance().blacklistPeer(peer); // PASS TO CONTROLLER Controller.getInstance().onError(peer); @@ -233,7 +234,7 @@ public void onMessage(Message message) { } public void broadcast(Message message, List exclude) { - LOGGER.debug(Lang.getInstance().translate("Broadcasting")); + LOGGER.trace(Lang.getInstance().translate("Broadcasting") + " message type " + message.getType()); try { synchronized (this.connectedPeers) { @@ -249,7 +250,7 @@ public void broadcast(Message message, List exclude) { // Iterator fast-fail due to change in connectedPeers } - LOGGER.debug(Lang.getInstance().translate("Broadcasting end")); + LOGGER.trace(Lang.getInstance().translate("Broadcasting end") + " message type " + message.getType()); } @Override @@ -287,17 +288,10 @@ public void stop() { public static boolean isHostLocalAddress(InetAddress address) { // easy address checks first - if (address.isSiteLocalAddress() || address.isLoopbackAddress() || address.isAnyLocalAddress()) + if (address.isLoopbackAddress() || address.isAnyLocalAddress()) return true; - try { - // Is address bound to one of our interfaces? - NetworkInterface netIf = NetworkInterface.getByInetAddress(address); - return netIf != null; - } catch (Exception e) { - // Couldn't check - play safe and say it's local - return true; - } + return Settings.getInstance().isLocalAddress(address); } } diff --git a/Qora/src/network/Peer.java b/Qora/src/network/Peer.java index c30904ad..78adbadc 100644 --- a/Qora/src/network/Peer.java +++ b/Qora/src/network/Peer.java @@ -293,7 +293,7 @@ public void run() { if (socket == null || socket.isClosed()) { LOGGER.debug(Lang.getInstance().translate("Socket already closed") + " " + address); } else { - LOGGER.info(Lang.getInstance().translate("Socket issue with peer") + " " + address + e.getMessage()); + LOGGER.info(Lang.getInstance().translate("Socket issue with peer") + " " + address + ": " + e.getMessage()); } // Disconnect peer @@ -346,7 +346,7 @@ public boolean sendMessage(Message message) { // RETURN return true; } catch (Exception e) { - LOGGER.debug(e.getMessage(), e); + LOGGER.trace(e.getMessage(), e); // ERROR this.callback.onError(this, e.getMessage()); diff --git a/Qora/src/network/Pinger.java b/Qora/src/network/Pinger.java index d3be2906..3cc82b4f 100644 --- a/Qora/src/network/Pinger.java +++ b/Qora/src/network/Pinger.java @@ -127,16 +127,7 @@ public void run() { * @see Peer#close() */ public void stopPing() { - if (this.isAlive()) { + if (this.isAlive()) this.interrupt(); - - try { - this.join(); - } catch (InterruptedException e) { - // We've probably reached here from run() above calling - // Peer.onPingFailure() so when we return run() above will - // terminate - } - } } } diff --git a/Qora/src/qora/BlockBuffer.java b/Qora/src/qora/BlockBuffer.java index 750b0006..53cba1d6 100644 --- a/Qora/src/qora/BlockBuffer.java +++ b/Qora/src/qora/BlockBuffer.java @@ -91,11 +91,15 @@ public void run() } //CHECK BLOCK SIGNATURE + // Don't check block signature as blocks may arrive out-of-order + // and signature might depend on processing of previous blocks + /* if(!response.getBlock().isSignatureValid()) { error = true; return; } + */ //ADD TO LIST blockingQueue.add(response.getBlock()); diff --git a/Qora/src/qora/BlockGenerator.java b/Qora/src/qora/BlockGenerator.java index 540bc867..889a862a 100644 --- a/Qora/src/qora/BlockGenerator.java +++ b/Qora/src/qora/BlockGenerator.java @@ -183,6 +183,7 @@ public void run() if(!Controller.getInstance().isUpToDate() && !Controller.getInstance().isProcessingWalletSynchronize()) { Controller.getInstance().update(); + continue; } //CHECK IF WE HAVE CONNECTIONS diff --git a/Qora/src/qora/Synchronizer.java b/Qora/src/qora/Synchronizer.java index 5f4501cc..815b5fc2 100644 --- a/Qora/src/qora/Synchronizer.java +++ b/Qora/src/qora/Synchronizer.java @@ -266,8 +266,7 @@ private void orphanBackToCommonBlock(DBSet dbOrFork, Block lastCommonBlock, List lastBlock = dbOrFork.getBlockMap().getLastBlock(); } - // XXX: Why 11? - while (lastBlock.getHeight() >= height && lastBlock.getHeight() > 11) { + while (lastBlock.getHeight() >= height && lastBlock.getHeight() > 1) { // Optionally save orphaned transactions if caller has provided // storage if (orphanedTransactions != null) diff --git a/Qora/src/qora/block/Block.java b/Qora/src/qora/block/Block.java index 953934d5..3c7133c2 100644 --- a/Qora/src/qora/block/Block.java +++ b/Qora/src/qora/block/Block.java @@ -580,6 +580,10 @@ public boolean isValid(DBSet db) return false; } + // Check parent doesn't already have a child + if (this.getParent(db).getChild(db) != null) + return false; + //CHECK IF TIMESTAMP IS VALID -500 MS ERROR MARGIN TIME if(this.timestamp - 500 > NTP.getTime() || this.timestamp < this.getParent(db).timestamp) { diff --git a/Qora/src/settings/Settings.java b/Qora/src/settings/Settings.java index e74bb2b6..4ed20239 100644 --- a/Qora/src/settings/Settings.java +++ b/Qora/src/settings/Settings.java @@ -8,6 +8,7 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.URL; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -121,7 +122,6 @@ public static void FreeInstance() private Settings() { - this.localAddress = this.getCurrentIp(); int alreadyPassed = 0; File file = new File(""); @@ -211,6 +211,9 @@ private Settings() LOGGER.error(e.getMessage(),e); this.peersJSON = new JSONObject(); } + + this.localAddress = this.getCurrentIp(); + LOGGER.debug("Our IP: " + this.localAddress); } public JSONObject Dump() @@ -785,7 +788,14 @@ public boolean isLocalAddress(InetAddress address) { } public InetAddress getCurrentIp() { - try { + if (this.settingsJSON.containsKey("localAddress")) + try { + return InetAddress.getByName((String) this.settingsJSON.get("localAddress")); + } catch (UnknownHostException e) { + // fall through to other methods + } + + try { Enumeration networkInterfaces = NetworkInterface .getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()) { diff --git a/Qora/src/test/CryptoTests.java b/Qora/src/test/CryptoTests.java new file mode 100644 index 00000000..0b51f18c --- /dev/null +++ b/Qora/src/test/CryptoTests.java @@ -0,0 +1,36 @@ +package test; + +import java.io.IOException; + +import org.junit.Test; + +import qora.crypto.RIPEMD160; +import utils.Converter; + +public class CryptoTests { + + public static void main(String args[]) throws IOException { + final int nBytes = System.in.available(); + byte[] input = new byte[nBytes]; + System.in.read(input); + + RIPEMD160 ripEmd160 = new RIPEMD160(); + byte[] output = ripEmd160.digest(input); + + System.out.println(Converter.toHex(output)); + } + + @Test + public void testRIPEMD160() throws Exception { + // byte[] input = Converter.parseHexString("0000000000000000000000000000000000000000000000000000000000000000"); + // byte[] input = Converter.parseHexString("4186612a675689c1e20c8078da482576e9e99a3cc0602f88882d5e641eb38785"); + // byte[] input = "The quick brown fox jumps over the lazy cog".getBytes(); + byte[] input = Converter.parseHexString("ff"); + + RIPEMD160 ripEmd160 = new RIPEMD160(); + byte[] output = ripEmd160.digest(input); + + System.out.println(Converter.toHex(output)); + } + +} From f74e4b2677defbf180256ceaf3a3e0dbfd2187df Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 26 Sep 2018 09:35:37 +0100 Subject: [PATCH 2/2] Improve shutdown sequence so multiple shutdown threads don't cause early exit. For example, sending a signal to the JVM process[*] triggers JVM shutdown hook, which in turn calls Controller.stopAll(). Controller.stopAll calls ClosingDialog.getInstance() which also calls Controller.stopAll(). Previously the 2nd call would return quickly causing System.exit() to be called while the 1st call is still closing the database. Now, with the synchronized() block, all subsequent calls wait until the 1st is done and then all exit quickly. [*] Signal could be sent during O/S shutdown which is harder to notice --- Qora/src/api/QoraResource.java | 8 +--- Qora/src/controller/Controller.java | 72 ++++++++++++++++------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Qora/src/api/QoraResource.java b/Qora/src/api/QoraResource.java index aa85066b..e9676f80 100644 --- a/Qora/src/api/QoraResource.java +++ b/Qora/src/api/QoraResource.java @@ -35,12 +35,8 @@ public String stop() } //STOP - if (Gui.isGuiStarted()) - ClosingDialog.getInstance(); - else { - Controller.getInstance().stopAll(); - System.exit(0); - } + Controller.getInstance().stopAll(); + System.exit(0); //RETURN return String.valueOf(true); diff --git a/Qora/src/controller/Controller.java b/Qora/src/controller/Controller.java index 7b9bc71b..69acf034 100644 --- a/Qora/src/controller/Controller.java +++ b/Qora/src/controller/Controller.java @@ -581,44 +581,50 @@ public void deleteWalletObserver(Observer o) { } private boolean isStopping = false; + private Object stoppingLock = new Object(); public void stopAll() { - // PREVENT MULTIPLE CALLS - if (!this.isStopping) { - this.isStopping = true; - - // STOP SENDING OUR HEIGHT TO PEERS - this.timerPeerHeightUpdate.cancel(); - - // STOP BLOCK PROCESSOR - LOGGER.info(Lang.getInstance().translate("Stopping block processor")); - ClosingDialog.getInstance().updateProgress("Stopping block processor"); - this.synchronizer.stop(); - - // STOP BLOCK GENERATOR - LOGGER.info(Lang.getInstance().translate("Stopping block generator")); - ClosingDialog.getInstance().updateProgress("Stopping block generator"); - this.blockGenerator.shutdown(); - - // STOP MESSAGE PROCESSOR - LOGGER.info(Lang.getInstance().translate("Stopping message processor")); - ClosingDialog.getInstance().updateProgress("Stopping message processor"); - this.network.stop(); - - // CLOSE DATABASE - LOGGER.info(Lang.getInstance().translate("Closing database")); - ClosingDialog.getInstance().updateProgress("Closing database"); - DBSet.getInstance().close(); + // Prevent multiple calls. + // This method can be called via JVM shutdown hook (e.g. signal), API 'stop' or GUI 'exit', among others. + // In particular, ClosingDialog.getInstance() below can trigger a call to stopAll(). + // We want all successive calls to block until the first call has finished. + synchronized (this.stoppingLock) { + if (!this.isStopping) { + this.isStopping = true; + + // STOP SENDING OUR HEIGHT TO PEERS + this.timerPeerHeightUpdate.cancel(); + + // STOP BLOCK PROCESSOR + LOGGER.info(Lang.getInstance().translate("Stopping block processor")); + ClosingDialog.getInstance().updateProgress("Stopping block processor"); + this.synchronizer.stop(); + + // STOP BLOCK GENERATOR + LOGGER.info(Lang.getInstance().translate("Stopping block generator")); + ClosingDialog.getInstance().updateProgress("Stopping block generator"); + this.blockGenerator.shutdown(); + + // STOP MESSAGE PROCESSOR + LOGGER.info(Lang.getInstance().translate("Stopping message processor")); + ClosingDialog.getInstance().updateProgress("Stopping message processor"); + this.network.stop(); + + // CLOSE DATABASE + LOGGER.info(Lang.getInstance().translate("Closing database")); + ClosingDialog.getInstance().updateProgress("Closing database"); + DBSet.getInstance().close(); - // CLOSE WALLET - LOGGER.info(Lang.getInstance().translate("Closing wallet")); - ClosingDialog.getInstance().updateProgress("Closing wallet"); - this.wallet.close(); + // CLOSE WALLET + LOGGER.info(Lang.getInstance().translate("Closing wallet")); + ClosingDialog.getInstance().updateProgress("Closing wallet"); + this.wallet.close(); - ClosingDialog.getInstance().updateProgress("Creating database backup"); - createDataCheckpoint(); + ClosingDialog.getInstance().updateProgress("Creating database backup"); + createDataCheckpoint(); - LOGGER.info(Lang.getInstance().translate("Closed.")); + LOGGER.info(Lang.getInstance().translate("Closed.")); + } } }