diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 616fd611..154f7e25 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -1,6 +1,8 @@ package org.qortal.controller; import java.math.BigInteger; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -20,6 +22,7 @@ import org.qortal.data.account.MintingAccountData; import org.qortal.data.account.RewardShareData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; +import org.qortal.data.block.CommonBlockData; import org.qortal.data.transaction.TransactionData; import org.qortal.network.Network; import org.qortal.network.Peer; @@ -75,6 +78,10 @@ public class BlockMinter extends Thread { BlockRepository blockRepository = repository.getBlockRepository(); BlockData previousBlockData = null; + // Vars to keep track of blocks that were skipped due to chain weight + byte[] parentSignatureForLastLowWeightBlock = null; + Long timeOfLastLowWeightBlock = null; + List newBlocks = new ArrayList<>(); // Flags for tracking change in whether minting is possible, @@ -188,6 +195,9 @@ public class BlockMinter extends Thread { // Reduce log timeout logTimeout = 10 * 1000L; + + // Last low weight block is no longer valid + parentSignatureForLastLowWeightBlock = null; } // Discard accounts we have already built blocks with @@ -204,6 +214,14 @@ public class BlockMinter extends Thread { 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()) { @@ -295,6 +313,41 @@ public class BlockMinter extends Thread { } } + 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..."); + } + + // Clear variables that track low weight blocks + parentSignatureForLastLowWeightBlock = null; + timeOfLastLowWeightBlock = null; + + // Add unconfirmed transactions addUnconfirmedTransactions(repository, newBlock); @@ -462,6 +515,61 @@ public class BlockMinter extends Thread { } } + private BigInteger getOurChainWeightSinceBlock(Repository repository, BlockSummaryData commonBlock, List peerBlockSummaries) throws DataException { + final int commonBlockHeight = commonBlock.getHeight(); + final byte[] commonBlockSig = commonBlock.getSignature(); + int mutualHeight = commonBlockHeight; + + // Fetch our corresponding block summaries + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + List ourBlockSummaries = repository.getBlockRepository() + .getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight()); + if (!ourBlockSummaries.isEmpty()) { + Synchronizer.getInstance().populateBlockSummariesMinterLevels(repository, ourBlockSummaries); + } + + if (ourBlockSummaries != null && peerBlockSummaries != null) { + mutualHeight += Math.min(ourBlockSummaries.size(), peerBlockSummaries.size()); + } + return Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight); + } + + private boolean higherWeightChainExists(Repository repository, BigInteger blockCandidateWeight) throws DataException { + if (blockCandidateWeight == null) { + // Can't make decisions without knowing the block candidate weight + return false; + } + NumberFormat formatter = new DecimalFormat("0.###E0"); + + List peers = Network.getInstance().getHandshakedPeers(); + // Loop through handshaked peers and check for any new block candidates + for (Peer peer : peers) { + if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) { + // This peer has common block data + CommonBlockData commonBlockData = peer.getCommonBlockData(); + BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary(); + if (commonBlockData.getChainWeight() != null) { + // The synchronizer has calculated this peer's chain weight + BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock()); + BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight); + BigInteger peerChainWeight = commonBlockData.getChainWeight(); + if (peerChainWeight.compareTo(ourChainWeight) >= 0) { + // This peer has a higher weight chain than ours + LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight)); + return true; + + } else { + LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight)); + } + } else { + LOGGER.debug("Peer {} has no chain weight", peer); + } + } else { + LOGGER.debug("Peer {} has no common block data", peer); + } + } + return false; + } private static void moderatedLog(Runnable logFunction) { // We only log if logging at TRACE or previous log timeout has expired if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis()) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 1d604edd..b98c5fa2 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -5,6 +5,7 @@ import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -871,9 +872,9 @@ public class Synchronizer extends Thread { // Make sure we're the only thread modifying the blockchain // If we're already synchronizing with another peer then this will also return fast ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); - if (!blockchainLock.tryLock()) { + if (!blockchainLock.tryLock(3, TimeUnit.SECONDS)) { // Wasn't peer's fault we couldn't sync - LOGGER.debug("Synchronizer couldn't acquire blockchain lock"); + LOGGER.info("Synchronizer couldn't acquire blockchain lock"); return SynchronizationResult.NO_BLOCKCHAIN_LOCK; } @@ -1525,7 +1526,7 @@ public class Synchronizer extends Thread { return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates()); } - private void populateBlockSummariesMinterLevels(Repository repository, List blockSummaries) throws DataException { + public void populateBlockSummariesMinterLevels(Repository repository, List blockSummaries) throws DataException { final int firstBlockHeight = blockSummaries.get(0).getHeight(); for (int i = 0; i < blockSummaries.size(); ++i) { diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileRequestThread.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileRequestThread.java index 7e510407..5f491411 100644 --- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileRequestThread.java +++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileRequestThread.java @@ -42,9 +42,11 @@ public class ArbitraryDataFileRequestThread implements Runnable { } private void processFileHashes(Long now) { - ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance(); + if (Controller.isStopping()) { + return; + } - ArbitraryTransactionData arbitraryTransactionData = null; + ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance(); String signature58 = null; String hash58 = null; Peer peer = null; @@ -97,7 +99,7 @@ public class ArbitraryDataFileRequestThread implements Runnable { // Fetch the transaction data try (final Repository repository = RepositoryManager.getRepository()) { - arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature); + ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature); if (arbitraryTransactionData == null) { return; }