From 0cf32f6c5e3a3e1c79d77747600ad5fbd94ceba2 Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Mon, 16 May 2022 20:09:47 +0100
Subject: [PATCH] BlockMinter now only acquires repository instance as needed
 to prevent long HSQLDB rollbacks

---
 .../org/qortal/controller/BlockMinter.java    | 580 +++++++++---------
 1 file changed, 293 insertions(+), 287 deletions(-)

diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 76b57c44..07a31ad7 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -65,9 +65,8 @@ public class BlockMinter extends Thread {
 			// Lite nodes do not mint
 			return;
 		}
-
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
+		if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
+			try (final Repository repository = RepositoryManager.getRepository()) {
 				// Wipe existing unconfirmed transactions
 				List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
 
@@ -77,30 +76,31 @@ public class BlockMinter extends Thread {
 				}
 
 				repository.saveChanges();
+			} catch (DataException e) {
+				LOGGER.warn("Repository issue trying to wipe unconfirmed transactions on start-up: {}", e.getMessage());
+				// Fall-through to normal behaviour in case we can recover
 			}
+		}
 
-			// Going to need this a lot...
-			BlockRepository blockRepository = repository.getBlockRepository();
-			BlockData previousBlockData = null;
+		BlockData previousBlockData = null;
 
-			// Vars to keep track of blocks that were skipped due to chain weight
-			byte[] parentSignatureForLastLowWeightBlock = null;
-			Long timeOfLastLowWeightBlock = null;
+		// Vars to keep track of blocks that were skipped due to chain weight
+		byte[] parentSignatureForLastLowWeightBlock = null;
+		Long timeOfLastLowWeightBlock = null;
 
-			List<Block> newBlocks = new ArrayList<>();
+		List<Block> newBlocks = new ArrayList<>();
 
-			// Flags for tracking change in whether minting is possible,
-			// so we can notify Controller, and further update SysTray, etc.
-			boolean isMintingPossible = false;
-			boolean wasMintingPossible = isMintingPossible;
-			while (running) {
-				repository.discardChanges(); // Free repository locks, if any
+		// Flags for tracking change in whether minting is possible,
+		// so we can notify Controller, and further update SysTray, etc.
+		boolean isMintingPossible = false;
+		boolean wasMintingPossible = isMintingPossible;
+		while (running) {
+			if (isMintingPossible != wasMintingPossible)
+				Controller.getInstance().onMintingPossibleChange(isMintingPossible);
 
-				if (isMintingPossible != wasMintingPossible)
-					Controller.getInstance().onMintingPossibleChange(isMintingPossible);
-
-				wasMintingPossible = isMintingPossible;
+			wasMintingPossible = isMintingPossible;
 
+			try {
 				// Sleep for a while
 				Thread.sleep(1000);
 
@@ -118,315 +118,321 @@ public class BlockMinter extends Thread {
 				if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
 					continue;
 
-				List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
-				// No minting accounts?
-				if (mintingAccountsData.isEmpty())
-					continue;
+				try (final Repository repository = RepositoryManager.getRepository()) {
+					// Going to need this a lot...
+					BlockRepository blockRepository = repository.getBlockRepository();
 
-				// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
-				// Note that minting accounts are actually reward-shares in Qortal
-				Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
-				while (madi.hasNext()) {
-					MintingAccountData mintingAccountData = madi.next();
-
-					RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
-					if (rewardShareData == null) {
-						// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
-						madi.remove();
-						continue;
-					}
-
-					Account mintingAccount = new Account(repository, rewardShareData.getMinter());
-					if (!mintingAccount.canMint()) {
-						// Minting-account component of reward-share can no longer mint - disregard
-						madi.remove();
-						continue;
-					}
-
-					// Optional (non-validated) prevention of block submissions below a defined level.
-					// This is an unvalidated version of Blockchain.minAccountLevelToMint
-					// and exists only to reduce block candidates by default.
-					int level = mintingAccount.getEffectiveMintingLevel();
-					if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
-						madi.remove();
-						continue;
-					}
-				}
-
-				// Needs a mutable copy of the unmodifiableList
-				List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
-				BlockData lastBlockData = blockRepository.getLastBlock();
-
-				// Disregard peers that have "misbehaved" recently
-				peers.removeIf(Controller.hasMisbehaved);
-
-				// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
-				// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
-				if (Synchronizer.getInstance().getRecoveryMode() == false)
-					peers.removeIf(Controller.hasNoRecentBlock);
-
-				// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
-				if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
-					continue;
-
-				// If we are stuck on an invalid block, we should allow an alternative to be minted
-				boolean recoverInvalidBlock = false;
-				if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
-					// We've had at least one invalid block
-					long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
-					long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
-					if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
-						if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
-							// Last valid block was more than 10 mins ago, but we've had an invalid block since then
-							// Assume that the chain has stalled because there is no alternative valid candidate
-							// Enter recovery mode to allow alternative, valid candidates to be minted
-							recoverInvalidBlock = true;
-						}
-					}
-				}
-
-				// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
-				if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
-					if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
+					List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
+					// No minting accounts?
+					if (mintingAccountsData.isEmpty())
 						continue;
 
-				// There are enough peers with a recent block and our latest block is recent
-				// so go ahead and mint a block if possible.
-				isMintingPossible = true;
+					// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
+					// Note that minting accounts are actually reward-shares in Qortal
+					Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
+					while (madi.hasNext()) {
+						MintingAccountData mintingAccountData = madi.next();
 
-				// Check blockchain hasn't changed
-				if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
-					previousBlockData = lastBlockData;
-					newBlocks.clear();
-
-					// Reduce log timeout
-					logTimeout = 10 * 1000L;
-
-					// Last low weight block is no longer valid
-					parentSignatureForLastLowWeightBlock = null;
-				}
-
-				// Discard accounts we have already built blocks with
-				mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
-
-				// Do we need to build any potential new blocks?
-				List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
-
-				// We might need to sit the next block out, if one of our minting accounts signed the previous one
-				final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
-				final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
-				if (mintedLastBlock) {
-					LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
-					continue;
-				}
-
-				if (parentSignatureForLastLowWeightBlock != null) {
-					// The last iteration found a higher weight block in the network, so sleep for a while
-					// to allow is to sync the higher weight chain. We are sleeping here rather than when
-					// detected as we don't want to hold the blockchain lock open.
-					LOGGER.debug("Sleeping for 10 seconds...");
-					Thread.sleep(10 * 1000L);
-				}
-
-				for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
-					// First block does the AT heavy-lifting
-					if (newBlocks.isEmpty()) {
-						Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
-						if (newBlock == null) {
-							// For some reason we can't mint right now
-							moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
+						RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
+						if (rewardShareData == null) {
+							// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
+							madi.remove();
 							continue;
 						}
 
-						newBlocks.add(newBlock);
-					} else {
-						// The blocks for other minters require less effort...
-						Block newBlock = newBlocks.get(0).remint(mintingAccount);
-						if (newBlock == null) {
-							// For some reason we can't mint right now
-							moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
+						Account mintingAccount = new Account(repository, rewardShareData.getMinter());
+						if (!mintingAccount.canMint()) {
+							// Minting-account component of reward-share can no longer mint - disregard
+							madi.remove();
 							continue;
 						}
 
-						newBlocks.add(newBlock);
+						// Optional (non-validated) prevention of block submissions below a defined level.
+						// This is an unvalidated version of Blockchain.minAccountLevelToMint
+						// and exists only to reduce block candidates by default.
+						int level = mintingAccount.getEffectiveMintingLevel();
+						if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
+							madi.remove();
+							continue;
+						}
 					}
-				}
 
-				// No potential block candidates?
-				if (newBlocks.isEmpty())
-					continue;
+					// Needs a mutable copy of the unmodifiableList
+					List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
+					BlockData lastBlockData = blockRepository.getLastBlock();
 
-				// Make sure we're the only thread modifying the blockchain
-				ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
-				if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
-					LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
-					continue;
-				}
+					// Disregard peers that have "misbehaved" recently
+					peers.removeIf(Controller.hasMisbehaved);
 
-				boolean newBlockMinted = false;
-				Block newBlock = null;
+					// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
+					// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
+					if (Synchronizer.getInstance().getRecoveryMode() == false)
+						peers.removeIf(Controller.hasNoRecentBlock);
 
-				try {
-					// Clear repository session state so we have latest view of data
-					repository.discardChanges();
-
-					// Now that we have blockchain lock, do final check that chain hasn't changed
-					BlockData latestBlockData = blockRepository.getLastBlock();
-					if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
+					// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
+					if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
 						continue;
 
-					List<Block> goodBlocks = new ArrayList<>();
-					for (Block testBlock : newBlocks) {
-						// Is new block's timestamp valid yet?
-						// We do a separate check as some timestamp checks are skipped for testchains
-						if (testBlock.isTimestampValid() != ValidationResult.OK)
-							continue;
-
-						testBlock.preProcess();
-
-						// Is new block valid yet? (Before adding unconfirmed transactions)
-						ValidationResult result = testBlock.isValid();
-						if (result != ValidationResult.OK) {
-							moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
-
-							continue;
-						}
-
-						goodBlocks.add(testBlock);
-					}
-
-					if (goodBlocks.isEmpty())
-						continue;
-
-					// Pick best block
-					final int parentHeight = previousBlockData.getHeight();
-					final byte[] parentBlockSignature = previousBlockData.getSignature();
-
-					BigInteger bestWeight = null;
-
-					for (int bi = 0; bi < goodBlocks.size(); ++bi) {
-						BlockData blockData = goodBlocks.get(bi).getBlockData();
-
-						BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
-						int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
-						blockSummaryData.setMinterLevel(minterLevel);
-
-						BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
-
-						if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
-							newBlock = goodBlocks.get(bi);
-							bestWeight = blockWeight;
-						}
-					}
-
-					try {
-						if (this.higherWeightChainExists(repository, bestWeight)) {
-
-							// Check if the base block has updated since the last time we were here
-							if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
-									!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
-								// We've switched to a different chain, so reset the timer
-								timeOfLastLowWeightBlock = NTP.getTime();
+					// If we are stuck on an invalid block, we should allow an alternative to be minted
+					boolean recoverInvalidBlock = false;
+					if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
+						// We've had at least one invalid block
+						long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
+						long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
+						if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
+							if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
+								// Last valid block was more than 10 mins ago, but we've had an invalid block since then
+								// Assume that the chain has stalled because there is no alternative valid candidate
+								// Enter recovery mode to allow alternative, valid candidates to be minted
+								recoverInvalidBlock = true;
 							}
-							parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
+						}
+					}
 
-							// If less than 30 seconds has passed since first detection the higher weight chain,
-							// we should skip our block submission to give us the opportunity to sync to the better chain
-							if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
-								LOGGER.debug("Higher weight chain found in peers, so not signing a block this round");
-								LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
+					// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
+					if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
+						if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
+							continue;
+
+					// There are enough peers with a recent block and our latest block is recent
+					// so go ahead and mint a block if possible.
+					isMintingPossible = true;
+
+					// Reattach newBlocks to new repository handle
+					for (Block newBlock : newBlocks)
+						newBlock.setRepository(repository);
+
+					// Check blockchain hasn't changed
+					if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
+						previousBlockData = lastBlockData;
+						newBlocks.clear();
+
+						// Reduce log timeout
+						logTimeout = 10 * 1000L;
+
+						// Last low weight block is no longer valid
+						parentSignatureForLastLowWeightBlock = null;
+					}
+
+					// Discard accounts we have already built blocks with
+					mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
+
+					// Do we need to build any potential new blocks?
+					List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
+
+					// We might need to sit the next block out, if one of our minting accounts signed the previous one
+					final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
+					final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
+					if (mintedLastBlock) {
+						LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
+						continue;
+					}
+
+					if (parentSignatureForLastLowWeightBlock != null) {
+						// The last iteration found a higher weight block in the network, so sleep for a while
+						// to allow is to sync the higher weight chain. We are sleeping here rather than when
+						// detected as we don't want to hold the blockchain lock open.
+						LOGGER.info("Sleeping for 10 seconds...");
+						Thread.sleep(10 * 1000L);
+					}
+
+					for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
+						// First block does the AT heavy-lifting
+						if (newBlocks.isEmpty()) {
+							Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
+							if (newBlock == null) {
+								// For some reason we can't mint right now
+								moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
 								continue;
 							}
-							else {
-								// More than 30 seconds have passed, so we should submit our block candidate anyway.
-								LOGGER.debug("More than 30 seconds passed, so proceeding to submit block candidate...");
+
+							newBlocks.add(newBlock);
+						} else {
+							// The blocks for other minters require less effort...
+							Block newBlock = newBlocks.get(0).remint(mintingAccount);
+							if (newBlock == null) {
+								// For some reason we can't mint right now
+								moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
+								continue;
 							}
+
+							newBlocks.add(newBlock);
 						}
-						else {
-							LOGGER.debug("No higher weight chain found in peers");
-						}
-					} catch (DataException e) {
-						LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
 					}
 
-					// Discard any uncommitted changes as a result of the higher weight chain detection
-					repository.discardChanges();
+					// No potential block candidates?
+					if (newBlocks.isEmpty())
+						continue;
 
-					// Clear variables that track low weight blocks
-					parentSignatureForLastLowWeightBlock = null;
-					timeOfLastLowWeightBlock = null;
-
-
-					// Add unconfirmed transactions
-					addUnconfirmedTransactions(repository, newBlock);
-
-					// Sign to create block's signature
-					newBlock.sign();
-
-					// Is newBlock still valid?
-					ValidationResult validationResult = newBlock.isValid();
-					if (validationResult != ValidationResult.OK) {
-						// No longer valid? Report and discard
-						LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
-
-						// Rebuild block candidates, just to be sure
-						newBlocks.clear();
+					// Make sure we're the only thread modifying the blockchain
+					ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+					if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
+						LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
 						continue;
 					}
 
-					// Add to blockchain - something else will notice and broadcast new block to network
+					boolean newBlockMinted = false;
+					Block newBlock = null;
+
 					try {
-						newBlock.process();
+						// Clear repository session state so we have latest view of data
+						repository.discardChanges();
 
-						repository.saveChanges();
+						// Now that we have blockchain lock, do final check that chain hasn't changed
+						BlockData latestBlockData = blockRepository.getLastBlock();
+						if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
+							continue;
 
-						LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
+						List<Block> goodBlocks = new ArrayList<>();
+						for (Block testBlock : newBlocks) {
+							// Is new block's timestamp valid yet?
+							// We do a separate check as some timestamp checks are skipped for testchains
+							if (testBlock.isTimestampValid() != ValidationResult.OK)
+								continue;
 
-						RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
+							testBlock.preProcess();
 
-						if (rewardShareData != null) {
-							LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
-									newBlock.getBlockData().getHeight(),
-									Base58.encode(newBlock.getBlockData().getSignature()),
-									Base58.encode(newBlock.getParent().getSignature()),
-									rewardShareData.getMinter(),
-									rewardShareData.getRecipient()));
-						} else {
-							LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
-									newBlock.getBlockData().getHeight(),
-									Base58.encode(newBlock.getBlockData().getSignature()),
-									Base58.encode(newBlock.getParent().getSignature()),
-									newBlock.getMinter().getAddress()));
+							// Is new block valid yet? (Before adding unconfirmed transactions)
+							ValidationResult result = testBlock.isValid();
+							if (result != ValidationResult.OK) {
+								moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
+
+								continue;
+							}
+
+							goodBlocks.add(testBlock);
 						}
 
-						// Notify network after we're released blockchain lock
-						newBlockMinted = true;
+						if (goodBlocks.isEmpty())
+							continue;
 
-						// Notify Controller
-						repository.discardChanges(); // clear transaction status to prevent deadlocks
-						Controller.getInstance().onNewBlock(newBlock.getBlockData());
-					} catch (DataException e) {
-						// Unable to process block - report and discard
-						LOGGER.error("Unable to process newly minted block?", e);
-						newBlocks.clear();
+						// Pick best block
+						final int parentHeight = previousBlockData.getHeight();
+						final byte[] parentBlockSignature = previousBlockData.getSignature();
+
+						BigInteger bestWeight = null;
+
+						for (int bi = 0; bi < goodBlocks.size(); ++bi) {
+							BlockData blockData = goodBlocks.get(bi).getBlockData();
+
+							BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
+							int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
+							blockSummaryData.setMinterLevel(minterLevel);
+
+							BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
+
+							if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
+								newBlock = goodBlocks.get(bi);
+								bestWeight = blockWeight;
+							}
+						}
+
+						try {
+							if (this.higherWeightChainExists(repository, bestWeight)) {
+
+								// Check if the base block has updated since the last time we were here
+								if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
+										!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
+									// We've switched to a different chain, so reset the timer
+									timeOfLastLowWeightBlock = NTP.getTime();
+								}
+								parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
+
+								// If less than 30 seconds has passed since first detection the higher weight chain,
+								// we should skip our block submission to give us the opportunity to sync to the better chain
+								if (NTP.getTime() - timeOfLastLowWeightBlock < 30 * 1000L) {
+									LOGGER.info("Higher weight chain found in peers, so not signing a block this round");
+									LOGGER.info("Time since detected: {}", NTP.getTime() - timeOfLastLowWeightBlock);
+									continue;
+								} else {
+									// More than 30 seconds have passed, so we should submit our block candidate anyway.
+									LOGGER.info("More than 30 seconds passed, so proceeding to submit block candidate...");
+								}
+							} else {
+								LOGGER.debug("No higher weight chain found in peers");
+							}
+						} catch (DataException e) {
+							LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
+						}
+
+						// Discard any uncommitted changes as a result of the higher weight chain detection
+						repository.discardChanges();
+
+						// Clear variables that track low weight blocks
+						parentSignatureForLastLowWeightBlock = null;
+						timeOfLastLowWeightBlock = null;
+
+						// Add unconfirmed transactions
+						addUnconfirmedTransactions(repository, newBlock);
+
+						// Sign to create block's signature
+						newBlock.sign();
+
+						// Is newBlock still valid?
+						ValidationResult validationResult = newBlock.isValid();
+						if (validationResult != ValidationResult.OK) {
+							// No longer valid? Report and discard
+							LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
+
+							// Rebuild block candidates, just to be sure
+							newBlocks.clear();
+							continue;
+						}
+
+						// Add to blockchain - something else will notice and broadcast new block to network
+						try {
+							newBlock.process();
+
+							repository.saveChanges();
+
+							LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
+
+							RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
+
+							if (rewardShareData != null) {
+								LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
+										newBlock.getBlockData().getHeight(),
+										Base58.encode(newBlock.getBlockData().getSignature()),
+										Base58.encode(newBlock.getParent().getSignature()),
+										rewardShareData.getMinter(),
+										rewardShareData.getRecipient()));
+							} else {
+								LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
+										newBlock.getBlockData().getHeight(),
+										Base58.encode(newBlock.getBlockData().getSignature()),
+										Base58.encode(newBlock.getParent().getSignature()),
+										newBlock.getMinter().getAddress()));
+							}
+
+							// Notify network after we're released blockchain lock
+							newBlockMinted = true;
+
+							// Notify Controller
+							repository.discardChanges(); // clear transaction status to prevent deadlocks
+							Controller.getInstance().onNewBlock(newBlock.getBlockData());
+						} catch (DataException e) {
+							// Unable to process block - report and discard
+							LOGGER.error("Unable to process newly minted block?", e);
+							newBlocks.clear();
+						}
+					} finally {
+						blockchainLock.unlock();
 					}
-				} finally {
-					blockchainLock.unlock();
-				}
 
-				if (newBlockMinted) {
-					// Broadcast our new chain to network
-					BlockData newBlockData = newBlock.getBlockData();
+					if (newBlockMinted) {
+						// Broadcast our new chain to network
+						BlockData newBlockData = newBlock.getBlockData();
 
-					Network network = Network.getInstance();
-					network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
+						Network network = Network.getInstance();
+						network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
+					}
+				} catch (DataException e) {
+					LOGGER.warn("Repository issue while running block minter", e);
 				}
+			} catch (InterruptedException e) {
+				// We've been interrupted - time to exit
+				return;
 			}
-		} catch (DataException e) {
-			LOGGER.warn("Repository issue while running block minter", e);
-		} catch (InterruptedException e) {
-			// We've been interrupted - time to exit
-			return;
 		}
 	}