Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions QORTIUM-CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ own chain.

## Change Entries

### 2026-06-02 - Expose preview peer reachability diagnostics

Added inbound and outbound peer counts plus P2P and QDN reachability flags to the node status response so preview testers can tell the difference between outbound-only connectivity and public reachability. Preview participants now target four outbound chain peers when enough reachable peers are available, and the preview docs explain how firewall or router forwarding affects peer counts.

### 2026-06-01 - build(deps-dev): bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.5 to 3.5.6

Updated the Maven Surefire Plugin to the current patch release so Qortium's JUnit test runner stays current. This keeps the existing Maven test workflow and skip flags intact while picking up upstream Surefire maintenance fixes and improvements.
Expand Down
6 changes: 6 additions & 0 deletions preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ online accounts before activating so small preview groups can exercise reward
distribution sooner. Chain-parameter updates use a 20-block activation delay on
preview so governance changes can be tested within the same session.

Participant nodes target four outbound chain peers when enough reachable peers
are known. Nodes that do not forward inbound ports can still sync and publish
outbound, but they may not receive inbound peer connections from other testers.
The `/admin/status` response reports inbound/outbound peer counts and P2P/QDN
reachability flags to make that distinction visible during preview testing.

## Start A Preview Participant

Build Qortium from the repository root:
Expand Down
11 changes: 11 additions & 0 deletions preview/TESTER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ preview\reset.bat
genesis, and settle into the current preview height.
- The first useful tests are connecting to the seed nodes, staying synced, sending
chat messages, trying QDN features, and reporting issues.
- Preview participant nodes try to keep four outbound chain peers when enough
reachable peers are available. Nodes behind a firewall or router can still use
preview normally, but they may only show outbound peers unless other nodes can
connect back to them.
- Preview participant and seed nodes expose limited public read-only API access
by default so Qortium Home can discover useful peers, browse QDN resources,
and read common chain data. Your local node still keeps transaction building,
Expand All @@ -156,6 +160,13 @@ and `24894` for QDN/data. If you do not open those ports, your node can still
use the preview network normally, but other users may not be able to use it as a
public read or QDN peer.

The `/admin/status` response includes directional peer counts and inbound
reachability fields. If `numberOfOutboundConnections` is healthy but
`numberOfInboundConnections` stays at `0`, your node is probably connected
outbound but not publicly reachable for P2P. If `isP2PInboundReachable` or
`isQDNInboundReachable` is `false`, check firewall or router forwarding for the
matching preview port.

The public seed status endpoints are:

```text
Expand Down
2 changes: 1 addition & 1 deletion preview/settings-preview.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dataPath": "data-preview",
"blockchainConfig": "previewchain.json",
"minBlockchainPeers": 1,
"minOutboundPeers": 2,
"minOutboundPeers": 4,
"maxPeers": 32,
"maxDataPeers": 32,
"maxNetworkThreadPoolSize": 128,
Expand Down
117 changes: 113 additions & 4 deletions src/main/java/org/qortium/api/model/NodeStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.qortium.network.Network;
import org.qortium.network.NetworkData;
import org.qortium.network.Peer;
import org.qortium.network.PeerList;
import org.qortium.settings.Settings;

import javax.xml.bind.annotation.XmlAccessType;
Expand Down Expand Up @@ -54,21 +55,90 @@ public enum SyncPhase {

public final int numberOfConnections;

@Schema(
description = "Number of handshaked chain peers that connected inbound to this node."
)
public final int numberOfInboundConnections;

@Schema(
description = "Number of handshaked chain peers this node connected to outbound."
)
public final int numberOfOutboundConnections;

public final int numberOfDataConnections;

@Schema(
description = "Number of handshaked QDN/data peers that connected inbound to this node."
)
public final int numberOfInboundDataConnections;

@Schema(
description = "Number of handshaked QDN/data peers this node connected to outbound."
)
public final int numberOfOutboundDataConnections;

@Schema(
description = "Whether this node currently appears reachable for inbound chain peer connections."
)
public final boolean isP2PInboundReachable;

@Schema(
description = "Whether the chain peer listen socket is bound locally."
)
public final boolean isP2PListenSocketAvailable;

@Schema(
description = "Whether the chain peer listen port was mapped through UPnP."
)
public final boolean isP2PPortMapped;

@Schema(
description = "Whether this node currently appears reachable for inbound QDN/data peer connections."
)
public final boolean isQDNInboundReachable;

@Schema(
description = "Whether the QDN/data listen socket is bound locally."
)
public final boolean isQDNListenSocketAvailable;

@Schema(
description = "Whether the QDN/data listen port was mapped through UPnP."
)
public final boolean isQDNPortMapped;

public final int height;

public NodeStatus() {
this.isMintingPossible = OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures();

Synchronizer synchronizer = Synchronizer.getInstance();
Controller controller = Controller.getInstance();
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
Network network = Network.getInstance();
NetworkData networkData = NetworkData.getInstance();
List<Peer> handshakedPeers = network.getImmutableHandshakedPeers();
PeerList handshakedDataPeers = networkData.getImmutableHandshakedPeers();
PeerConnectionStats chainPeerStats = calculatePeerConnectionStats(handshakedPeers.size(),
countOutboundConnections(handshakedPeers), network.canAcceptInbound(), network.isListenSocketAvailable(),
network.isPortMapped());
PeerConnectionStats dataPeerStats = calculatePeerConnectionStats(handshakedDataPeers.size(),
countOutboundConnections(handshakedDataPeers), networkData.canAcceptInbound(),
networkData.isListenSocketAvailable(), networkData.isPortMapped());

this.isSynchronizing = synchronizer.isSynchronizing();
this.numberOfConnections = handshakedPeers.size();

this.numberOfDataConnections = NetworkData.getInstance().getImmutableHandshakedPeers().size();
this.numberOfConnections = chainPeerStats.totalConnections;
this.numberOfInboundConnections = chainPeerStats.inboundConnections;
this.numberOfOutboundConnections = chainPeerStats.outboundConnections;

this.numberOfDataConnections = dataPeerStats.totalConnections;
this.numberOfInboundDataConnections = dataPeerStats.inboundConnections;
this.numberOfOutboundDataConnections = dataPeerStats.outboundConnections;
this.isP2PInboundReachable = chainPeerStats.inboundReachable;
this.isP2PListenSocketAvailable = chainPeerStats.listenSocketAvailable;
this.isP2PPortMapped = chainPeerStats.portMapped;
this.isQDNInboundReachable = dataPeerStats.inboundReachable;
this.isQDNListenSocketAvailable = dataPeerStats.listenSocketAvailable;
this.isQDNPortMapped = dataPeerStats.portMapped;

this.height = controller.getChainHeight();

Expand Down Expand Up @@ -125,6 +195,16 @@ public static SyncProgress calculateSyncProgress(int height, Integer activeSyncT
return new SyncProgress(syncTargetHeight, syncBlocksRemaining, syncPercent, syncPhase);
}

public static PeerConnectionStats calculatePeerConnectionStats(int totalConnections, int outboundConnections,
boolean inboundReachable, boolean listenSocketAvailable, boolean portMapped) {
int boundedTotalConnections = Math.max(0, totalConnections);
int boundedOutboundConnections = Math.max(0, Math.min(outboundConnections, boundedTotalConnections));
int inboundConnections = boundedTotalConnections - boundedOutboundConnections;

return new PeerConnectionStats(boundedTotalConnections, inboundConnections, boundedOutboundConnections,
inboundReachable, listenSocketAvailable, portMapped);
}

private static Integer chooseSyncTargetHeight(int height, Integer activeSyncTargetHeight, Integer bestPeerHeight,
boolean isUpToDate) {
Integer syncTargetHeight = null;
Expand Down Expand Up @@ -163,6 +243,35 @@ private static Integer getBestPeerHeight(List<Peer> peers) {
return bestPeerHeight;
}

private static int countOutboundConnections(Iterable<Peer> peers) {
int outboundConnections = 0;

for (Peer peer : peers)
if (peer.isOutbound())
outboundConnections++;

return outboundConnections;
}

public static class PeerConnectionStats {
public final int totalConnections;
public final int inboundConnections;
public final int outboundConnections;
public final boolean inboundReachable;
public final boolean listenSocketAvailable;
public final boolean portMapped;

private PeerConnectionStats(int totalConnections, int inboundConnections, int outboundConnections,
boolean inboundReachable, boolean listenSocketAvailable, boolean portMapped) {
this.totalConnections = totalConnections;
this.inboundConnections = inboundConnections;
this.outboundConnections = outboundConnections;
this.inboundReachable = inboundReachable;
this.listenSocketAvailable = listenSocketAvailable;
this.portMapped = portMapped;
}
}

public static class SyncProgress {
public final Integer syncTargetHeight;
public final Integer syncBlocksRemaining;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/qortium/network/Network.java
Original file line number Diff line number Diff line change
Expand Up @@ -2470,15 +2470,15 @@ public boolean canAcceptInbound() {
return this.inboundReachability.canAcceptInbound();
}

boolean isListenSocketAvailable() {
public boolean isListenSocketAvailable() {
return this.inboundReachability.isListenSocketAvailable();
}

boolean isPortMapped() {
public boolean isPortMapped() {
return this.inboundReachability.isPortMapped();
}

long getLastInboundHandshakeTimestamp() {
public long getLastInboundHandshakeTimestamp() {
return this.inboundReachability.getLastInboundHandshakeTimestamp();
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/qortium/network/NetworkData.java
Original file line number Diff line number Diff line change
Expand Up @@ -2118,15 +2118,15 @@ public boolean canAcceptInbound() {
return this.inboundReachability.canAcceptInbound();
}

boolean isListenSocketAvailable() {
public boolean isListenSocketAvailable() {
return this.inboundReachability.isListenSocketAvailable();
}

boolean isPortMapped() {
public boolean isPortMapped() {
return this.inboundReachability.isPortMapped();
}

long getLastInboundHandshakeTimestamp() {
public long getLastInboundHandshakeTimestamp() {
return this.inboundReachability.getLastInboundHandshakeTimestamp();
}

Expand Down
27 changes: 27 additions & 0 deletions src/test/java/org/qortium/test/api/NodeStatusTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import org.junit.Test;
import org.qortium.api.model.NodeStatus;
import org.qortium.api.model.NodeStatus.PeerConnectionStats;
import org.qortium.api.model.NodeStatus.SyncPhase;
import org.qortium.api.model.NodeStatus.SyncProgress;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class NodeStatusTests {

Expand Down Expand Up @@ -80,4 +83,28 @@ public void testSyncedNodeReportsComplete() {
assertEquals(SyncPhase.SYNCED, progress.syncPhase);
}

@Test
public void testPeerConnectionStatsSplitInboundAndOutbound() {
PeerConnectionStats stats = NodeStatus.calculatePeerConnectionStats(5, 2, true, true, false);

assertEquals(5, stats.totalConnections);
assertEquals(3, stats.inboundConnections);
assertEquals(2, stats.outboundConnections);
assertTrue(stats.inboundReachable);
assertTrue(stats.listenSocketAvailable);
assertFalse(stats.portMapped);
}

@Test
public void testPeerConnectionStatsBoundsInvalidCounts() {
PeerConnectionStats stats = NodeStatus.calculatePeerConnectionStats(2, 5, false, true, true);

assertEquals(2, stats.totalConnections);
assertEquals(0, stats.inboundConnections);
assertEquals(2, stats.outboundConnections);
assertFalse(stats.inboundReachable);
assertTrue(stats.listenSocketAvailable);
assertTrue(stats.portMapped);
}

}