From a69078988897fadedce8bb230f72d8e869703fa4 Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Tue, 31 May 2022 21:06:34 +0100
Subject: [PATCH] Initial work on BLOCK_SUMMARIES_V2, part of a bigger arc to
 improve synchronization.

Touches quite a few files because:

* Deprecate HEIGHT_V2 because it doesn't contain enough info to be fully useful during sync.
Newer peers will re-use BLOCK_SUMMARIES_V2.

* For newer peers, instead of sending / broadcasting HEIGHT_V2,
send top N block summaries instead, to avoid requests for minor reorgs.

* When responding to GET_BLOCK, and we don't actually have the requested block,
we currently send an empty BLOCK_SUMMARIES message instead of not responding,
which would cause a slow timeout in Synchronizer.

This pattern has spread to other network message response code,
so now we introduce a generic 'unknown' message type for all these cases.

* Remove PeerChainTipData class entirely and re-use BlockSummaryData instead.

* Each Peer instance used to hold PeerChainTipData - essentially single latest block summary - but now holds a List of latest block summaries.

* PeerChainTipData getter/setter methods modified for compatibility at this point in time.

* Repository methods that return BlockSummaryData (or lists of) now try to fully populate them,
including newly added block reference field.

* Re-worked Peer.canUseCommonBlockData() to be more readable

* Cherry-picked patch to Message.fromByteBuffer() to pass an empty, read-only ByteBuffer to subclass fromByteBuffer() methods, instead of null.
This allows natural use of BufferUnderflowException if a subclass tries to use read(), or hasRemaining(), etc. from an empty data-payload message.
Previously this could have caused an NPE.
---
 .../org/qortal/api/model/ConnectedPeer.java   |  10 +-
 .../org/qortal/controller/BlockMinter.java    |  16 +--
 .../org/qortal/controller/Controller.java     | 102 +++++++++++------
 .../org/qortal/controller/Synchronizer.java   |  44 ++++----
 .../arbitrary/ArbitraryDataFileManager.java   |   7 +-
 .../qortal/data/block/BlockSummaryData.java   |  24 +++-
 .../qortal/data/block/CommonBlockData.java    |   8 +-
 .../qortal/data/network/PeerChainTipData.java |  37 -------
 src/main/java/org/qortal/network/Network.java |  62 +++++++++--
 src/main/java/org/qortal/network/Peer.java    |  66 ++++++-----
 .../message/BlockSummariesV2Message.java      | 104 ++++++++++++++++++
 .../message/GenericUnknownMessage.java        |  23 ++++
 .../qortal/network/message/MessageType.java   |   2 +
 .../hsqldb/HSQLDBBlockArchiveRepository.java  |   8 +-
 .../hsqldb/HSQLDBBlockRepository.java         |  13 ++-
 15 files changed, 367 insertions(+), 159 deletions(-)
 delete mode 100644 src/main/java/org/qortal/data/network/PeerChainTipData.java
 create mode 100644 src/main/java/org/qortal/network/message/BlockSummariesV2Message.java
 create mode 100644 src/main/java/org/qortal/network/message/GenericUnknownMessage.java

diff --git a/src/main/java/org/qortal/api/model/ConnectedPeer.java b/src/main/java/org/qortal/api/model/ConnectedPeer.java
index 21bfc1f9..3d383321 100644
--- a/src/main/java/org/qortal/api/model/ConnectedPeer.java
+++ b/src/main/java/org/qortal/api/model/ConnectedPeer.java
@@ -1,7 +1,7 @@
 package org.qortal.api.model;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import org.qortal.data.network.PeerChainTipData;
+import org.qortal.data.block.BlockSummaryData;
 import org.qortal.data.network.PeerData;
 import org.qortal.network.Handshake;
 import org.qortal.network.Peer;
@@ -63,11 +63,11 @@ public class ConnectedPeer {
             this.age = "connecting...";
         }
 
-        PeerChainTipData peerChainTipData = peer.getChainTipData();
+        BlockSummaryData peerChainTipData = peer.getChainTipData();
         if (peerChainTipData != null) {
-            this.lastHeight = peerChainTipData.getLastHeight();
-            this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
-            this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
+            this.lastHeight = peerChainTipData.getHeight();
+            this.lastBlockSignature = peerChainTipData.getSignature();
+            this.lastBlockTimestamp = peerChainTipData.getTimestamp();
         }
     }
 
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 343ab4af..a07d37fe 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -26,6 +26,9 @@ import org.qortal.data.block.CommonBlockData;
 import org.qortal.data.transaction.TransactionData;
 import org.qortal.network.Network;
 import org.qortal.network.Peer;
+import org.qortal.network.message.BlockSummariesV2Message;
+import org.qortal.network.message.HeightV2Message;
+import org.qortal.network.message.Message;
 import org.qortal.repository.BlockRepository;
 import org.qortal.repository.DataException;
 import org.qortal.repository.Repository;
@@ -431,16 +434,9 @@ public class BlockMinter extends Thread {
 						blockchainLock.unlock();
 					}
 
-					if (newBlockMinted) {
-						// Broadcast our new chain to network
-						BlockData newBlockData = newBlock.getBlockData();
-
-						Network network = Network.getInstance();
-						network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
-					}
-				} catch (InterruptedException e) {
-					// We've been interrupted - time to exit
-					return;
+				if (newBlockMinted) {
+					// Broadcast our new chain to network
+					Network.getInstance().broadcastOurChain();
 				}
 			}
 		} catch (DataException e) {
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 8e1dfd8a..ce994757 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -45,7 +45,6 @@ import org.qortal.data.account.AccountData;
 import org.qortal.data.block.BlockData;
 import org.qortal.data.block.BlockSummaryData;
 import org.qortal.data.naming.NameData;
-import org.qortal.data.network.PeerChainTipData;
 import org.qortal.data.network.PeerData;
 import org.qortal.data.transaction.ChatTransactionData;
 import org.qortal.data.transaction.TransactionData;
@@ -731,25 +730,25 @@ public class Controller extends Thread {
 
 	public static final Predicate<Peer> hasNoRecentBlock = peer -> {
 		final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
-		final PeerChainTipData peerChainTipData = peer.getChainTipData();
-		return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
+		final BlockSummaryData peerChainTipData = peer.getChainTipData();
+		return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
 	};
 
 	public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
 		final BlockData latestBlockData = getInstance().getChainTip();
-		final PeerChainTipData peerChainTipData = peer.getChainTipData();
-		return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
+		final BlockSummaryData peerChainTipData = peer.getChainTipData();
+		return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
 	};
 
 	public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
-		final PeerChainTipData peerChainTipData = peer.getChainTipData();
-		return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
+		final BlockSummaryData peerChainTipData = peer.getChainTipData();
+		return peerChainTipData == null || peerChainTipData.getHeight() == 1;
 	};
 
 	public static final Predicate<Peer> hasInferiorChainTip = peer -> {
-		final PeerChainTipData peerChainTipData = peer.getChainTipData();
+		final BlockSummaryData peerChainTipData = peer.getChainTipData();
 		final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
-		return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getLastBlockSignature()));
+		return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
 	};
 
 	public static final Predicate<Peer> hasOldVersion = peer -> {
@@ -1011,8 +1010,7 @@ public class Controller extends Thread {
 		network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
 
 		// Send our current height
-		BlockData latestBlockData = getChainTip();
-		network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
+		network.broadcastOurChain();
 
 		// Request unconfirmed transaction signatures, but only if we're up-to-date.
 		// If we're NOT up-to-date then priority is synchronizing first
@@ -1219,6 +1217,10 @@ public class Controller extends Thread {
 				onNetworkHeightV2Message(peer, message);
 				break;
 
+			case BLOCK_SUMMARIES_V2:
+				onNetworkBlockSummariesV2Message(peer, message);
+				break;
+
 			case GET_TRANSACTION:
 				TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
 				break;
@@ -1373,8 +1375,10 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
 
-				// We'll send empty block summaries message as it's very short
-				Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
+						? new GenericUnknownMessage()
+						: new BlockSummariesMessage(Collections.emptyList());
 				blockUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(blockUnknownMessage))
 					peer.disconnect("failed to send block-unknown response");
@@ -1423,11 +1427,15 @@ public class Controller extends Thread {
 		this.stats.getBlockSummariesStats.requests.incrementAndGet();
 
 		// If peer's parent signature matches our latest block signature
-		// then we can short-circuit with an empty response
+		// then we have no blocks after that and can short-circuit with an empty response
 		BlockData chainTip = getChainTip();
 		if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
-			Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
+			Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+					? new BlockSummariesV2Message(Collections.emptyList())
+					: new BlockSummariesMessage(Collections.emptyList());
+
 			blockSummariesMessage.setId(message.getId());
+
 			if (!peer.sendMessage(blockSummariesMessage))
 				peer.disconnect("failed to send block summaries");
 
@@ -1483,7 +1491,9 @@ public class Controller extends Thread {
 				this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
 		}
 
-		Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
+		Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+				? new BlockSummariesV2Message(blockSummaries)
+				: new BlockSummariesMessage(blockSummaries);
 		blockSummariesMessage.setId(message.getId());
 		if (!peer.sendMessage(blockSummariesMessage))
 			peer.disconnect("failed to send block summaries");
@@ -1558,18 +1568,48 @@ public class Controller extends Thread {
 			// If peer is inbound and we've not updated their height
 			// then this is probably their initial HEIGHT_V2 message
 			// so they need a corresponding HEIGHT_V2 message from us
-			if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
-				peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
+			if (!peer.isOutbound() && peer.getChainTipData() == null) {
+				Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
+
+				if (responseMessage == null || !peer.sendMessage(responseMessage)) {
+					peer.disconnect("failed to send our chain tip info");
+					return;
+				}
+			}
 		}
 
 		// Update peer chain tip data
-		PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
+		BlockSummaryData newChainTipData = new BlockSummaryData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getMinterPublicKey(), heightV2Message.getTimestamp());
 		peer.setChainTipData(newChainTipData);
 
 		// Potentially synchronize
 		Synchronizer.getInstance().requestSync();
 	}
 
+	private void onNetworkBlockSummariesV2Message(Peer peer, Message message) {
+		BlockSummariesV2Message blockSummariesV2Message = (BlockSummariesV2Message) message;
+
+		if (!Settings.getInstance().isLite()) {
+			// If peer is inbound and we've not updated their height
+			// then this is probably their initial BLOCK_SUMMARIES_V2 message
+			// so they need a corresponding BLOCK_SUMMARIES_V2 message from us
+			if (!peer.isOutbound() && peer.getChainTipData() == null) {
+				Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
+
+				if (responseMessage == null || !peer.sendMessage(responseMessage)) {
+					peer.disconnect("failed to send our chain tip info");
+					return;
+				}
+			}
+		}
+
+		// Update peer chain tip data
+		peer.setChainTipSummaries(blockSummariesV2Message.getBlockSummaries());
+
+		// Potentially synchronize
+		Synchronizer.getInstance().requestSync();
+	}
+
 	private void onNetworkGetAccountMessage(Peer peer, Message message) {
 		GetAccountMessage getAccountMessage = (GetAccountMessage) message;
 		String address = getAccountMessage.getAddress();
@@ -1585,8 +1625,8 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address));
 
-				// We'll send empty block summaries message as it's very short
-				Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message accountUnknownMessage = new GenericUnknownMessage();
 				accountUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(accountUnknownMessage))
 					peer.disconnect("failed to send account-unknown response");
@@ -1621,8 +1661,8 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId));
 
-				// We'll send empty block summaries message as it's very short
-				Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message accountUnknownMessage = new GenericUnknownMessage();
 				accountUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(accountUnknownMessage))
 					peer.disconnect("failed to send account-unknown response");
@@ -1665,8 +1705,8 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address));
 
-				// We'll send empty block summaries message as it's very short
-				Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message accountUnknownMessage = new GenericUnknownMessage();
 				accountUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(accountUnknownMessage))
 					peer.disconnect("failed to send account-unknown response");
@@ -1702,8 +1742,8 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address));
 
-				// We'll send empty block summaries message as it's very short
-				Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message accountUnknownMessage = new GenericUnknownMessage();
 				accountUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(accountUnknownMessage))
 					peer.disconnect("failed to send account-unknown response");
@@ -1737,8 +1777,8 @@ public class Controller extends Thread {
 				// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
 				LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
 
-				// We'll send empty block summaries message as it's very short
-				Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+				// Send generic 'unknown' message as it's very short
+				Message nameUnknownMessage = new GenericUnknownMessage();
 				nameUnknownMessage.setId(message.getId());
 				if (!peer.sendMessage(nameUnknownMessage))
 					peer.disconnect("failed to send name-unknown response");
@@ -1786,14 +1826,14 @@ public class Controller extends Thread {
 				continue;
 			}
 
-			final PeerChainTipData peerChainTipData = peer.getChainTipData();
+			BlockSummaryData peerChainTipData = peer.getChainTipData();
 			if (peerChainTipData == null) {
 				iterator.remove();
 				continue;
 			}
 
 			// Disregard peers that don't have a recent block
-			if (peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp) {
+			if (peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp) {
 				iterator.remove();
 				continue;
 			}
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 74a4a785..a6fbfe71 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -19,7 +19,6 @@ import org.qortal.block.BlockChain;
 import org.qortal.data.block.BlockData;
 import org.qortal.data.block.BlockSummaryData;
 import org.qortal.data.block.CommonBlockData;
-import org.qortal.data.network.PeerChainTipData;
 import org.qortal.data.transaction.RewardShareTransactionData;
 import org.qortal.data.transaction.TransactionData;
 import org.qortal.event.Event;
@@ -282,7 +281,7 @@ public class Synchronizer extends Thread {
 		BlockData priorChainTip = Controller.getInstance().getChainTip();
 
 		synchronized (this.syncLock) {
-			this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
+			this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getHeight();
 
 			// Only update SysTray if we're potentially changing height
 			if (this.syncPercent < 100) {
@@ -312,7 +311,7 @@ public class Synchronizer extends Thread {
 
 				case INFERIOR_CHAIN: {
 					// Update our list of inferior chain tips
-					ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
+					ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
 					if (!inferiorChainSignatures.contains(inferiorChainSignature))
 						inferiorChainSignatures.add(inferiorChainSignature);
 
@@ -320,7 +319,8 @@ public class Synchronizer extends Thread {
 					LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
 
 					// Notify peer of our superior chain
-					if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
+					Message message = Network.getInstance().buildHeightOrChainTipInfo(peer);
+					if (message == null || !peer.sendMessage(message))
 						peer.disconnect("failed to notify peer of our superior chain");
 					break;
 				}
@@ -341,7 +341,7 @@ public class Synchronizer extends Thread {
 					// fall-through...
 				case NOTHING_TO_DO: {
 					// Update our list of inferior chain tips
-					ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
+					ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
 					if (!inferiorChainSignatures.contains(inferiorChainSignature))
 						inferiorChainSignatures.add(inferiorChainSignature);
 
@@ -369,8 +369,7 @@ public class Synchronizer extends Thread {
 				// Reset our cache of inferior chains
 				inferiorChainSignatures.clear();
 
-				Network network = Network.getInstance();
-				network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
+				Network.getInstance().broadcastOurChain();
 
 				EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
 			}
@@ -513,13 +512,13 @@ public class Synchronizer extends Thread {
 			final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
 			final int ourInitialHeight = ourLatestBlockData.getHeight();
 
-			PeerChainTipData peerChainTipData = peer.getChainTipData();
-			int peerHeight = peerChainTipData.getLastHeight();
-			byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
+			BlockSummaryData peerChainTipData = peer.getChainTipData();
+			int peerHeight = peerChainTipData.getHeight();
+			byte[] peersLastBlockSignature = peerChainTipData.getSignature();
 
 			byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
 			LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
-					peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
+					peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
 					ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
 
 			List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
@@ -637,9 +636,9 @@ public class Synchronizer extends Thread {
 							return peers;
 
 						// Count the number of blocks this peer has beyond our common block
-						final PeerChainTipData peerChainTipData = peer.getChainTipData();
-						final int peerHeight = peerChainTipData.getLastHeight();
-						final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
+						final BlockSummaryData peerChainTipData = peer.getChainTipData();
+						final int peerHeight = peerChainTipData.getHeight();
+						final byte[] peerLastBlockSignature = peerChainTipData.getSignature();
 						final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
 						// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
 						int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
@@ -727,8 +726,9 @@ public class Synchronizer extends Thread {
 
 					LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
 					for (Peer peer : peersSharingCommonBlock) {
-						final int peerHeight = peer.getChainTipData().getLastHeight();
-						final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
+						BlockSummaryData peerChainTipData = peer.getChainTipData();
+						final int peerHeight = peerChainTipData.getHeight();
+						final Long peerLastBlockTimestamp = peerChainTipData.getTimestamp();
 						final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
 						final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
 
@@ -825,7 +825,7 @@ public class Synchronizer extends Thread {
 		// Calculate the length of the shortest peer chain sharing this common block
 		int minChainLength = 0;
 		for (Peer peer : peersSharingCommonBlock) {
-			final int peerHeight = peer.getChainTipData().getLastHeight();
+			final int peerHeight = peer.getChainTipData().getHeight();
 			final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
 
 			if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
@@ -933,13 +933,13 @@ public class Synchronizer extends Thread {
 					final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
 					final int ourInitialHeight = ourLatestBlockData.getHeight();
 
-					PeerChainTipData peerChainTipData = peer.getChainTipData();
-					int peerHeight = peerChainTipData.getLastHeight();
-					byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
+					BlockSummaryData peerChainTipData = peer.getChainTipData();
+					int peerHeight = peerChainTipData.getHeight();
+					byte[] peersLastBlockSignature = peerChainTipData.getSignature();
 
 					byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
 					String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
-							peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
+							peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
 							ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
 					LOGGER.info(syncString);
 
@@ -1313,7 +1313,7 @@ public class Synchronizer extends Thread {
 			// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
 			if (!recoveryMode && peer.getChainTipData() != null) {
 				final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
-				final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
+				final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
 				if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
 					LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
 					return SynchronizationResult.CHAIN_TIP_TOO_OLD;
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
index 22cf4144..30b0fcca 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
@@ -595,9 +595,10 @@ public class ArbitraryDataFileManager extends Thread {
                 // Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
                 LOGGER.debug(String.format("Sending 'file unknown' response to peer %s for GET_FILE request for unknown file %s", peer, arbitraryDataFile));
 
-                // We'll send empty block summaries message as it's very short
-                // TODO: use a different message type here
-                Message fileUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+                // Send generic 'unknown' message as it's very short
+                Message fileUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
+                        ? new GenericUnknownMessage()
+                        : new BlockSummariesMessage(Collections.emptyList());
                 fileUnknownMessage.setId(message.getId());
                 if (!peer.sendMessage(fileUnknownMessage)) {
                     LOGGER.debug("Couldn't sent file-unknown response");
diff --git a/src/main/java/org/qortal/data/block/BlockSummaryData.java b/src/main/java/org/qortal/data/block/BlockSummaryData.java
index 2167f0f0..57e29d0d 100644
--- a/src/main/java/org/qortal/data/block/BlockSummaryData.java
+++ b/src/main/java/org/qortal/data/block/BlockSummaryData.java
@@ -11,11 +11,12 @@ public class BlockSummaryData {
 	private int height;
 	private byte[] signature;
 	private byte[] minterPublicKey;
-	private int onlineAccountsCount;
 
 	// Optional, set during construction
+	private Integer onlineAccountsCount;
 	private Long timestamp;
 	private Integer transactionCount;
+	private byte[] reference;
 
 	// Optional, set after construction
 	private Integer minterLevel;
@@ -25,6 +26,15 @@ public class BlockSummaryData {
 	protected BlockSummaryData() {
 	}
 
+	/** Constructor typically populated with fields from HeightV2Message */
+	public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, long timestamp) {
+		this.height = height;
+		this.signature = signature;
+		this.minterPublicKey = minterPublicKey;
+		this.timestamp = timestamp;
+	}
+
+	/** Constructor typically populated with fields from BlockSummariesMessage */
 	public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
 		this.height = height;
 		this.signature = signature;
@@ -32,13 +42,16 @@ public class BlockSummaryData {
 		this.onlineAccountsCount = onlineAccountsCount;
 	}
 
-	public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
+	/** Constructor typically populated with fields from BlockSummariesV2Message */
+	public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, Integer onlineAccountsCount,
+							Long timestamp, Integer transactionCount, byte[] reference) {
 		this.height = height;
 		this.signature = signature;
 		this.minterPublicKey = minterPublicKey;
 		this.onlineAccountsCount = onlineAccountsCount;
 		this.timestamp = timestamp;
 		this.transactionCount = transactionCount;
+		this.reference = reference;
 	}
 
 	public BlockSummaryData(BlockData blockData) {
@@ -49,6 +62,7 @@ public class BlockSummaryData {
 
 		this.timestamp = blockData.getTimestamp();
 		this.transactionCount = blockData.getTransactionCount();
+		this.reference = blockData.getReference();
 	}
 
 	// Getters / setters
@@ -65,7 +79,7 @@ public class BlockSummaryData {
 		return this.minterPublicKey;
 	}
 
-	public int getOnlineAccountsCount() {
+	public Integer getOnlineAccountsCount() {
 		return this.onlineAccountsCount;
 	}
 
@@ -77,6 +91,10 @@ public class BlockSummaryData {
 		return this.transactionCount;
 	}
 
+	public byte[] getReference() {
+		return this.reference;
+	}
+
 	public Integer getMinterLevel() {
 		return this.minterLevel;
 	}
diff --git a/src/main/java/org/qortal/data/block/CommonBlockData.java b/src/main/java/org/qortal/data/block/CommonBlockData.java
index dd502df7..37e9649b 100644
--- a/src/main/java/org/qortal/data/block/CommonBlockData.java
+++ b/src/main/java/org/qortal/data/block/CommonBlockData.java
@@ -1,7 +1,5 @@
 package org.qortal.data.block;
 
-import org.qortal.data.network.PeerChainTipData;
-
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import java.math.BigInteger;
@@ -14,14 +12,14 @@ public class CommonBlockData {
 	private BlockSummaryData commonBlockSummary = null;
 	private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
 	private BigInteger chainWeight = null;
-	private PeerChainTipData chainTipData = null;
+	private BlockSummaryData chainTipData = null;
 
 	// Constructors
 
 	protected CommonBlockData() {
 	}
 
-	public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
+	public CommonBlockData(BlockSummaryData commonBlockSummary, BlockSummaryData chainTipData) {
 		this.commonBlockSummary = commonBlockSummary;
 		this.chainTipData = chainTipData;
 	}
@@ -49,7 +47,7 @@ public class CommonBlockData {
 		this.chainWeight = chainWeight;
 	}
 
-	public PeerChainTipData getChainTipData() {
+	public BlockSummaryData getChainTipData() {
 		return this.chainTipData;
 	}
 
diff --git a/src/main/java/org/qortal/data/network/PeerChainTipData.java b/src/main/java/org/qortal/data/network/PeerChainTipData.java
deleted file mode 100644
index d8dbbad4..00000000
--- a/src/main/java/org/qortal/data/network/PeerChainTipData.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.qortal.data.network;
-
-public class PeerChainTipData {
-
-	/** Latest block height as reported by peer. */
-	private Integer lastHeight;
-	/** Latest block signature as reported by peer. */
-	private byte[] lastBlockSignature;
-	/** Latest block timestamp as reported by peer. */
-	private Long lastBlockTimestamp;
-	/** Latest block minter public key as reported by peer. */
-	private byte[] lastBlockMinter;
-
-	public PeerChainTipData(Integer lastHeight, byte[] lastBlockSignature, Long lastBlockTimestamp, byte[] lastBlockMinter) {
-		this.lastHeight = lastHeight;
-		this.lastBlockSignature = lastBlockSignature;
-		this.lastBlockTimestamp = lastBlockTimestamp;
-		this.lastBlockMinter = lastBlockMinter;
-	}
-
-	public Integer getLastHeight() {
-		return this.lastHeight;
-	}
-
-	public byte[] getLastBlockSignature() {
-		return this.lastBlockSignature;
-	}
-
-	public Long getLastBlockTimestamp() {
-		return this.lastBlockTimestamp;
-	}
-
-	public byte[] getLastBlockMinter() {
-		return this.lastBlockMinter;
-	}
-
-}
diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java
index 57073e99..8aac68f0 100644
--- a/src/main/java/org/qortal/network/Network.java
+++ b/src/main/java/org/qortal/network/Network.java
@@ -11,6 +11,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
 import org.qortal.controller.arbitrary.ArbitraryDataManager;
 import org.qortal.crypto.Crypto;
 import org.qortal.data.block.BlockData;
+import org.qortal.data.block.BlockSummaryData;
 import org.qortal.data.network.PeerData;
 import org.qortal.data.transaction.TransactionData;
 import org.qortal.network.message.*;
@@ -90,6 +91,8 @@ public class Network {
 
     private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
 
+    private static final int BROADCAST_CHAIN_TIP_DEPTH = 7; // Just enough to fill a SINGLE TCP packet (~1440 bytes)
+
     // Generate our node keys / ID
     private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
     private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
@@ -1087,10 +1090,16 @@ public class Network {
 
         if (peer.isOutbound()) {
             if (!Settings.getInstance().isLite()) {
-                // Send our height
-                Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
-                if (!peer.sendMessage(heightMessage)) {
-                    peer.disconnect("failed to send height/info");
+                // Send our height / chain tip info
+                Message message = this.buildHeightOrChainTipInfo(peer);
+
+                if (message == null) {
+                    peer.disconnect("Couldn't build our chain tip info");
+                    return;
+                }
+
+                if (!peer.sendMessage(message)) {
+                    peer.disconnect("failed to send height / chain tip info");
                     return;
                 }
             }
@@ -1164,10 +1173,47 @@ public class Network {
         return new PeersV2Message(peerAddresses);
     }
 
-    public Message buildHeightMessage(Peer peer, BlockData blockData) {
-        // HEIGHT_V2 contains way more useful info
-        return new HeightV2Message(blockData.getHeight(), blockData.getSignature(),
-                blockData.getTimestamp(), blockData.getMinterPublicKey());
+    /** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
+     *
+     *  @return Message, or null if DataException was thrown.
+     */
+    public Message buildHeightOrChainTipInfo(Peer peer) {
+        if (peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION) {
+            int latestHeight = Controller.getInstance().getChainHeight();
+
+            try (final Repository repository = RepositoryManager.getRepository()) {
+                List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
+                return new BlockSummariesV2Message(latestBlockSummaries);
+            } catch (DataException e) {
+                return null;
+            }
+        } else {
+            // For older peers
+            BlockData latestBlockData = Controller.getInstance().getChainTip();
+            return new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
+                    latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
+        }
+    }
+
+    public void broadcastOurChain() {
+        BlockData latestBlockData = Controller.getInstance().getChainTip();
+        int latestHeight = latestBlockData.getHeight();
+
+        try (final Repository repository = RepositoryManager.getRepository()) {
+            List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
+            Message latestBlockSummariesMessage = new BlockSummariesV2Message(latestBlockSummaries);
+
+            // For older peers
+            Message heightMessage = new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
+                    latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
+
+            Network.getInstance().broadcast(broadcastPeer -> broadcastPeer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+                    ? latestBlockSummariesMessage
+                    : heightMessage
+            );
+        } catch (DataException e) {
+            LOGGER.warn("Couldn't broadcast our chain tip info", e);
+        }
     }
 
     public Message buildNewTransactionMessage(Peer peer, TransactionData transactionData) {
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index cac0ccc9..a187d29b 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -6,8 +6,8 @@ import com.google.common.net.InetAddresses;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.qortal.controller.Controller;
+import org.qortal.data.block.BlockSummaryData;
 import org.qortal.data.block.CommonBlockData;
-import org.qortal.data.network.PeerChainTipData;
 import org.qortal.data.network.PeerData;
 import org.qortal.network.message.ChallengeMessage;
 import org.qortal.network.message.Message;
@@ -148,7 +148,7 @@ public class Peer {
     /**
      * Latest block info as reported by peer.
      */
-    private PeerChainTipData peersChainTipData;
+    private List<BlockSummaryData> peersChainTipData = Collections.emptyList();
 
     /**
      * Our common block with this peer
@@ -353,28 +353,34 @@ public class Peer {
         }
     }
 
-    public PeerChainTipData getChainTipData() {
-        synchronized (this.peerInfoLock) {
-            return this.peersChainTipData;
-        }
+    public BlockSummaryData getChainTipData() {
+        List<BlockSummaryData> chainTipSummaries = this.peersChainTipData;
+
+        if (chainTipSummaries.isEmpty())
+            return null;
+
+        // Return last entry, which should have greatest height
+        return chainTipSummaries.get(chainTipSummaries.size() - 1);
     }
 
-    public void setChainTipData(PeerChainTipData chainTipData) {
-        synchronized (this.peerInfoLock) {
-            this.peersChainTipData = chainTipData;
-        }
+    public void setChainTipData(BlockSummaryData chainTipData) {
+        this.peersChainTipData = Collections.singletonList(chainTipData);
+    }
+
+    public List<BlockSummaryData> getChainTipSummaries() {
+        return this.peersChainTipData;
+    }
+
+    public void setChainTipSummaries(List<BlockSummaryData> chainTipSummaries) {
+        this.peersChainTipData = List.copyOf(chainTipSummaries);
     }
 
     public CommonBlockData getCommonBlockData() {
-        synchronized (this.peerInfoLock) {
-            return this.commonBlockData;
-        }
+        return this.commonBlockData;
     }
 
     public void setCommonBlockData(CommonBlockData commonBlockData) {
-        synchronized (this.peerInfoLock) {
-            this.commonBlockData = commonBlockData;
-        }
+        this.commonBlockData = commonBlockData;
     }
 
     public boolean isSyncInProgress() {
@@ -904,20 +910,22 @@ public class Peer {
     // Common block data
 
     public boolean canUseCachedCommonBlockData() {
-        PeerChainTipData peerChainTipData = this.getChainTipData();
-        CommonBlockData commonBlockData = this.getCommonBlockData();
+        BlockSummaryData peerChainTipData = this.getChainTipData();
+        if (peerChainTipData == null || peerChainTipData.getSignature() == null)
+            return false;
 
-        if (peerChainTipData != null && commonBlockData != null) {
-            PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData();
-            if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null
-                    && commonBlockChainTipData.getLastBlockSignature() != null) {
-                if (Arrays.equals(peerChainTipData.getLastBlockSignature(),
-                        commonBlockChainTipData.getLastBlockSignature())) {
-                    return true;
-                }
-            }
-        }
-        return false;
+        CommonBlockData commonBlockData = this.getCommonBlockData();
+        if (commonBlockData == null)
+            return false;
+
+        BlockSummaryData commonBlockChainTipData = commonBlockData.getChainTipData();
+        if (commonBlockChainTipData == null || commonBlockChainTipData.getSignature() == null)
+            return false;
+
+        if (!Arrays.equals(peerChainTipData.getSignature(), commonBlockChainTipData.getSignature()))
+            return false;
+
+        return true;
     }
 
 
diff --git a/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java b/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java
new file mode 100644
index 00000000..96c661a4
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java
@@ -0,0 +1,104 @@
+package org.qortal.network.message;
+
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import org.qortal.data.block.BlockSummaryData;
+import org.qortal.transform.Transformer;
+import org.qortal.transform.block.BlockTransformer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BlockSummariesV2Message extends Message {
+
+	public static final long MINIMUM_PEER_VERSION = 0x03000400cbL;
+
+	private static final int BLOCK_SUMMARY_V2_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH /* block signature */
+			+ Transformer.PUBLIC_KEY_LENGTH /* minter public key */
+			+ Transformer.INT_LENGTH /* online accounts count */
+			+ Transformer.LONG_LENGTH /* block timestamp */
+			+ Transformer.INT_LENGTH /* transactions count */
+			+ BlockTransformer.BLOCK_SIGNATURE_LENGTH; /* block reference */
+
+	private List<BlockSummaryData> blockSummaries;
+
+	public BlockSummariesV2Message(List<BlockSummaryData> blockSummaries) {
+		super(MessageType.BLOCK_SUMMARIES_V2);
+
+		// Shortcut for when there are no summaries
+		if (blockSummaries.isEmpty()) {
+			this.dataBytes = Message.EMPTY_DATA_BYTES;
+			return;
+		}
+
+		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+		try {
+			// First summary's height
+			bytes.write(Ints.toByteArray(blockSummaries.get(0).getHeight()));
+
+			for (BlockSummaryData blockSummary : blockSummaries) {
+				bytes.write(blockSummary.getSignature());
+				bytes.write(blockSummary.getMinterPublicKey());
+				bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
+				bytes.write(Longs.toByteArray(blockSummary.getTimestamp()));
+				bytes.write(Ints.toByteArray(blockSummary.getTransactionCount()));
+				bytes.write(blockSummary.getReference());
+			}
+		} catch (IOException e) {
+			throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+		}
+
+		this.dataBytes = bytes.toByteArray();
+		this.checksumBytes = Message.generateChecksum(this.dataBytes);
+	}
+
+	private BlockSummariesV2Message(int id, List<BlockSummaryData> blockSummaries) {
+		super(id, MessageType.BLOCK_SUMMARIES_V2);
+
+		this.blockSummaries = blockSummaries;
+	}
+
+	public List<BlockSummaryData> getBlockSummaries() {
+		return this.blockSummaries;
+	}
+
+	public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+		int height = bytes.getInt();
+
+		// Expecting bytes remaining to be exact multiples of BLOCK_SUMMARY_V2_LENGTH
+		if (bytes.remaining() % BLOCK_SUMMARY_V2_LENGTH != 0)
+			throw new BufferUnderflowException();
+
+		List<BlockSummaryData> blockSummaries = new ArrayList<>();
+		while (bytes.hasRemaining()) {
+			byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
+			bytes.get(signature);
+
+			byte[] minterPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
+			bytes.get(minterPublicKey);
+
+			int onlineAccountsCount = bytes.getInt();
+
+			long timestamp = bytes.getLong();
+
+			int transactionsCount = bytes.getInt();
+
+			byte[] reference = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
+			bytes.get(reference);
+
+			BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey,
+					onlineAccountsCount, timestamp, transactionsCount, reference);
+			blockSummaries.add(blockSummary);
+
+			height++;
+		}
+
+		return new BlockSummariesV2Message(id, blockSummaries);
+	}
+
+}
diff --git a/src/main/java/org/qortal/network/message/GenericUnknownMessage.java b/src/main/java/org/qortal/network/message/GenericUnknownMessage.java
new file mode 100644
index 00000000..15faaa1b
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/GenericUnknownMessage.java
@@ -0,0 +1,23 @@
+package org.qortal.network.message;
+
+import java.nio.ByteBuffer;
+
+public class GenericUnknownMessage extends Message {
+
+    public static final long MINIMUM_PEER_VERSION = 0x03000400cbL;
+
+    public GenericUnknownMessage() {
+        super(MessageType.GENERIC_UNKNOWN);
+
+        this.dataBytes = EMPTY_DATA_BYTES;
+    }
+
+    private GenericUnknownMessage(int id) {
+        super(id, MessageType.GENERIC_UNKNOWN);
+    }
+
+    public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+        return new GenericUnknownMessage(id);
+    }
+
+}
diff --git a/src/main/java/org/qortal/network/message/MessageType.java b/src/main/java/org/qortal/network/message/MessageType.java
index 087e7fbf..4dd4a3c8 100644
--- a/src/main/java/org/qortal/network/message/MessageType.java
+++ b/src/main/java/org/qortal/network/message/MessageType.java
@@ -21,6 +21,7 @@ public enum MessageType {
     HEIGHT_V2(10, HeightV2Message::fromByteBuffer),
     PING(11, PingMessage::fromByteBuffer),
     PONG(12, PongMessage::fromByteBuffer),
+    GENERIC_UNKNOWN(13, GenericUnknownMessage::fromByteBuffer),
 
     // Requesting data
     PEERS_V2(20, PeersV2Message::fromByteBuffer),
@@ -41,6 +42,7 @@ public enum MessageType {
 
     BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
     GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
+    BLOCK_SUMMARIES_V2(72, BlockSummariesV2Message::fromByteBuffer),
 
     ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
     GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
index cc7e1611..c3c5638a 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
@@ -143,13 +143,17 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
                 byte[] blockMinterPublicKey = resultSet.getBytes(3);
 
                 // Fetch additional info from the archive itself
-                int onlineAccountsCount = 0;
+                Integer onlineAccountsCount = null;
+                Long timestamp = null;
+                Integer transactionCount = null;
+                byte[] reference = null;
+
                 BlockData blockData = this.fromSignature(signature);
                 if (blockData != null) {
                     onlineAccountsCount = blockData.getOnlineAccountsCount();
                 }
 
-                BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
+                BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount, timestamp, transactionCount, reference);
                 blockSummaries.add(blockSummary);
             } while (resultSet.next());
 
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
index b8238085..f38d549c 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
@@ -297,7 +297,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
 	@Override
 	public List<BlockSummaryData> getBlockSummariesBySigner(byte[] signerPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
 		StringBuilder sql = new StringBuilder(512);
-		sql.append("SELECT signature, height, Blocks.minter, online_accounts_count FROM ");
+		sql.append("SELECT signature, height, Blocks.minter, online_accounts_count, minted_when, transaction_count, Blocks.reference FROM ");
 
 		// List of minter account's public key and reward-share public keys with minter's public key
 		sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
@@ -322,8 +322,12 @@ public class HSQLDBBlockRepository implements BlockRepository {
 				int height = resultSet.getInt(2);
 				byte[] blockMinterPublicKey = resultSet.getBytes(3);
 				int onlineAccountsCount = resultSet.getInt(4);
+				long timestamp = resultSet.getLong(5);
+				int transactionCount = resultSet.getInt(6);
+				byte[] reference = resultSet.getBytes(7);
 
-				BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
+				BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount,
+						timestamp, transactionCount, reference);
 				blockSummaries.add(blockSummary);
 			} while (resultSet.next());
 
@@ -355,7 +359,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
 
 	@Override
 	public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
-		String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count "
+		String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "
 				+ "FROM Blocks WHERE height BETWEEN ? AND ?";
 
 		List<BlockSummaryData> blockSummaries = new ArrayList<>();
@@ -371,9 +375,10 @@ public class HSQLDBBlockRepository implements BlockRepository {
 				int onlineAccountsCount = resultSet.getInt(4);
 				long timestamp = resultSet.getLong(5);
 				int transactionCount = resultSet.getInt(6);
+				byte[] reference = resultSet.getBytes(7);
 
 				BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
-						timestamp, transactionCount);
+						timestamp, transactionCount, reference);
 				blockSummaries.add(blockSummary);
 			} while (resultSet.next());