From 270ac88b5182de0a7318fab41ecc14e41142c558 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 15:45:15 +0000 Subject: [PATCH 01/25] Added GetBlocksMessage and BlocksMessage, which allow multiple blocks to be transferred between peers in a single request. When communicating with a peer that is running at least version 1.5.0, it will now sync multiple blocks at once in Synchronizer.syncToPeerChain(). This allows us to bypass the single block syncing (and retry mechanism), which has proven to be unviable when there are multiple active forks with several blocks in each chain. For peers below v1.5.0, the logic should remain unaffected and it will continue to sync blocks individually. --- src/main/java/org/qortal/block/Block.java | 14 +- .../org/qortal/controller/Controller.java | 80 ++++++ .../org/qortal/controller/Synchronizer.java | 239 ++++++++++++------ src/main/java/org/qortal/network/Network.java | 1 + .../qortal/network/message/BlocksMessage.java | 90 +++++++ .../network/message/GetBlocksMessage.java | 65 +++++ .../org/qortal/network/message/Message.java | 5 +- .../org/qortal/transaction/Transaction.java | 4 + .../transform/block/BlockTransformer.java | 22 +- 9 files changed, 436 insertions(+), 84 deletions(-) create mode 100644 src/main/java/org/qortal/network/message/BlocksMessage.java create mode 100644 src/main/java/org/qortal/network/message/GetBlocksMessage.java diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 8551e4e7..e2560ac2 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -469,6 +469,16 @@ public class Block { return this.minter; } + + public void setRepository(Repository repository) throws DataException { + this.repository = repository; + + for (Transaction transaction : this.getTransactions()) { + transaction.setRepository(repository); + } + } + + // More information /** @@ -517,8 +527,10 @@ public class Block { long nonAtTransactionCount = transactionsData.stream().filter(transactionData -> transactionData.getType() != TransactionType.AT).count(); // The number of non-AT transactions fetched from repository should correspond with Block's transactionCount - if (nonAtTransactionCount != this.blockData.getTransactionCount()) + if (nonAtTransactionCount != this.blockData.getTransactionCount()) { + LOGGER.error(() -> String.format("Block's transactions from repository (%d) do not match block's transaction count (%d)", nonAtTransactionCount, this.blockData.getTransactionCount())); throw new IllegalStateException("Block's transactions from repository do not match block's transaction count"); + } this.transactions = new ArrayList<>(); diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index a0ca1d05..40c25d84 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -68,9 +68,11 @@ import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.network.message.ArbitraryDataMessage; import org.qortal.network.message.BlockSummariesMessage; +import org.qortal.network.message.BlocksMessage; import org.qortal.network.message.CachedBlockMessage; import org.qortal.network.message.GetArbitraryDataMessage; import org.qortal.network.message.GetBlockMessage; +import org.qortal.network.message.GetBlocksMessage; import org.qortal.network.message.GetBlockSummariesMessage; import org.qortal.network.message.GetOnlineAccountsMessage; import org.qortal.network.message.GetPeersMessage; @@ -216,6 +218,18 @@ public class Controller extends Thread { } public GetBlockMessageStats getBlockMessageStats = new GetBlockMessageStats(); + public static class GetBlocksMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong cacheHits = new AtomicLong(); + public AtomicLong unknownBlocks = new AtomicLong(); + public AtomicLong cacheFills = new AtomicLong(); + public AtomicLong fullyFromCache = new AtomicLong(); + + public GetBlocksMessageStats() { + } + } + public GetBlocksMessageStats getBlocksMessageStats = new GetBlocksMessageStats(); + public static class GetBlockSummariesStats { public AtomicLong requests = new AtomicLong(); public AtomicLong cacheHits = new AtomicLong(); @@ -1094,6 +1108,10 @@ public class Controller extends Thread { onNetworkGetBlockMessage(peer, message); break; + case GET_BLOCKS: + onNetworkGetBlocksMessage(peer, message); + break; + case TRANSACTION: onNetworkTransactionMessage(peer, message); break; @@ -1208,6 +1226,68 @@ public class Controller extends Thread { } } + private void onNetworkGetBlocksMessage(Peer peer, Message message) { + GetBlocksMessage getBlocksMessage = (GetBlocksMessage) message; + byte[] parentSignature = getBlocksMessage.getParentSignature(); + this.stats.getBlocksMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + + // If peer's parent signature matches our latest block signature + // then we can short-circuit with an empty response + BlockData chainTip = getChainTip(); + if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) { + Message blocksMessage = new BlocksMessage(Collections.emptyList()); + blocksMessage.setId(message.getId()); + if (!peer.sendMessage(blocksMessage)) + peer.disconnect("failed to send blocks"); + + return; + } + + List blockDataList = new ArrayList<>(); + + // Attempt to serve from our cache of latest blocks + synchronized (this.latestBlocks) { + blockDataList = this.latestBlocks.stream() + .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature)) + .map(BlockData::new) + .collect(Collectors.toList()); + } + + if (blockDataList.isEmpty()) { + int numberRequested = Math.min(Network.MAX_BLOCKS_PER_REPLY, getBlocksMessage.getNumberRequested()); + + BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); + + while (blockData != null && blockDataList.size() < numberRequested) { + blockDataList.add(blockData); + + blockData = repository.getBlockRepository().fromReference(blockData.getSignature()); + } + } else { + this.stats.getBlocksMessageStats.cacheHits.incrementAndGet(); + + if (blockDataList.size() >= getBlocksMessage.getNumberRequested()) + this.stats.getBlocksMessageStats.fullyFromCache.incrementAndGet(); + } + + List blocks = new ArrayList<>(); + for (BlockData blockData : blockDataList) { + Block block = new Block(repository, blockData); + blocks.add(block); + } + + Message blocksMessage = new BlocksMessage(blocks); + blocksMessage.setId(message.getId()); + if (!peer.sendMessage(blocksMessage)) + peer.disconnect("failed to send blocks"); + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while sending blocks after %s to peer %s", Base58.encode(parentSignature), peer), e); + } + } + private void onNetworkTransactionMessage(Peer peer, Message message) { TransactionMessage transactionMessage = (TransactionMessage) message; TransactionData transactionData = transactionMessage.getTransactionData(); diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 0804c2df..5b81622a 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -22,8 +22,10 @@ import org.qortal.data.transaction.RewardShareTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.network.Peer; import org.qortal.network.message.BlockMessage; +import org.qortal.network.message.BlocksMessage; import org.qortal.network.message.BlockSummariesMessage; import org.qortal.network.message.GetBlockMessage; +import org.qortal.network.message.GetBlocksMessage; import org.qortal.network.message.GetBlockSummariesMessage; import org.qortal.network.message.GetSignaturesV2Message; import org.qortal.network.message.Message; @@ -56,6 +58,9 @@ public class Synchronizer { /** Number of retry attempts if a peer fails to respond with the requested data */ private static final int MAXIMUM_RETRIES = 3; // XXX move to Settings? + /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ + private static final long PEER_VERSION_150 = 0x0100050000L; + private static Synchronizer instance; @@ -360,97 +365,161 @@ public class Synchronizer { // Overall plan: fetch peer's blocks first, then orphan, then apply - // Convert any leftover (post-common) block summaries into signatures to request from peer - List peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList()); // Calculate the total number of additional blocks this peer has beyond the common block int additionalPeerBlocksAfterCommonBlock = peerHeight - commonBlockHeight; - // Subtract the number of signatures that we already have, as we don't need to request them again - int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); - // Fetch remaining block signatures, if needed - int retryCount = 0; - while (numberSignaturesRequired > 0) { - byte[] latestPeerSignature = peerBlockSignatures.isEmpty() ? commonBlockSig : peerBlockSignatures.get(peerBlockSignatures.size() - 1); - int lastPeerHeight = commonBlockHeight + peerBlockSignatures.size(); - int numberOfSignaturesToRequest = Math.min(numberSignaturesRequired, MAXIMUM_REQUEST_SIZE); - LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s", - numberOfSignaturesToRequest, (numberOfSignaturesToRequest != 1 ? "s": ""), lastPeerHeight, Base58.encode(latestPeerSignature))); - - List moreBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberOfSignaturesToRequest); - - if (moreBlockSignatures == null || moreBlockSignatures.isEmpty()) { - LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer, - lastPeerHeight, Base58.encode(latestPeerSignature))); - - if (retryCount >= MAXIMUM_RETRIES) { - // Give up with this peer - return SynchronizationResult.NO_REPLY; - } - else { - // Retry until retryCount reaches MAXIMUM_RETRIES - retryCount++; - int triesRemaining = MAXIMUM_RETRIES - retryCount; - LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s": ""))); - continue; - } - } - - // Reset retryCount because the last request succeeded - retryCount = 0; - - LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : ""))); - - peerBlockSignatures.addAll(moreBlockSignatures); - numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); - } - - // Fetch blocks using signatures - LOGGER.debug(String.format("Fetching new blocks from peer %s after height %d", peer, commonBlockHeight)); + // Firstly, attempt to retrieve the blocks themselves, rather than signatures. This is supported by newer peers. + // We could optionally check for a version here if we didn't want to make unnecessary requests List peerBlocks = new ArrayList<>(); - retryCount = 0; - while (peerBlocks.size() < peerBlockSignatures.size()) { - byte[] blockSignature = peerBlockSignatures.get(peerBlocks.size()); + if (peer.getPeersVersion() >= PEER_VERSION_150) { + // This peer supports syncing multiple blocks at once via GetBlocksMessage + int numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); + while (numberBlocksRequired > 0) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; - LOGGER.debug(String.format("Fetching block with signature %.8s", Base58.encode(blockSignature))); - int blockHeightToRequest = commonBlockHeight + peerBlocks.size() + 1; // +1 because we are requesting the next block, beyond what we already have in the peerBlocks array - Block newBlock = this.fetchBlock(repository, peer, blockSignature); + byte[] latestPeerSignature = peerBlocks.isEmpty() ? commonBlockSig : peerBlocks.get(peerBlocks.size() - 1).getSignature(); + int lastPeerHeight = commonBlockHeight + peerBlocks.size(); + int numberOfBlocksToRequest = Math.min(numberBlocksRequired, MAXIMUM_REQUEST_SIZE); - if (newBlock == null) { - LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, blockHeightToRequest, Base58.encode(blockSignature))); + LOGGER.trace(String.format("Requesting %d block%s after height %d, sig %.8s", + numberOfBlocksToRequest, (numberOfBlocksToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); - if (retryCount >= MAXIMUM_RETRIES) { - // Give up with this peer - return SynchronizationResult.NO_REPLY; + List blocks = this.fetchBlocks(repository, peer, latestPeerSignature, numberOfBlocksToRequest); + if (blocks == null || blocks.isEmpty()) { + LOGGER.info(String.format("Peer %s failed to respond with more blocks after height %d, sig %.8s", peer, + lastPeerHeight, Base58.encode(latestPeerSignature))); + + if (peerBlocks.isEmpty()) { + return SynchronizationResult.NO_REPLY; + } } - else { - // Retry until retryCount reaches MAXIMUM_RETRIES - retryCount++; - int triesRemaining = MAXIMUM_RETRIES - retryCount; - LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s": ""))); - continue; + + LOGGER.debug(String.format("Received %d blocks from peer %s", blocks.size(), peer)); + + try { + for (Block block : blocks) { + + // Set the repository, because we couldn't do that when originally constructing the Block + block.setRepository(repository); + + // Transactions are transmitted without approval status so determine that now + for (Transaction transaction : block.getTransactions()) { + transaction.setInitialApprovalStatus(); + } + + peerBlocks.add(block); + } + } catch (IllegalStateException e) { + LOGGER.error("Error processing transactions in block", e); + return SynchronizationResult.REPOSITORY_ISSUE; } + + numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); } - - if (!newBlock.isSignatureValid()) { - LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer, - blockHeightToRequest, Base58.encode(blockSignature))); - return SynchronizationResult.INVALID_DATA; - } - - // Reset retryCount because the last request succeeded - retryCount = 0; - - LOGGER.debug(String.format("Received block with height %d, sig: %.8s", newBlock.getBlockData().getHeight(), Base58.encode(blockSignature))); - - // Transactions are transmitted without approval status so determine that now - for (Transaction transaction : newBlock.getTransactions()) - transaction.setInitialApprovalStatus(); - - peerBlocks.add(newBlock); } + else { + // Older peer version - use slow sync + + // Convert any leftover (post-common) block summaries into signatures to request from peer + List peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList()); + + // Subtract the number of signatures that we already have, as we don't need to request them again + int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); + + + // Fetch remaining block signatures, if needed + int retryCount = 0; + while (numberSignaturesRequired > 0) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + + byte[] latestPeerSignature = peerBlockSignatures.isEmpty() ? commonBlockSig : peerBlockSignatures.get(peerBlockSignatures.size() - 1); + int lastPeerHeight = commonBlockHeight + peerBlockSignatures.size(); + int numberOfSignaturesToRequest = Math.min(numberSignaturesRequired, MAXIMUM_REQUEST_SIZE); + + LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s", + numberOfSignaturesToRequest, (numberOfSignaturesToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); + + List moreBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberOfSignaturesToRequest); + + if (moreBlockSignatures == null || moreBlockSignatures.isEmpty()) { + LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer, + lastPeerHeight, Base58.encode(latestPeerSignature))); + + if (retryCount >= MAXIMUM_RETRIES) { + // Give up with this peer + return SynchronizationResult.NO_REPLY; + } else { + // Retry until retryCount reaches MAXIMUM_RETRIES + retryCount++; + int triesRemaining = MAXIMUM_RETRIES - retryCount; + LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : ""))); + continue; + } + } + + // Reset retryCount because the last request succeeded + retryCount = 0; + + LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : ""))); + + peerBlockSignatures.addAll(moreBlockSignatures); + numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); + } + + // Fetch blocks using signatures + LOGGER.debug(String.format("Fetching new blocks from peer %s after height %d", peer, commonBlockHeight)); + + retryCount = 0; + while (peerBlocks.size() < peerBlockSignatures.size()) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + + byte[] blockSignature = peerBlockSignatures.get(peerBlocks.size()); + + LOGGER.debug(String.format("Fetching block with signature %.8s", Base58.encode(blockSignature))); + int blockHeightToRequest = commonBlockHeight + peerBlocks.size() + 1; // +1 because we are requesting the next block, beyond what we already have in the peerBlocks array + Block newBlock = this.fetchBlock(repository, peer, blockSignature); + + if (newBlock == null) { + LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, blockHeightToRequest, Base58.encode(blockSignature))); + + if (retryCount >= MAXIMUM_RETRIES) { + // Give up with this peer + return SynchronizationResult.NO_REPLY; + } else { + // Retry until retryCount reaches MAXIMUM_RETRIES + retryCount++; + int triesRemaining = MAXIMUM_RETRIES - retryCount; + LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : ""))); + continue; + } + } + + if (!newBlock.isSignatureValid()) { + LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer, + blockHeightToRequest, Base58.encode(blockSignature))); + return SynchronizationResult.INVALID_DATA; + } + + // Reset retryCount because the last request succeeded + retryCount = 0; + + LOGGER.debug(String.format("Received block with height %d, sig: %.8s", newBlock.getBlockData().getHeight(), Base58.encode(blockSignature))); + + // Transactions are transmitted without approval status so determine that now + for (Transaction transaction : newBlock.getTransactions()) + transaction.setInitialApprovalStatus(); + + peerBlocks.add(newBlock); + } + + } + // Unwind to common block (unless common block is our latest block) LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58)); @@ -625,6 +694,22 @@ public class Synchronizer { return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates()); } + private List fetchBlocks(Repository repository, Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException { + Message getBlocksMessage = new GetBlocksMessage(parentSignature, numberRequested); + + Message message = peer.getResponse(getBlocksMessage); + if (message == null || message.getType() != MessageType.BLOCKS) { + return null; + } + + BlocksMessage blocksMessage = (BlocksMessage) message; + if (blocksMessage == null || blocksMessage.getBlocks() == null) { + return null; + } + + return blocksMessage.getBlocks(); + } + private void populateBlockSummariesMinterLevels(Repository repository, List blockSummaries) throws DataException { final int firstBlockHeight = blockSummaries.get(0).getHeight(); diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index 0e9ac32b..7a234c7a 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -89,6 +89,7 @@ public class Network { public static final int MAX_SIGNATURES_PER_REPLY = 500; public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500; + public static final int MAX_BLOCKS_PER_REPLY = 500; // Generate our node keys / ID private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom()); diff --git a/src/main/java/org/qortal/network/message/BlocksMessage.java b/src/main/java/org/qortal/network/message/BlocksMessage.java new file mode 100644 index 00000000..f53de301 --- /dev/null +++ b/src/main/java/org/qortal/network/message/BlocksMessage.java @@ -0,0 +1,90 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.block.Block; +import org.qortal.data.at.ATStateData; +import org.qortal.data.block.BlockData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.transform.TransformationException; +import org.qortal.transform.block.BlockTransformer; +import org.qortal.utils.Triple; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class BlocksMessage extends Message { + + private static final Logger LOGGER = LogManager.getLogger(BlocksMessage.class); + + private List blocks; + + public BlocksMessage(List blocks) { + this(-1, blocks); + } + + private BlocksMessage(int id, List blocks) { + super(id, MessageType.BLOCKS); + + this.blocks = blocks; + } + + public List getBlocks() { + return this.blocks; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + + int count = bytes.getInt(); + List blocks = new ArrayList<>(); + + for (int i = 0; i < count; ++i) { + int height = bytes.getInt(); + + try { + boolean finalBlockInBuffer = (i == count-1); + + Triple, List> blockInfo = null; + blockInfo = BlockTransformer.fromByteBuffer(bytes, finalBlockInBuffer); + BlockData blockData = blockInfo.getA(); + blockData.setHeight(height); + + // We are unable to obtain a valid Repository instance here, so set it to null and we will attach it later + Block block = new Block(null, blockData, blockInfo.getB(), blockInfo.getC()); + blocks.add(block); + + } catch (TransformationException e) { + return null; + } + + } + + return new BlocksMessage(id, blocks); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(this.blocks.size())); + + for (Block block : this.blocks) { + bytes.write(Ints.toByteArray(block.getBlockData().getHeight())); + bytes.write(BlockTransformer.toBytes(block)); + } + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } catch (TransformationException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/GetBlocksMessage.java b/src/main/java/org/qortal/network/message/GetBlocksMessage.java new file mode 100644 index 00000000..ae5a78c4 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetBlocksMessage.java @@ -0,0 +1,65 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.transform.Transformer; +import org.qortal.transform.block.BlockTransformer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetBlocksMessage extends Message { + + private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH; + + private byte[] parentSignature; + private int numberRequested; + + public GetBlocksMessage(byte[] parentSignature, int numberRequested) { + this(-1, parentSignature, numberRequested); + } + + private GetBlocksMessage(int id, byte[] parentSignature, int numberRequested) { + super(id, MessageType.GET_BLOCKS); + + this.parentSignature = parentSignature; + this.numberRequested = numberRequested; + } + + public byte[] getParentSignature() { + return this.parentSignature; + } + + public int getNumberRequested() { + return this.numberRequested; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + Transformer.INT_LENGTH) + return null; + + byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH]; + bytes.get(parentSignature); + + int numberRequested = bytes.getInt(); + + return new GetBlocksMessage(id, parentSignature, numberRequested); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(this.parentSignature); + + bytes.write(Ints.toByteArray(this.numberRequested)); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index cc90fe81..d1546dce 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -80,7 +80,10 @@ public abstract class Message { GET_ONLINE_ACCOUNTS(81), ARBITRARY_DATA(90), - GET_ARBITRARY_DATA(91); + GET_ARBITRARY_DATA(91), + + BLOCKS(100), + GET_BLOCKS(101); public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index d7dd1455..2a57649c 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -315,6 +315,10 @@ public abstract class Transaction { return this.transactionData; } + public void setRepository(Repository repository) { + this.repository = repository; + } + // More information public static long getDeadline(TransactionData transactionData) { diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java index 8b91fd11..cce3e7d7 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -74,19 +74,30 @@ public class BlockTransformer extends Transformer { } /** - * Extract block data and transaction data from serialized bytes. - * + * Extract block data and transaction data from serialized bytes containing a single block. + * * @param bytes * @return BlockData and a List of transactions. * @throws TransformationException */ public static Triple, List> fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + return BlockTransformer.fromByteBuffer(byteBuffer, true); + } + + /** + * Extract block data and transaction data from serialized bytes containing one or more blocks. + * + * @param bytes + * @return the next block's BlockData and a List of transactions. + * @throws TransformationException + */ + public static Triple, List> fromByteBuffer(ByteBuffer byteBuffer, boolean finalBlockInBuffer) throws TransformationException { int version = byteBuffer.getInt(); - if (byteBuffer.remaining() < BASE_LENGTH + AT_BYTES_LENGTH - VERSION_LENGTH) + if (finalBlockInBuffer && byteBuffer.remaining() < BASE_LENGTH + AT_BYTES_LENGTH - VERSION_LENGTH) throw new TransformationException("Byte data too short for Block"); - if (byteBuffer.remaining() > BlockChain.getInstance().getMaxBlockSize()) + if (finalBlockInBuffer && byteBuffer.remaining() > BlockChain.getInstance().getMaxBlockSize()) throw new TransformationException("Byte data too long for Block"); long timestamp = byteBuffer.getLong(); @@ -210,7 +221,8 @@ public class BlockTransformer extends Transformer { byteBuffer.get(onlineAccountsSignatures); } - if (byteBuffer.hasRemaining()) + // We should only complain about excess byte data if we aren't expecting more blocks in this ByteBuffer + if (finalBlockInBuffer && byteBuffer.hasRemaining()) throw new TransformationException("Excess byte data found after parsing Block"); // We don't have a height! From a5308995b73f931962e00c5278d712609108aa5f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 15:46:30 +0000 Subject: [PATCH 02/25] Bump version to 1.5.0, to allow nodes to start using the new syncing method. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3eda336f..6697cc81 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 1.4.5 + 1.5.0 jar true From dbcf6de2d50dac2e9bb1c7ab51c02235bb0bb7cd Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 17:59:23 +0000 Subject: [PATCH 03/25] Added new settings "fastSyncEnabled" (default: false) and "fastSyncEnabledWhenResolvingFork" (default: true). --- src/main/java/org/qortal/settings/Settings.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index fb068b8d..d15a4ab9 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -123,6 +123,11 @@ public class Settings { /** Maximum number of threads for network proof-of-work compute, used during handshaking. */ private int networkPoWComputePoolSize = 2; + /** Whether to sync multiple blocks at once in normal operation */ + private boolean fastSyncEnabled = false; + /** Whether to sync multiple blocks at once when the peer has a different chain */ + private boolean fastSyncEnabledWhenResolvingFork = true; + // Which blockchains this node is running private String blockchainConfig = null; // use default from resources private BitcoinNet bitcoinNet = BitcoinNet.MAIN; @@ -436,6 +441,14 @@ public class Settings { return this.repositoryConnectionPoolSize; } + public boolean isFastSyncEnabled() { + return this.fastSyncEnabled; + } + + public boolean isFastSyncEnabledWhenResolvingFork() { + return this.fastSyncEnabledWhenResolvingFork; + } + public boolean isAutoUpdateEnabled() { return this.autoUpdateEnabled; } From 8c3753326f7e38d0b8da9ec8788971c6fedad2f2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 18:00:39 +0000 Subject: [PATCH 04/25] Check "isFastSyncEnabledWhenResolvingFork" setting before requesting multiple blocks from a peer. This allows users to opt out of this functionality, by setting it to false in their settings. --- src/main/java/org/qortal/controller/Synchronizer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 5b81622a..0c32a777 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -34,6 +34,7 @@ import org.qortal.network.message.Message.MessageType; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; import org.qortal.transaction.Transaction; import org.qortal.utils.Base58; @@ -370,12 +371,11 @@ public class Synchronizer { int additionalPeerBlocksAfterCommonBlock = peerHeight - commonBlockHeight; - // Firstly, attempt to retrieve the blocks themselves, rather than signatures. This is supported by newer peers. - // We could optionally check for a version here if we didn't want to make unnecessary requests + // Firstly, attempt to retrieve the blocks themselves, rather than signatures. This is supported by newer peers (version 1.5.0 and above). List peerBlocks = new ArrayList<>(); - if (peer.getPeersVersion() >= PEER_VERSION_150) { - // This peer supports syncing multiple blocks at once via GetBlocksMessage + if (Settings.getInstance().isFastSyncEnabledWhenResolvingFork() && peer.getPeersVersion() >= PEER_VERSION_150) { + // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings int numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); while (numberBlocksRequired > 0) { if (Controller.isStopping()) @@ -422,7 +422,7 @@ public class Synchronizer { } } else { - // Older peer version - use slow sync + // Older peer version, or fast sync is disabled in the settings - use slow sync // Convert any leftover (post-common) block summaries into signatures to request from peer List peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList()); From 3e0ff7f43fbe5d67ad6d0f80c8d175d59c42091f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 18:04:47 +0000 Subject: [PATCH 05/25] Split Synchronizer.applyNewBlocks() into two methods. If fast syncing is enabled in the settings (by default it's disabled) AND the peer is running at least v1.5.0, then it will route through to a new method which fetches multiple blocks at a time, in a very similar way to Synchronizer.syncToPeerChain(). If fast syncing is disabled in the settings, or we are communicating with a peer on an older version, it will continue to sync blocks individually. --- .../org/qortal/controller/Synchronizer.java | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 0c32a777..2aa2fb8a 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -572,8 +572,103 @@ public class Synchronizer { } private SynchronizationResult applyNewBlocks(Repository repository, BlockData commonBlockData, int ourInitialHeight, + Peer peer, int peerHeight, List peerBlockSummaries) throws InterruptedException, DataException { + + if (Settings.getInstance().isFastSyncEnabled() && peer.getPeersVersion() >= PEER_VERSION_150) + // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings + return this.applyNewBlocksUsingFastSync(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries); + else + // Older peer version, or fast sync is disabled in the settings - use slow sync + return this.applyNewBlocksUsingSlowSync(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries); + + } + + private SynchronizationResult applyNewBlocksUsingFastSync(Repository repository, BlockData commonBlockData, int ourInitialHeight, + Peer peer, int peerHeight, List peerBlockSummaries) throws InterruptedException, DataException { + LOGGER.debug(String.format("Fetching new blocks from peer %s using fast sync", peer)); + + final int commonBlockHeight = commonBlockData.getHeight(); + final byte[] commonBlockSig = commonBlockData.getSignature(); + byte[] latestPeerSignature = commonBlockSig; + + int ourHeight = ourInitialHeight; + + // Fetch, and apply, blocks from peer + int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE; + + while (ourHeight < peerHeight && ourHeight < maxBatchHeight) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + + int numberRequested = Math.min(maxBatchHeight - ourHeight, MAXIMUM_REQUEST_SIZE); + + LOGGER.trace(String.format("Fetching %d blocks after height %d, sig %.8s from %s", numberRequested, ourHeight, Base58.encode(latestPeerSignature), peer)); + List blocks = this.fetchBlocks(repository, peer, latestPeerSignature, numberRequested); + if (blocks == null || blocks.isEmpty()) { + LOGGER.info(String.format("Peer %s failed to respond with more blocks after height %d, sig %.8s", peer, + ourHeight, Base58.encode(latestPeerSignature))); + } + LOGGER.trace(String.format("Received %d blocks after height %d, sig %.8s from %s", blocks.size(), ourHeight, Base58.encode(latestPeerSignature), peer)); + + for (Block newBlock : blocks) { + ++ourHeight; + + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + + if (newBlock == null) { + LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, + ourHeight, Base58.encode(latestPeerSignature))); + return SynchronizationResult.NO_REPLY; + } + + if (!newBlock.isSignatureValid()) { + LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer, + ourHeight, Base58.encode(latestPeerSignature))); + return SynchronizationResult.INVALID_DATA; + } + + // Set the repository, because we couldn't do that when originally constructing the Block + newBlock.setRepository(repository); + + // Transactions are transmitted without approval status so determine that now + for (Transaction transaction : newBlock.getTransactions()) { + transaction.setInitialApprovalStatus(); + } + + ValidationResult blockResult = newBlock.isValid(); + if (blockResult != ValidationResult.OK) { + LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, + ourHeight, Base58.encode(latestPeerSignature), blockResult.name())); + return SynchronizationResult.INVALID_DATA; + } + + // Save transactions attached to this block + for (Transaction transaction : newBlock.getTransactions()) { + TransactionData transactionData = transaction.getTransactionData(); + repository.getTransactionRepository().save(transactionData); + } + + newBlock.process(); + + LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature()))); + + repository.saveChanges(); + + Controller.getInstance().onNewBlock(newBlock.getBlockData()); + + // Update latestPeerSignature so that subsequent batches start requesting from the correct block + latestPeerSignature = newBlock.getSignature(); + } + + } + + return SynchronizationResult.OK; + } + + private SynchronizationResult applyNewBlocksUsingSlowSync(Repository repository, BlockData commonBlockData, int ourInitialHeight, Peer peer, int peerHeight, List peerBlockSummaries) throws InterruptedException, DataException { - LOGGER.debug(String.format("Fetching new blocks from peer %s", peer)); + LOGGER.debug(String.format("Fetching new blocks from peer %s using slow sync", peer)); final int commonBlockHeight = commonBlockData.getHeight(); final byte[] commonBlockSig = commonBlockData.getSignature(); From 365662a2afd4c913cd8a0f180bb65cd18b9f9a6c Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 27 Mar 2021 19:25:27 +0000 Subject: [PATCH 06/25] MAXIMUM_RETRIES reduced from 3 to 1. It will now only retry once, which should save around 6 seconds of wasted synchronization time if a node is unable to respond with the requested block (due to a re-org, etc). --- src/main/java/org/qortal/controller/Synchronizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 2aa2fb8a..c747dd6e 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -57,7 +57,7 @@ public class Synchronizer { private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings? /** Number of retry attempts if a peer fails to respond with the requested data */ - private static final int MAXIMUM_RETRIES = 3; // XXX move to Settings? + private static final int MAXIMUM_RETRIES = 1; // XXX move to Settings? /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ private static final long PEER_VERSION_150 = 0x0100050000L; From 2556855bd77d20dbfad6850a8b97c9c500b56cb2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 28 Mar 2021 14:19:53 +0100 Subject: [PATCH 07/25] Added missing return statement if a peer fails to respond with blocks when fast syncing. --- src/main/java/org/qortal/controller/Synchronizer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index c747dd6e..ff96c5b7 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -607,6 +607,7 @@ public class Synchronizer { if (blocks == null || blocks.isEmpty()) { LOGGER.info(String.format("Peer %s failed to respond with more blocks after height %d, sig %.8s", peer, ourHeight, Base58.encode(latestPeerSignature))); + return SynchronizationResult.NO_REPLY; } LOGGER.trace(String.format("Received %d blocks after height %d, sig %.8s from %s", blocks.size(), ourHeight, Base58.encode(latestPeerSignature), peer)); From f22f954ae3c708088e8521ba5d2717d5b72d35de Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 28 Mar 2021 14:35:47 +0100 Subject: [PATCH 08/25] Use MAXIMUM_BLOCKS_REQUEST_SIZE for GetBlocksMessage and BlockMessage, instead of MAXIMUM_REQUEST_SIZE. Currently set to 1, as serialization of the BlocksMessage data on mainnet is too slow to use this for any significant number of blocks right now. Hopefully we can find a way to optimise this process, which will allow us to use this for multiple block syncing. Until then, sticking with single blocks should still be enough to help solve the network congestion and re-orgs we are seeing, because it gives us the ability to request the next block based on the previous block's signature, which was unavailable using GET_BLOCK. This removes the requirement to fetch all block signatures upfront, and therefore it shouldn't matter if the peer does a partial re-org whilst a node is syncing to it. --- src/main/java/org/qortal/controller/Synchronizer.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index ff96c5b7..0e953db1 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -56,6 +56,9 @@ public class Synchronizer { /** Maximum number of block signatures we ask from peer in one go */ private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings? + /** Maximum number of blocks we ask from peer in one go */ + private static final int MAXIMUM_BLOCKS_REQUEST_SIZE = 1; // XXX move to Settings? + /** Number of retry attempts if a peer fails to respond with the requested data */ private static final int MAXIMUM_RETRIES = 1; // XXX move to Settings? @@ -383,7 +386,7 @@ public class Synchronizer { byte[] latestPeerSignature = peerBlocks.isEmpty() ? commonBlockSig : peerBlocks.get(peerBlocks.size() - 1).getSignature(); int lastPeerHeight = commonBlockHeight + peerBlocks.size(); - int numberOfBlocksToRequest = Math.min(numberBlocksRequired, MAXIMUM_REQUEST_SIZE); + int numberOfBlocksToRequest = Math.min(numberBlocksRequired, MAXIMUM_BLOCKS_REQUEST_SIZE); LOGGER.trace(String.format("Requesting %d block%s after height %d, sig %.8s", numberOfBlocksToRequest, (numberOfBlocksToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); @@ -600,7 +603,7 @@ public class Synchronizer { if (Controller.isStopping()) return SynchronizationResult.SHUTTING_DOWN; - int numberRequested = Math.min(maxBatchHeight - ourHeight, MAXIMUM_REQUEST_SIZE); + int numberRequested = Math.min(maxBatchHeight - ourHeight, MAXIMUM_BLOCKS_REQUEST_SIZE); LOGGER.trace(String.format("Fetching %d blocks after height %d, sig %.8s from %s", numberRequested, ourHeight, Base58.encode(latestPeerSignature), peer)); List blocks = this.fetchBlocks(repository, peer, latestPeerSignature, numberRequested); From cb80280eaf68a2cc94dd8f2dbe98652b714c188c Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 28 Mar 2021 14:47:57 +0100 Subject: [PATCH 09/25] Bump Peer response timeout from 3s to 4s --- src/main/java/org/qortal/network/Peer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index f619111a..9212fb56 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -46,7 +46,7 @@ public class Peer { private static final int CONNECT_TIMEOUT = 2000; // ms /** Maximum time to wait for a message reply to arrive from peer. (ms) */ - private static final int RESPONSE_TIMEOUT = 3000; // ms + private static final int RESPONSE_TIMEOUT = 4000; // ms /** * Interval between PING messages to a peer. (ms) From f2bbafe6c295a967bcf49d5dc5ea0d8759a2a10a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 28 Mar 2021 16:52:15 +0100 Subject: [PATCH 10/25] Added missing break statement if a peer fails to respond with blocks when resolving a fork via fast sync. --- src/main/java/org/qortal/controller/Synchronizer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 0e953db1..ade6fb88 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -399,6 +399,7 @@ public class Synchronizer { if (peerBlocks.isEmpty()) { return SynchronizationResult.NO_REPLY; } + break; } LOGGER.debug(String.format("Received %d blocks from peer %s", blocks.size(), peer)); From 08f3d653ccbdce11f77a28311f32f5dca39207bb Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 28 Mar 2021 17:27:04 +0100 Subject: [PATCH 11/25] Added new settings "maxBlocksPerRequest" and "maxBlocksPerResponse", to control the number of blocks requested and returned by nodes when using GetBlocksMessage and BlocksMessage. --- .../java/org/qortal/controller/Controller.java | 12 +++++++++--- .../java/org/qortal/controller/Synchronizer.java | 14 +++++++++----- src/main/java/org/qortal/network/Network.java | 1 - .../org/qortal/network/message/BlocksMessage.java | 1 + src/main/java/org/qortal/settings/Settings.java | 8 ++++++++ 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 40c25d84..d988f24d 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1245,6 +1245,10 @@ public class Controller extends Thread { return; } + // Ensure that we don't serve more blocks than the amount specified in the settings + // Serializing multiple blocks is very slow, so by default we are using a low limit + int blockLimitPerRequest = Settings.getInstance().getMaxBlocksPerResponse(); + List blockDataList = new ArrayList<>(); // Attempt to serve from our cache of latest blocks @@ -1256,7 +1260,7 @@ public class Controller extends Thread { } if (blockDataList.isEmpty()) { - int numberRequested = Math.min(Network.MAX_BLOCKS_PER_REPLY, getBlocksMessage.getNumberRequested()); + int numberRequested = Math.min(blockLimitPerRequest, getBlocksMessage.getNumberRequested()); BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); @@ -1274,8 +1278,10 @@ public class Controller extends Thread { List blocks = new ArrayList<>(); for (BlockData blockData : blockDataList) { - Block block = new Block(repository, blockData); - blocks.add(block); + if (blocks.size() < blockLimitPerRequest) { + Block block = new Block(repository, blockData); + blocks.add(block); + } } Message blocksMessage = new BlocksMessage(blocks); diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index ade6fb88..c72d18da 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -56,9 +56,6 @@ public class Synchronizer { /** Maximum number of block signatures we ask from peer in one go */ private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings? - /** Maximum number of blocks we ask from peer in one go */ - private static final int MAXIMUM_BLOCKS_REQUEST_SIZE = 1; // XXX move to Settings? - /** Number of retry attempts if a peer fails to respond with the requested data */ private static final int MAXIMUM_RETRIES = 1; // XXX move to Settings? @@ -380,13 +377,17 @@ public class Synchronizer { if (Settings.getInstance().isFastSyncEnabledWhenResolvingFork() && peer.getPeersVersion() >= PEER_VERSION_150) { // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings int numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); + + // Ensure that we don't request more blocks than specified in the settings + int maxBlocksPerRequest = Settings.getInstance().getMaxBlocksPerRequest(); + while (numberBlocksRequired > 0) { if (Controller.isStopping()) return SynchronizationResult.SHUTTING_DOWN; byte[] latestPeerSignature = peerBlocks.isEmpty() ? commonBlockSig : peerBlocks.get(peerBlocks.size() - 1).getSignature(); int lastPeerHeight = commonBlockHeight + peerBlocks.size(); - int numberOfBlocksToRequest = Math.min(numberBlocksRequired, MAXIMUM_BLOCKS_REQUEST_SIZE); + int numberOfBlocksToRequest = Math.min(numberBlocksRequired, maxBlocksPerRequest); LOGGER.trace(String.format("Requesting %d block%s after height %d, sig %.8s", numberOfBlocksToRequest, (numberOfBlocksToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); @@ -600,11 +601,14 @@ public class Synchronizer { // Fetch, and apply, blocks from peer int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE; + // Ensure that we don't request more blocks than specified in the settings + int maxBlocksPerRequest = Settings.getInstance().getMaxBlocksPerRequest(); + while (ourHeight < peerHeight && ourHeight < maxBatchHeight) { if (Controller.isStopping()) return SynchronizationResult.SHUTTING_DOWN; - int numberRequested = Math.min(maxBatchHeight - ourHeight, MAXIMUM_BLOCKS_REQUEST_SIZE); + int numberRequested = Math.min(maxBatchHeight - ourHeight, maxBlocksPerRequest); LOGGER.trace(String.format("Fetching %d blocks after height %d, sig %.8s from %s", numberRequested, ourHeight, Base58.encode(latestPeerSignature), peer)); List blocks = this.fetchBlocks(repository, peer, latestPeerSignature, numberRequested); diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index 7a234c7a..0e9ac32b 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -89,7 +89,6 @@ public class Network { public static final int MAX_SIGNATURES_PER_REPLY = 500; public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500; - public static final int MAX_BLOCKS_PER_REPLY = 500; // Generate our node keys / ID private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom()); diff --git a/src/main/java/org/qortal/network/message/BlocksMessage.java b/src/main/java/org/qortal/network/message/BlocksMessage.java index f53de301..b997ead5 100644 --- a/src/main/java/org/qortal/network/message/BlocksMessage.java +++ b/src/main/java/org/qortal/network/message/BlocksMessage.java @@ -78,6 +78,7 @@ public class BlocksMessage extends Message { bytes.write(Ints.toByteArray(block.getBlockData().getHeight())); bytes.write(BlockTransformer.toBytes(block)); } + LOGGER.trace(String.format("Total length of %d blocks is %d bytes", this.blocks.size(), bytes.size())); return bytes.toByteArray(); } catch (IOException e) { diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index d15a4ab9..3070bf96 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -127,6 +127,10 @@ public class Settings { private boolean fastSyncEnabled = false; /** Whether to sync multiple blocks at once when the peer has a different chain */ private boolean fastSyncEnabledWhenResolvingFork = true; + /** Maximum number of blocks to request at once */ + private int maxBlocksPerRequest = 1; + /** Maximum number of blocks this node will serve in a single response */ + private int maxBlocksPerResponse = 5; // Which blockchains this node is running private String blockchainConfig = null; // use default from resources @@ -449,6 +453,10 @@ public class Settings { return this.fastSyncEnabledWhenResolvingFork; } + public int getMaxBlocksPerRequest() { return this.maxBlocksPerRequest; } + + public int getMaxBlocksPerResponse() { return this.maxBlocksPerResponse; } + public boolean isAutoUpdateEnabled() { return this.autoUpdateEnabled; } From 6c5dbf7bd053b6eb0ffa4e77bc0d32597482387f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 8 May 2021 16:36:42 +0100 Subject: [PATCH 12/25] Preliminary version for fast sync set to 1.6.0, because 1.5.x is already released. --- src/main/java/org/qortal/controller/Synchronizer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 4f3833b3..a160ea62 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -61,7 +61,7 @@ public class Synchronizer { private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings? /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ - private static final long PEER_VERSION_150 = 0x0100050000L; + private static final long PEER_VERSION_160 = 0x0100060000L; private static Synchronizer instance; @@ -967,7 +967,7 @@ public class Synchronizer { private SynchronizationResult applyNewBlocks(Repository repository, BlockData commonBlockData, int ourInitialHeight, Peer peer, int peerHeight, List peerBlockSummaries) throws InterruptedException, DataException { - if (Settings.getInstance().isFastSyncEnabled() && peer.getPeersVersion() >= PEER_VERSION_150) + if (Settings.getInstance().isFastSyncEnabled() && peer.getPeersVersion() >= PEER_VERSION_160) // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings return this.applyNewBlocksUsingFastSync(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries); else From 3aa9b5f0b6f3eee4bf2cb00ce4e49954472f19ed Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 8 May 2021 22:36:41 +0100 Subject: [PATCH 13/25] Increased timeout when syncing multiple blocks from 4s to 10s. --- .../org/qortal/controller/Synchronizer.java | 5 +++- src/main/java/org/qortal/network/Peer.java | 23 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index a160ea62..cba5d3c0 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -63,6 +63,9 @@ public class Synchronizer { /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ private static final long PEER_VERSION_160 = 0x0100060000L; + /** Maximum time to wait for a peer to respond with blocks (ms) */ + private static final int FETCH_BLOCKS_TIMEOUT = 10000; + private static Synchronizer instance; @@ -1189,7 +1192,7 @@ public class Synchronizer { private List fetchBlocks(Repository repository, Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException { Message getBlocksMessage = new GetBlocksMessage(parentSignature, numberRequested); - Message message = peer.getResponse(getBlocksMessage); + Message message = peer.getResponseWithTimeout(getBlocksMessage, FETCH_BLOCKS_TIMEOUT); if (message == null || message.getType() != MessageType.BLOCKS) { return null; } diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index cc4ad918..e5bd369d 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -538,7 +538,23 @@ public class Peer { } /** - * Send message to peer and await response. + * Send message to peer and await response, using default RESPONSE_TIMEOUT. + *

+ * Message is assigned a random ID and sent. If a response with matching ID is received then it is returned to caller. + *

+ * If no response with matching ID within timeout, or some other error/exception occurs, then return null.
+ * (Assume peer will be rapidly disconnected after this). + * + * @param message + * @return Message if valid response received; null if not or error/exception occurs + * @throws InterruptedException + */ + public Message getResponse(Message message) throws InterruptedException { + return getResponseWithTimeout(message, RESPONSE_TIMEOUT); + } + + /** + * Send message to peer and await response, using custom timeout. *

* Message is assigned a random ID and sent. If a response with matching ID is received then it is returned to caller. *

@@ -546,10 +562,11 @@ public class Peer { * (Assume peer will be rapidly disconnected after this). * * @param message + * @param timeout * @return Message if valid response received; null if not or error/exception occurs * @throws InterruptedException */ - public Message getResponse(Message message) throws InterruptedException { + public Message getResponseWithTimeout(Message message, int timeout) throws InterruptedException { BlockingQueue blockingQueue = new ArrayBlockingQueue<>(1); // Assign random ID to this message @@ -570,7 +587,7 @@ public class Peer { } try { - return blockingQueue.poll(RESPONSE_TIMEOUT, TimeUnit.MILLISECONDS); + return blockingQueue.poll(timeout, TimeUnit.MILLISECONDS); } finally { this.replyQueues.remove(id); } From d2ea5633fb82269d7c3e683b453b7b342ef6d589 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 9 May 2021 08:25:24 +0100 Subject: [PATCH 14/25] Fixed divide by zero exception. Block.calcKeyDistance() cannot be called on some trimmed blocks, because the minter level is unable to be inferred in some cases. This generally hasn't been an issue, but the new Block.logDebugInfo() method is invoking it for all blocks. For now I am adding defensiveness to the debug method, but longer term we might want to add defensiveness to Block.calcKeyDistance() itself, if we ever encounter this issue again. I will leave it alone for now, to reduce risk. --- src/main/java/org/qortal/block/Block.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 384ca193..41adbd78 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -2021,7 +2021,7 @@ public class Block { LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount())); BlockSummaryData blockSummaryData = new BlockSummaryData(this.getBlockData()); - if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null) + if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null || minterLevel == 0) return; blockSummaryData.setMinterLevel(minterLevel); From 68544715bf5f98df2e2baae190dfaa30c895e50a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 9 May 2021 09:00:53 +0100 Subject: [PATCH 15/25] Skip Block.logDebugInfo() altogether if the log level is more specific than DEBUG, to avoid wasting resources. --- src/main/java/org/qortal/block/Block.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 41adbd78..1f00afc2 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -2010,6 +2010,10 @@ public class Block { private void logDebugInfo() { try { + // Avoid calculations if possible. We have to check against INFO here, since Level.isMoreSpecificThan() confusingly uses <= rather than just < + if (LOGGER.getLevel().isMoreSpecificThan(Level.INFO)) + return; + if (this.repository == null || this.getMinter() == null || this.getBlockData() == null) return; From 428af3c0e8da7d9165d14f05a9e4a666eed1199c Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 9 May 2021 13:57:47 +0100 Subject: [PATCH 16/25] Only use fast sync on trimmed blocks, as others are too large. This could probably be improved to make sure that all blocks in the next request are within the trimmed time range, but it's enough for now. --- src/main/java/org/qortal/controller/Synchronizer.java | 3 ++- src/main/java/org/qortal/data/block/BlockData.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index cba5d3c0..6ceee7f3 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -970,7 +970,8 @@ public class Synchronizer { private SynchronizationResult applyNewBlocks(Repository repository, BlockData commonBlockData, int ourInitialHeight, Peer peer, int peerHeight, List peerBlockSummaries) throws InterruptedException, DataException { - if (Settings.getInstance().isFastSyncEnabled() && peer.getPeersVersion() >= PEER_VERSION_160) + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + if (Settings.getInstance().isFastSyncEnabled() && peer.getPeersVersion() >= PEER_VERSION_160 && ourLatestBlockData.isTrimmed()) // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings return this.applyNewBlocksUsingFastSync(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries); else diff --git a/src/main/java/org/qortal/data/block/BlockData.java b/src/main/java/org/qortal/data/block/BlockData.java index 3567d0f8..e2d6bad1 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -9,7 +9,10 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import org.qortal.block.BlockChain; +import org.qortal.settings.Settings; import org.qortal.crypto.Crypto; +import org.qortal.utils.NTP; // All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) @@ -204,6 +207,13 @@ public class BlockData implements Serializable { return this.onlineAccountsSignatures; } + public boolean isTrimmed() { + long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime(); + long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime(); + long blockTimestamp = this.getTimestamp(); + return blockTimestamp < onlineAccountSignaturesTrimmedTimestamp && blockTimestamp < currentTrimmableTimestamp; + } + // JAXB special @XmlElement(name = "minterAddress") From 255233fe38fc048f04e0e87cef8224fc906c5aa8 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 10 May 2021 09:10:14 +0100 Subject: [PATCH 17/25] Reduced log spam. --- src/main/java/org/qortal/network/Handshake.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java index 8bee63a2..78b181ce 100644 --- a/src/main/java/org/qortal/network/Handshake.java +++ b/src/main/java/org/qortal/network/Handshake.java @@ -75,7 +75,7 @@ public enum Handshake { // Ensure the peer is running at least the minimum version allowed for connections final String minPeerVersion = Settings.getInstance().getMinPeerVersion(); if (peer.isAtLeastVersion(minPeerVersion) == false) { - LOGGER.info(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); + LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); return null; } } From 688404011b3dd80e4c6de52b7e96457139f5be78 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 30 May 2021 10:08:50 +0100 Subject: [PATCH 18/25] Relocate FETCH_BLOCKS_TIMEOUT to Peer.java and use a static import. --- src/main/java/org/qortal/controller/Synchronizer.java | 5 +++-- src/main/java/org/qortal/network/Peer.java | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index a0ffaff9..185e77a0 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -42,6 +42,8 @@ import org.qortal.transaction.Transaction; import org.qortal.utils.Base58; import org.qortal.utils.NTP; +import static org.qortal.network.Peer.FETCH_BLOCKS_TIMEOUT; + public class Synchronizer { private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class); @@ -63,8 +65,7 @@ public class Synchronizer { /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ private static final long PEER_VERSION_160 = 0x0100060000L; - /** Maximum time to wait for a peer to respond with blocks (ms) */ - private static final int FETCH_BLOCKS_TIMEOUT = 10000; + private static Synchronizer instance; diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index e50c18d6..8c364dc7 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -47,6 +47,11 @@ public class Peer { */ private static final int RESPONSE_TIMEOUT = 3000; // ms + /** + * Maximum time to wait for a peer to respond with blocks (ms) + */ + public static final int FETCH_BLOCKS_TIMEOUT = 10000; + /** * Interval between PING messages to a peer. (ms) *

From f58a52eaa4ba6b68b9dcae0a04d2bda56906930b Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 30 May 2021 13:10:38 +0100 Subject: [PATCH 19/25] Further work to increase the response timeout when requesting multiple blocks. --- .../java/org/qortal/controller/Controller.java | 4 +++- src/main/java/org/qortal/network/Peer.java | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 428b9043..96d325f4 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -103,6 +103,8 @@ import org.qortal.utils.Triple; import com.google.common.primitives.Longs; +import static org.qortal.network.Peer.FETCH_BLOCKS_TIMEOUT; + public class Controller extends Thread { static { @@ -1374,7 +1376,7 @@ public class Controller extends Thread { Message blocksMessage = new BlocksMessage(blocks); blocksMessage.setId(message.getId()); - if (!peer.sendMessage(blocksMessage)) + if (!peer.sendMessageWithTimeout(blocksMessage, FETCH_BLOCKS_TIMEOUT)) peer.disconnect("failed to send blocks"); } catch (DataException e) { diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index 8c364dc7..c2535118 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -524,12 +524,22 @@ public class Peer { } /** - * Attempt to send Message to peer. + * Attempt to send Message to peer, using default RESPONSE_TIMEOUT. * * @param message message to be sent * @return true if message successfully sent; false otherwise */ public boolean sendMessage(Message message) { + return this.sendMessageWithTimeout(message, RESPONSE_TIMEOUT); + } + + /** + * Attempt to send Message to peer, using custom timeout. + * + * @param message message to be sent + * @return true if message successfully sent; false otherwise + */ + public boolean sendMessageWithTimeout(Message message, int timeout) { if (!this.socketChannel.isOpen()) { return false; } @@ -563,7 +573,7 @@ public class Peer { */ Thread.sleep(1L); //NOSONAR squid:S2276 - if (System.currentTimeMillis() - sendStart > RESPONSE_TIMEOUT) { + if (System.currentTimeMillis() - sendStart > timeout) { // We've taken too long to send this message return false; } @@ -630,7 +640,7 @@ public class Peer { message.setId(id); // Try to send message - if (!this.sendMessage(message)) { + if (!this.sendMessageWithTimeout(message, timeout)) { this.replyQueues.remove(id); return null; } From ed423ed0418a44b2d7842794d37e6513222fb6b4 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 30 May 2021 14:54:13 +0100 Subject: [PATCH 20/25] Increased MAX_DATA_SIZE and SYNC_BATCH_SIZE, to increase the effectiveness of the batch sync. --- src/main/java/org/qortal/controller/Synchronizer.java | 2 +- src/main/java/org/qortal/network/message/Message.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 185e77a0..c393232d 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -49,7 +49,7 @@ public class Synchronizer { private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class); /** Max number of new blocks we aim to add to chain tip in each sync round */ - private static final int SYNC_BATCH_SIZE = 200; // XXX move to Settings? + private static final int SYNC_BATCH_SIZE = 1000; // XXX move to Settings? /** Initial jump back of block height when searching for common block with peer */ private static final int INITIAL_BLOCK_STEP = 8; diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index d1546dce..07c44c7b 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -25,7 +25,7 @@ public abstract class Message { private static final int MAGIC_LENGTH = 4; private static final int CHECKSUM_LENGTH = 4; - private static final int MAX_DATA_SIZE = 1024 * 1024; // 1MB + private static final int MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB @SuppressWarnings("serial") public static class MessageException extends Exception { From 2ceba45782724b6b67196ebba21bdfb6bbe65150 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 30 May 2021 14:57:58 +0100 Subject: [PATCH 21/25] Fast sync default blocks per request increased to 100. --- src/main/java/org/qortal/settings/Settings.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index a9898a87..32308db1 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -138,9 +138,9 @@ public class Settings { /** Whether to sync multiple blocks at once when the peer has a different chain */ private boolean fastSyncEnabledWhenResolvingFork = true; /** Maximum number of blocks to request at once */ - private int maxBlocksPerRequest = 1; + private int maxBlocksPerRequest = 100; /** Maximum number of blocks this node will serve in a single response */ - private int maxBlocksPerResponse = 5; + private int maxBlocksPerResponse = 100; // Which blockchains this node is running private String blockchainConfig = null; // use default from resources From cffbd41f2614644f73750437c28f63a098654868 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 2 Jun 2021 09:09:06 +0100 Subject: [PATCH 22/25] Reduce memory allocations in onNetworkGetBlocksMessage --- .../org/qortal/controller/Controller.java | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 96d325f4..2f8cb56d 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1338,40 +1338,16 @@ public class Controller extends Thread { // Ensure that we don't serve more blocks than the amount specified in the settings // Serializing multiple blocks is very slow, so by default we are using a low limit int blockLimitPerRequest = Settings.getInstance().getMaxBlocksPerResponse(); - - List blockDataList = new ArrayList<>(); - - // Attempt to serve from our cache of latest blocks - synchronized (this.latestBlocks) { - blockDataList = this.latestBlocks.stream() - .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature)) - .map(BlockData::new) - .collect(Collectors.toList()); - } - - if (blockDataList.isEmpty()) { - int numberRequested = Math.min(blockLimitPerRequest, getBlocksMessage.getNumberRequested()); - - BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); - - while (blockData != null && blockDataList.size() < numberRequested) { - blockDataList.add(blockData); - - blockData = repository.getBlockRepository().fromReference(blockData.getSignature()); - } - } else { - this.stats.getBlocksMessageStats.cacheHits.incrementAndGet(); - - if (blockDataList.size() >= getBlocksMessage.getNumberRequested()) - this.stats.getBlocksMessageStats.fullyFromCache.incrementAndGet(); - } + int untrimmedBlockLimitPerRequest = Settings.getInstance().getMaxBlocksPerResponse(); + int numberRequested = Math.min(blockLimitPerRequest, getBlocksMessage.getNumberRequested()); List blocks = new ArrayList<>(); - for (BlockData blockData : blockDataList) { - if (blocks.size() < blockLimitPerRequest) { - Block block = new Block(repository, blockData); - blocks.add(block); - } + BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); + + while (blockData != null && blocks.size() < numberRequested) { + Block block = new Block(repository, blockData); + blocks.add(block); + blockData = repository.getBlockRepository().fromReference(blockData.getSignature()); } Message blocksMessage = new BlocksMessage(blocks); From c63a7884cbb15bab3f2da8adaa947868d6d0e768 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 2 Jun 2021 09:10:25 +0100 Subject: [PATCH 23/25] Limit to 10 untrimmed blocks per response, as they are larger than the trimmed ones. --- src/main/java/org/qortal/controller/Controller.java | 4 ++++ src/main/java/org/qortal/settings/Settings.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 2f8cb56d..8bf8e955 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1345,6 +1345,10 @@ public class Controller extends Thread { BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); while (blockData != null && blocks.size() < numberRequested) { + // If we're dealing with untrimmed blocks, ensure we don't go above the untrimmedBlockLimitPerRequest + if (blockData.isTrimmed() == false && blocks.size() >= untrimmedBlockLimitPerRequest) { + break; + } Block block = new Block(repository, blockData); blocks.add(block); blockData = repository.getBlockRepository().fromReference(blockData.getSignature()); diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 32308db1..184cb0cd 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -141,6 +141,8 @@ public class Settings { private int maxBlocksPerRequest = 100; /** Maximum number of blocks this node will serve in a single response */ private int maxBlocksPerResponse = 100; + /** Maximum number of untrimmed blocks this node will serve in a single response */ + private int maxUntrimmedBlocksPerResponse = 10; // Which blockchains this node is running private String blockchainConfig = null; // use default from resources @@ -474,6 +476,8 @@ public class Settings { public int getMaxBlocksPerResponse() { return this.maxBlocksPerResponse; } + public int getMaxUntrimmedBlocksPerResponse() { return this.maxUntrimmedBlocksPerResponse; } + public boolean isAutoUpdateEnabled() { return this.autoUpdateEnabled; } From bc6b3fb5f4248beaeef615be6516b06f6a0bee9f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 9 Jun 2021 13:04:49 +0100 Subject: [PATCH 24/25] Include timestamps in block-timings.sh --- tools/block-timings.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/block-timings.sh b/tools/block-timings.sh index 5324209b..88d8d643 100755 --- a/tools/block-timings.sh +++ b/tools/block-timings.sh @@ -69,11 +69,13 @@ function fetch_and_process_blocks { online_accounts_count=$(echo "${block_minting_info}" | jq -r .onlineAccountsCount) key_distance_ratio=$(echo "${block_minting_info}" | jq -r .keyDistanceRatio) time_delta=$(echo "${block_minting_info}" | jq -r .timeDelta) + timestamp=$(echo "${block_minting_info}" | jq -r .timestamp) time_offset=$(calculate_time_offset "${key_distance_ratio}") block_time=$((target-deviation+time_offset)) echo "=== BLOCK ${height} ===" + echo "Timestamp: ${timestamp}" echo "Minter level: ${minter_level}" echo "Online accounts: ${online_accounts_count}" echo "Key distance ratio: ${key_distance_ratio}" From 904be3005fea9ab377ff26c02de5f8ecace6fcc0 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 12 Jun 2021 13:07:25 +0100 Subject: [PATCH 25/25] Enable fast sync by default. --- src/main/java/org/qortal/settings/Settings.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 184cb0cd..55f421af 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -134,13 +134,13 @@ public class Settings { private boolean allowConnectionsWithOlderPeerVersions = true; /** Whether to sync multiple blocks at once in normal operation */ - private boolean fastSyncEnabled = false; + private boolean fastSyncEnabled = true; /** Whether to sync multiple blocks at once when the peer has a different chain */ private boolean fastSyncEnabledWhenResolvingFork = true; /** Maximum number of blocks to request at once */ private int maxBlocksPerRequest = 100; /** Maximum number of blocks this node will serve in a single response */ - private int maxBlocksPerResponse = 100; + private int maxBlocksPerResponse = 200; /** Maximum number of untrimmed blocks this node will serve in a single response */ private int maxUntrimmedBlocksPerResponse = 10;