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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ local.properties
#.project

.DS_Store

*.args
27 changes: 20 additions & 7 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,15 @@
<optional>true</optional>
</dependency>

<!-- Default crypto backend: pure-Java Bouncy Castle (see io.bosonnetwork.crypto.CryptoProvider) -->
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
</dependency>
<dependency>
<groupId>io.tmio</groupId>
<artifactId>tuweni-crypto</artifactId>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<scope>test</scope>
</dependency>

<dependency>
Expand Down Expand Up @@ -163,6 +159,23 @@
<scope>test</scope>
</dependency>

<!--
libsodium binding (Apache Tuweni / JNR). Test scope only: it backs the
SodiumCryptoProvider used by the crypto compatibility tests to verify that the
Bouncy Castle backend stays byte-for-byte compatible with libsodium. Not shipped
in the production distribution.
-->
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.tmio</groupId>
<artifactId>tuweni-crypto</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
Expand Down
6 changes: 6 additions & 0 deletions api/src/main/java/io/bosonnetwork/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ default CompletableFuture<Optional<NodeInfo>> findNode(Id id) {

/**
* Finds a node by its ID with a specific lookup option.
* <p>
* When present, the returned {@link NodeInfo} records which address families answered the lookup:
* {@link NodeInfo#hasAddress4()} and {@link NodeInfo#hasAddress6()} are true only for the families
* that contributed a result. A dual-stack node that responded over a single family therefore yields
* a single-address {@link NodeInfo} (a {@link LookupOption#CONSERVATIVE} lookup queries both families
* and still succeeds with a partial result if only one responds).
*
* @param id the {@link Id} of the node to find
* @param option the {@link LookupOption} to use
Expand Down
124 changes: 73 additions & 51 deletions api/src/main/java/io/bosonnetwork/NodeInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Objects;

import org.jspecify.annotations.Nullable;
Expand All @@ -42,17 +43,15 @@
* {@link #getIpAddress()}) prefer the IPv4 address and fall back to IPv6; use the family-specific
* accessors to target a particular protocol family.
* <p>
* The id and addresses are immutable and define {@link #equals(Object)}/{@link #hashCode()}; the
* version and the default protocol family ({@link #narrowDown(StandardProtocolFamily)}) are mutable
* and excluded from equality. Instances are not thread-safe for the mutable fields; callers that
* share an instance across threads should treat it as effectively immutable.
* Instances are immutable: the id and addresses define {@link #equals(Object)}/{@link #hashCode()},
* and the preferred protocol family is fixed at construction. {@link #narrowDown(StandardProtocolFamily)}
* returns a new instance rather than mutating. Immutable instances are safe to share across threads.
*/
public class NodeInfo {
private final Id id;
private final @Nullable InetSocketAddress addr4;
private final @Nullable InetSocketAddress addr6;
private int version;
private @Nullable StandardProtocolFamily defaultProtocolFamily;
private final StandardProtocolFamily defaultProtocolFamily;

private NodeInfo(Id id, @Nullable InetSocketAddress sockAddr4, @Nullable InetSocketAddress sockAddr6) {
Objects.requireNonNull(id, "id");
Expand Down Expand Up @@ -84,6 +83,12 @@ private NodeInfo(Id id, @Nullable InetSocketAddress sockAddr4, @Nullable InetSoc
this.defaultProtocolFamily = sockAddr4 != null ? StandardProtocolFamily.INET : StandardProtocolFamily.INET6;
}

/**
* Construct a {@code NodeInfo} object from a single socket address.
*
* @param id the node id.
* @param sockAddr the node socket address, can be IPv4 or IPv6.
*/
protected NodeInfo(Id id, InetSocketAddress sockAddr) {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(sockAddr, "sockAddr");
Expand Down Expand Up @@ -115,7 +120,6 @@ protected NodeInfo(NodeInfo ni) {
this.id = ni.id;
this.addr4 = ni.addr4;
this.addr6 = ni.addr6;
this.version = ni.version;
this.defaultProtocolFamily = ni.defaultProtocolFamily;
}

Expand All @@ -124,6 +128,7 @@ protected NodeInfo(NodeInfo ni) {
*
* @param id the node id.
* @param sockAddr the node socket address, can be IPv4 or IPv6.
* @return the constructed {@code NodeInfo}.
*/
public static NodeInfo of(Id id, InetSocketAddress sockAddr) {
return new NodeInfo(id, sockAddr);
Expand All @@ -135,6 +140,7 @@ public static NodeInfo of(Id id, InetSocketAddress sockAddr) {
* @param id the node id.
* @param inetAddr the node IP address, can be IPv4 or IPv6.
* @param port the node port number.
* @return the constructed {@code NodeInfo}.
*/
public static NodeInfo of(Id id, InetAddress inetAddr, int port) {
Objects.requireNonNull(id, "id");
Expand All @@ -148,6 +154,7 @@ public static NodeInfo of(Id id, InetAddress inetAddr, int port) {
* @param id the node id.
* @param host the node host name or address string.
* @param port the node port number.
* @return the constructed {@code NodeInfo}.
*/
public static NodeInfo of(Id id, String host, int port) {
Objects.requireNonNull(id, "id");
Expand All @@ -161,6 +168,7 @@ public static NodeInfo of(Id id, String host, int port) {
* @param id the node id.
* @param inetAddr the node raw IP address, can be IPv4 or IPv6.
* @param port the node port number.
* @return the constructed {@code NodeInfo}.
*/
public static NodeInfo of(Id id, byte[] inetAddr, int port) {
Objects.requireNonNull(id, "id");
Expand All @@ -178,6 +186,7 @@ public static NodeInfo of(Id id, byte[] inetAddr, int port) {
* @param id the node id.
* @param sockAddr4 the IPv4 socket address, can be null.
* @param sockAddr6 the IPv6 socket address, can be null.
* @return the constructed {@code NodeInfo}.
* @throws IllegalArgumentException if both addresses are null, or if the port is invalid.
*/
public static NodeInfo of(Id id, @Nullable InetSocketAddress sockAddr4, @Nullable InetSocketAddress sockAddr6) {
Expand All @@ -192,6 +201,7 @@ public static NodeInfo of(Id id, @Nullable InetSocketAddress sockAddr4, @Nullabl
* @param port4 the IPv4 port number, ignored if {@code inetAddr4} is null.
* @param inetAddr6 the IPv6 address, can be null.
* @param port6 the IPv6 port number, ignored if {@code inetAddr6} is null.
* @return the constructed {@code NodeInfo}.
* @throws IllegalArgumentException if both addresses are null, or if an address/port is invalid.
*/
public static NodeInfo of(Id id, @Nullable InetAddress inetAddr4, int port4, @Nullable InetAddress inetAddr6, int port6) {
Expand Down Expand Up @@ -230,6 +240,7 @@ public static NodeInfo of(Id id, @Nullable InetAddress inetAddr4, int port4, @Nu
* @param port4 the IPv4 port number, ignored if {@code host4} is null.
* @param host6 the IPv6 host name or address string, can be null.
* @param port6 the IPv6 port number, ignored if {@code host6} is null.
* @return the constructed {@code NodeInfo}.
* @throws IllegalArgumentException if both hosts are null, or if an address/port is invalid.
*/
public static NodeInfo of(Id id, @Nullable String host4, int port4, @Nullable String host6, int port6) {
Expand Down Expand Up @@ -268,6 +279,7 @@ public static NodeInfo of(Id id, @Nullable String host4, int port4, @Nullable St
* @param port4 the IPv4 port number, ignored if {@code inetAddr4} is null.
* @param inetAddr6 the raw IPv6 address bytes, can be null.
* @param port6 the IPv6 port number, ignored if {@code inetAddr6} is null.
* @return the constructed {@code NodeInfo}.
* @throws IllegalArgumentException if both addresses are null, or if an address/port is invalid.
*/
public static NodeInfo of(Id id, byte @Nullable [] inetAddr4, int port4, byte @Nullable [] inetAddr6, int port6) {
Expand Down Expand Up @@ -322,27 +334,33 @@ public Id getId() {
}

/**
* Narrow the node down to a single protocol family, making the given family the one returned by
* the generic accessors ({@link #getAddress()}, {@link #getHost()}, {@link #getPort()}, etc.).
* Returns a view of this node narrowed to a single protocol family, dropping any address of the
* other family. The returned node carries only the requested family's address, so its generic
* accessors unambiguously refer to that family and it compares equal only to other single-family
* nodes with the same id and address. If this node already has only the requested family, it is
* returned unchanged.
*
* @param family the protocol family to make default; the node must have an address for it.
* @param family the protocol family to keep (INET or INET6); the node must have an address for it.
* @return a single-address {@code NodeInfo} for the requested family.
* @throws IllegalStateException if no address of the requested family is available.
* @throws IllegalArgumentException if the family is not INET or INET6.
*/
public void narrowDown(StandardProtocolFamily family) {
switch (family) {
case INET -> {
if (addr4 == null)
throw new IllegalStateException("No IPv4 address is available");
}
case INET6 -> {
if (addr6 == null)
throw new IllegalStateException("No IPv6 address is available");
}
public NodeInfo narrowDown(StandardProtocolFamily family) {
InetSocketAddress addr = switch (family) {
case INET -> addr4;
case INET6 -> addr6;
default -> throw new IllegalArgumentException("Unsupported protocol family: " + family);
}
};

if (addr == null)
throw new IllegalStateException("No " +
(family == StandardProtocolFamily.INET ? "IPv4" : "IPv6") + " address is available");

this.defaultProtocolFamily = family;
// Already single-family (of the requested family, since its address is present): share it.
if (!hasMultiAddresses())
return this;

return new NodeInfo(id, addr);
}

/**
Expand Down Expand Up @@ -388,15 +406,43 @@ public boolean hasMultiAddresses() {
}

/**
* Gets the socket address of the node.
* Returns the IPv4 address if available, otherwise returns the IPv6 address.
* Returns the protocol family used by the generic accessors ({@link #getAddress()},
* {@link #getHost()}, {@link #getPort()}, {@link #getIpAddress()}). For a dual-stack node this is
* IPv4 by default; for a single-stack node it is the only available family.
*
* @return the preferred protocol family (INET or INET6).
*/
public StandardProtocolFamily getPreferredFamily() {
return defaultProtocolFamily;
}

/**
* Retrieves a list of network addresses, including both IPv4 and IPv6 addresses, if available.
*
* @return a list of InetSocketAddress objects containing the available network addresses.
* The list may include both IPv4 and IPv6 addresses, only IPv4 addresses,
* only IPv6 addresses, or be empty if no addresses are available.
*/
public List<InetSocketAddress> getAddresses() {
if (addr4 != null && addr6 != null)
return List.of(addr4, addr6);
else if (addr4 != null)
return List.of(addr4);
else if (addr6 != null)
return List.of(addr6);
else
return List.of();
}

/**
* Gets the socket address of the node for the {@linkplain #getPreferredFamily() preferred family}.
* For a dual-stack node this is the IPv4 address; for a single-stack node it is the only available
* address. Use {@link #getAddress4()}/{@link #getAddress6()} or {@link #getAddress(StandardProtocolFamily)}
* to target a specific family.
*
* @return the socket address.
* @throws IllegalStateException if no address is available.
*/
public InetSocketAddress getAddress() {
if (defaultProtocolFamily == null)
throw new IllegalStateException("No default protocol family is set");
return Objects.requireNonNull(getAddress(defaultProtocolFamily));
}

Expand Down Expand Up @@ -440,8 +486,6 @@ public InetSocketAddress getAddress() {
* @return the IP address.
*/
public InetAddress getIpAddress() {
if (defaultProtocolFamily == null)
throw new IllegalStateException("No default protocol family is set");
return Objects.requireNonNull(getIpAddress(defaultProtocolFamily));
}

Expand Down Expand Up @@ -486,8 +530,6 @@ public InetAddress getIpAddress() {
* @return the host name or string of IP address.
*/
public String getHost() {
if (defaultProtocolFamily == null)
throw new IllegalStateException("No default protocol family is set");
return Objects.requireNonNull(getHost(defaultProtocolFamily));
}

Expand Down Expand Up @@ -534,8 +576,6 @@ public String getHost() {
* @return the port number.
*/
public int getPort() {
if (defaultProtocolFamily == null)
throw new IllegalStateException("No default protocol family is set");
return getPort(defaultProtocolFamily);
}

Expand Down Expand Up @@ -572,24 +612,6 @@ public int getPort6() {
return addr6 != null ? addr6.getPort() : -1;
}

/**
* Sets the node version number.
*
* @param version the version number.
*/
public void setVersion(int version) {
this.version = version;
}

/**
* Gets the node version.
*
* @return the version number.
*/
public int getVersion() {
return version;
}

/**
* Checks whether this node info conflicts with another, i.e.; they share the same id
* <em>or</em> the same socket address. This is a partial match used to detect identity/address
Expand Down
Loading
Loading