diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml
new file mode 100644
index 00000000..111dbbcc
--- /dev/null
+++ b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml
@@ -0,0 +1,24 @@
+
+
+ io.reticulum
+ reticulum-network-stack
+ 1.0-SNAPSHOT
+
+
+ true
+
+ 20240324170649
+
+
+ jar
+ 1.0-SNAPSHOT
+ 20240324170649
+
+
+ pom
+ 1.0-SNAPSHOT
+ 20240324170649
+
+
+
+
diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar
new file mode 100644
index 00000000..7612e6ad
Binary files /dev/null and b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar differ
diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom
new file mode 100644
index 00000000..1b1cc206
--- /dev/null
+++ b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom
@@ -0,0 +1,9 @@
+
+
+ 4.0.0
+ io.reticulum
+ reticulum-network-stack
+ 1.0-SNAPSHOT
+ POM was created from install:install-file
+
diff --git a/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml b/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml
new file mode 100644
index 00000000..6ff327fb
--- /dev/null
+++ b/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml
@@ -0,0 +1,11 @@
+
+
+ io.reticulum
+ reticulum-network-stack
+
+
+ 1.0-SNAPSHOT
+
+ 20240324170649
+
+
diff --git a/pom.xml b/pom.xml
index bbc044a2..28061bbb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,6 +6,10 @@
4.5.1
jar
+
true
7dc8c6f
0.15.10
@@ -56,6 +60,8 @@
1.2
2.16.2
1.9
+ 1.18.30
+ 2.14.3
src/main/java
@@ -103,6 +109,14 @@
${project.build.directory}/swagger-ui.unpacked
+
@@ -373,6 +387,11 @@
${maven-surefire-plugin.version}
${skipTests}
+
+
+ --add-opens=java.base/java.lang=ALL-UNNAMED
+ --add-opens=java.base/java.util=ALL-UNNAMED
+
@@ -441,6 +460,10 @@
jitpack.io
https://jitpack.io
+
+ true
+ always
+
@@ -780,5 +803,95 @@
jaxb-runtime
${jaxb-runtime.version}
+
+ io.reticulum
+ reticulum-network-stack
+ 1.0-SNAPSHOT
+
+
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ commons-codec
+ commons-codec
+ 1.15
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
+
+ org.msgpack
+ jackson-dataformat-msgpack
+ 0.9.3
+
+
+
+
+
+ io.netty
+ netty-all
+ 4.1.92.Final
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ 1.69
+
+
+ com.macasaet.fernet
+ fernet-java8
+ 1.4.2
+
+
+ org.apache.commons
+ commons-compress
+ 1.25.0
+
+
+ com.igormaznitsa
+ jbbp
+ 2.0.4
+
+
+ com.github.seancfoley
+ ipaddress
+ 5.4.0
+
+
+ org.msgpack
+ msgpack-core
+ 0.9.6
+
+
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 6d2562ab..c1e0e279 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -31,6 +31,7 @@ import org.qortal.globalization.Translator;
import org.qortal.gui.Gui;
import org.qortal.gui.SysTray;
import org.qortal.network.Network;
+import org.qortal.network.RNSNetwork;
import org.qortal.network.Peer;
import org.qortal.network.message.*;
import org.qortal.repository.*;
@@ -115,6 +116,7 @@ public class Controller extends Thread {
private long repositoryCheckpointTimestamp = startTime; // ms
private long prunePeersTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
+ private long pruneRNSPeersTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
/** Whether we can mint new blocks, as reported by BlockMinter. */
@@ -481,6 +483,15 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error
}
+ LOGGER.info("Starting Reticulum");
+ try {
+ RNSNetwork rns = RNSNetwork.getInstance();
+ rns.start();
+ LOGGER.debug("Reticulum instance: {}", rns.toString());
+ } catch (IOException | DataException e) {
+ LOGGER.error("Unable to start Reticulum", e);
+ }
+
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
@@ -582,6 +593,8 @@ public class Controller extends Thread {
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
+ //final long pruneRNSPeersInterval = 5 * 60 * 1000L; // Every 5 minutes
+ final long pruneRNSPeersInterval = 1 * 60 * 1000L; // Every 1 minute (during development)
// Start executor service for trimming or pruning
PruneManager.getInstance().start();
@@ -690,6 +703,18 @@ public class Controller extends Thread {
}
}
+ // Q: Do we need global pruning?
+ if (now >= pruneRNSPeersTimestamp + pruneRNSPeersInterval) {
+ pruneRNSPeersTimestamp = now + pruneRNSPeersInterval;
+
+ try {
+ LOGGER.debug("Pruning Reticulum peers...");
+ RNSNetwork.getInstance().prunePeers();
+ } catch (DataException e) {
+ LOGGER.warn(String.format("Repository issue when trying to prune Reticulum peers: %s", e.getMessage()));
+ }
+ }
+
// Delete expired transactions
if (now >= deleteExpiredTimestamp) {
deleteExpiredTimestamp = now + DELETE_EXPIRED_INTERVAL;
@@ -988,6 +1013,9 @@ public class Controller extends Thread {
LOGGER.info("Shutting down networking");
Network.getInstance().shutdown();
+ LOGGER.info("Shutting down Reticulum");
+ RNSNetwork.getInstance().shutdown();
+
LOGGER.info("Shutting down controller");
this.interrupt();
try {
diff --git a/src/main/java/org/qortal/network/RNSNetwork.java b/src/main/java/org/qortal/network/RNSNetwork.java
new file mode 100644
index 00000000..2472c891
--- /dev/null
+++ b/src/main/java/org/qortal/network/RNSNetwork.java
@@ -0,0 +1,425 @@
+package org.qortal.network;
+
+import java.io.IOException;
+//import java.nio.channels.SelectionKey;
+//import java.io.Paths;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.io.File;
+import java.util.*;
+//import java.util.function.BiConsumer;
+//import java.util.function.Consumer;
+//import java.util.function.Function;
+//import java.util.concurrent.*;
+//import java.util.concurrent.atomic.AtomicLong;
+
+//import org.qortal.data.network.PeerData;
+import org.qortal.repository.DataException;
+//import org.qortal.settings.Settings;
+import org.qortal.settings.Settings;
+//import org.qortal.utils.NTP;
+
+//import com.fasterxml.jackson.annotation.JsonGetter;
+
+import org.apache.commons.codec.binary.Hex;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+import io.reticulum.Reticulum;
+import io.reticulum.Transport;
+import io.reticulum.destination.Destination;
+import io.reticulum.destination.DestinationType;
+import io.reticulum.destination.Direction;
+import io.reticulum.identity.Identity;
+import io.reticulum.interfaces.ConnectionInterface;
+import io.reticulum.destination.ProofStrategy;
+import io.reticulum.transport.AnnounceHandler;
+import static io.reticulum.constant.ReticulumConstant.CONFIG_FILE_NAME;
+//import static io.reticulum.identity.IdentityKnownDestination.recall;
+//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
+//import static io.reticulum.destination.Direction.OUT;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.Synchronized;
+import io.reticulum.link.Link;
+import io.reticulum.link.LinkStatus;
+//import io.reticulum.packet.PacketReceipt;
+import io.reticulum.packet.Packet;
+
+//import static io.reticulum.link.LinkStatus.ACTIVE;
+import static io.reticulum.link.LinkStatus.CLOSED;
+import static io.reticulum.link.LinkStatus.PENDING;
+import static io.reticulum.link.LinkStatus.STALE;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+//import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+
+//import org.qortal.network.Network.NetworkProcessor;
+//import org.qortal.utils.ExecuteProduceConsume;
+//import org.qortal.utils.NamedThreadFactory;
+
+//import java.time.Instant;
+
+//import org.qortal.network.RNSPeer;
+
+@Slf4j
+public class RNSNetwork {
+
+ static final String APP_NAME = "qortal";
+ private Reticulum reticulum;
+ private Identity server_identity;
+ private Destination baseDestination; // service base (initially: anything node2node)
+ //private Destination dataDestination; // qdn services (eg. files like music, videos etc)
+ //private Destination liveDestination; // live/dynamic peer list (eg. video conferencing)
+ // the following should be retrieved from settings
+ private static Integer MAX_PEERS = 3;
+ private static Integer MIN_DESIRED_PEERS = 3;
+ //private final Integer MAX_PEERS = Settings.getInstance().getMaxReticulumPeers();
+ //private final Integer MIN_DESIRED_PEERS = Settings.getInstance().getMinDesiredReticulumPeers();
+ static final String defaultConfigPath = new String(".reticulum"); // if empty will look in Reticulums default paths
+ //private final String defaultConfigPath = Settings.getInstance().getDefaultConfigPathForReticulum();
+
+ //private static final Logger logger = LoggerFactory.getLogger(RNSNetwork.class);
+
+ //private final List linkedPeers = Collections.synchronizedList(new ArrayList<>());
+ //private List immutableLinkedPeers = Collections.emptyList();
+ private final List linkedPeers = Collections.synchronizedList(new ArrayList<>());
+
+ //private final ExecuteProduceConsume rnsNetworkEPC;
+ private static final long NETWORK_EPC_KEEPALIVE = 1000L; // 1 second
+ private volatile boolean isShuttingDown = false;
+ private int totalThreadCount = 0;
+
+ // TODO: settings - MaxReticulumPeers, MaxRNSNetworkThreadPoolSize (if needed)
+
+ // Constructor
+ private RNSNetwork () {
+ try {
+ initConfig(defaultConfigPath);
+ reticulum = new Reticulum(defaultConfigPath);
+ log.info("reticulum instance created: {}", reticulum.toString());
+ } catch (IOException e) {
+ log.error("unable to create Reticulum network", e);
+ }
+
+ // Settings.getInstance().getMaxRNSNetworkThreadPoolSize(), // statically set to 5 below
+ //ExecutorService RNSNetworkExecutor = new ThreadPoolExecutor(1,
+ // 5,
+ // NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
+ // new SynchronousQueue(),
+ // new NamedThreadFactory("RNSNetwork-EPC"));
+ //rnsNetworkEPC = new RNSNetworkProcessor(RNSNetworkExecutor);
+ }
+
+ // Note: potentially create persistent server_identity (utility rnid) and load it from file
+ public void start() throws IOException, DataException {
+
+ // create identity either from file or new (creating new keys)
+ var serverIdentityPath = reticulum.getStoragePath().resolve(APP_NAME);
+ if (Files.isReadable(serverIdentityPath)) {
+ server_identity = Identity.fromFile(serverIdentityPath);
+ log.info("server identity loaded from file {}", serverIdentityPath.toString());
+ } else {
+ server_identity = new Identity();
+ log.info("new server identity created dynamically.");
+ }
+ log.debug("Server Identity: {}", server_identity.toString());
+
+ // show the ifac_size of the configured interfaces (debug code)
+ for (ConnectionInterface i: Transport.getInstance().getInterfaces() ) {
+ log.info("interface {}, length: {}", i.getInterfaceName(), i.getIfacSize());
+ }
+
+ baseDestination = new Destination(
+ server_identity,
+ Direction.IN,
+ DestinationType.SINGLE,
+ APP_NAME,
+ "core"
+ );
+ //// ideas for other entry points
+ //dataDestination = new Destination(
+ // server_identity,
+ // Direction.IN,
+ // DestinationType.SINGLE,
+ // APP_NAME,
+ // "core",
+ // "qdn"
+ //);
+ //liveDestination = new Destination(
+ // server_identity,
+ // Direction.IN,
+ // DestinationType.SINGLE,
+ // APP_NAME,
+ // "core",
+ // "live"
+ //);
+ log.info("Destination "+Hex.encodeHexString(baseDestination.getHash())+" "+baseDestination.getName()+" running.");
+ //log.info("Destination "+Hex.encodeHexString(dataDestination.getHash())+" "+dataDestination.getName()+" running.");
+
+ baseDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
+ //dataDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
+
+ baseDestination.setAcceptLinkRequests(true);
+ //dataDestination.setAcceptLinkRequests(true);
+ //baseDestination.setLinkEstablishedCallback(this::linkExtabishedCallback);
+ baseDestination.setPacketCallback(this::packetCallback);
+ //baseDestination.setPacketCallback((message, packet) -> {
+ // log.info("xyz - Message raw {}", message);
+ // log.info("xyz - Packet {}", packet.toString());
+ //});
+
+ Transport.getInstance().registerAnnounceHandler(new QAnnounceHandler());
+ log.info("announceHandlers: {}", Transport.getInstance().getAnnounceHandlers());
+
+ baseDestination.announce();
+ //dataDestination.announce();
+ log.info("Sent initial announce from {} ({})", Hex.encodeHexString(baseDestination.getHash()), baseDestination.getName());
+
+ // Start up first networking thread (the "server loop")
+ //rnsNetworkEPC.start();
+ }
+
+ public void shutdown() {
+ isShuttingDown = true;
+ log.info("shutting down Reticulum");
+
+ // Stop processing threads (the "server loop")
+ //try {
+ // if (!this.rnsNetworkEPC.shutdown(5000)) {
+ // logger.warn("Network threads failed to terminate");
+ // }
+ //} catch (InterruptedException e) {
+ // logger.warn("Interrupted while waiting for networking threads to terminate");
+ //}
+
+ // Disconnect peers and terminate Reticulum
+ for (RNSPeer p : linkedPeers) {
+ if (nonNull(p.getLink())) {
+ p.getLink().teardown();
+ }
+ }
+ reticulum.exitHandler();
+ }
+
+ private void initConfig(String configDir) throws IOException {
+ File configDir1 = new File(defaultConfigPath);
+ if (!configDir1.exists()) {
+ configDir1.mkdir();
+ }
+ var configPath = Path.of(configDir1.getAbsolutePath());
+ Path configFile = configPath.resolve(CONFIG_FILE_NAME);
+
+ if (Files.notExists(configFile)) {
+ var defaultConfig = this.getClass().getClassLoader().getResourceAsStream("reticulum_default_config.yml");
+ Files.copy(defaultConfig, configFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private void packetCallback(byte[] message, Packet packet) {
+ log.info("xyz - Message raw {}", message);
+ log.info("xyz - Packet {}", packet.toString());
+ }
+
+ //public void announceBaseDestination () {
+ // getBaseDestination().announce();
+ //}
+
+ //public Consumer clientConnected(Link link) {
+ // log.info("Client connected");
+ // link.setLinkClosedCallback(clientDisconnected(link));
+ // link.setPacketCallback(null);
+ //}
+
+ //public void clientDisconnected(Link link) {
+ // log.info("Client disconnected");
+ // linkedPeers.remove(link);
+ //}
+
+ // client part
+ //@Slf4j
+ private static class QAnnounceHandler implements AnnounceHandler {
+ @Override
+ public String getAspectFilter() {
+ // handle all announces
+ return null;
+ }
+
+ @Override
+ @Synchronized
+ public void receivedAnnounce(byte[] destinationHash, Identity announcedIdentity, byte[] appData) {
+ var peerExists = false;
+
+ log.info("Received an announce from {}", Hex.encodeHexString(destinationHash));
+ //log.info("aspect: {}", getAspectFilter());
+ //log.info("destinationhash: {}, announcedIdentity: {}, appData: {}", destinationHash, announcedIdentity, appData);
+
+ if (nonNull(appData)) {
+ log.debug("The announce contained the following app data: {}", new String(appData, UTF_8));
+ }
+
+ // add to peer list if we can use more peers
+ //synchronized (this) {
+ List lps = RNSNetwork.getInstance().getLinkedPeers();
+ if (lps.size() < MAX_PEERS) {
+ for (RNSPeer p : lps) {
+ //log.info("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
+ if (Arrays.equals(p.getDestinationLink().getDestination().getHash(), destinationHash)) {
+ peerExists = true;
+ log.debug("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
+ break;
+ }
+ }
+ if (!peerExists) {
+ //log.info("announce handler - cerate new peer: **announcedIdentity**: {}, **recall**: {}", announcedIdentity, recall(destinationHash));
+ RNSPeer newPeer = new RNSPeer(destinationHash);
+ lps.add(newPeer);
+ log.info("added new RNSPeer, Destination - {}, Link: {}", newPeer.getDestinationHash(), newPeer.getDestinationLink());
+ }
+ }
+ //}
+ }
+ }
+
+ // Main thread
+
+ //class RNSNetworkProcessor extends ExecuteProduceConsume {
+ //
+ // //private final Logger logger = LoggerFactory.getLogger(RNSNetworkProcessor.class);
+ //
+ // private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
+ // private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
+ //
+ // private Iterator channelIterator = null;
+ //
+ // RNSNetworkProcessor(ExecutorService executor) {
+ // super(executor);
+ // }
+ //
+ // @Override
+ // protected void onSpawnFailure() {
+ // // For debugging:
+ // // ExecutorDumper.dump(this.executor, 3, ExecuteProduceConsume.class);
+ // }
+ //
+ // @Override
+ // protected Task produceTask(boolean canBlock) throws InterruptedException {
+ // Task task;
+ //
+ // //task = maybeProducePeerMessageTask();
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //final Long now = NTP.getTime();
+ // //
+ // //task = maybeProducePeerPingTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //task = maybeProduceConnectPeerTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //task = maybeProduceBroadcastTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // // Only this method can block to reduce CPU spin
+ // //return maybeProduceChannelTask(canBlock);
+ //
+ // // TODO: flesh out the tasks handled by Reticulum
+ // return null;
+ // }
+ // //...TODO: implement abstract methods...
+ //}
+
+
+ // getter / setter
+ private static class SingletonContainer {
+ private static final RNSNetwork INSTANCE = new RNSNetwork();
+ }
+
+ public static RNSNetwork getInstance() {
+ return SingletonContainer.INSTANCE;
+ }
+
+ public List getLinkedPeers() {
+ synchronized(this.linkedPeers) {
+ //return new ArrayList<>(this.linkedPeers);
+ return this.linkedPeers;
+ }
+ }
+
+ public Integer getTotalPeers() {
+ synchronized (this) {
+ return linkedPeers.size();
+ }
+ }
+
+ public Destination getBaseDestination() {
+ return baseDestination;
+ }
+
+ // maintenance
+
+ //private static class AnnounceTimer {
+ // //public void main(String[] args) throws InterruptedException
+ // public void main(String[] args) throws InterruptedException
+ // {
+ // Timer timer = new Timer();
+ // // run timer every 10s (10000ms)
+ // timer.schedule(new TimerTask() {
+ // @Override
+ // public void run() {
+ // System.out.println("AnnounceTimer: " + new java.util.Date());
+ // }
+ // }, 0, 10000);
+ // }
+ //}
+
+ @Synchronized
+ public void prunePeers() throws DataException {
+ // run periodically (by the Controller)
+ //log.info("Peer list (linkedPeers): {}",this.linkedPeers.toString());
+ //synchronized(this) {
+ //List linkList = getLinkedPeers();
+ List peerList = this.linkedPeers;
+ log.info("List of RNSPeers: {}", this.linkedPeers);
+ //log.info("number of links (linkedPeers) before prunig: {}", this.linkedPeers.size());
+ Link pLink;
+ LinkStatus lStatus;
+ for (RNSPeer p: peerList) {
+ pLink = p.getLink();
+ lStatus = pLink.getStatus();
+ //log.debug("link status: "+lStatus.toString());
+ // lStatus in: PENDING, HANDSHAKE, ACTIVE, STALE, CLOSED
+ if (lStatus == CLOSED) {
+ p.resetPeer();
+ peerList.remove(p);
+ } else if (lStatus == STALE) {
+ pLink.teardown();
+ p.resetPeer();
+ peerList.remove(p);
+ } else if (lStatus == PENDING) {
+ log.info("prunePeers - link state still {}", lStatus);
+ // TODO: can we help the Link along somehow?
+ }
+ }
+ log.info("number of links (linkedPeers) after prunig: {}", this.linkedPeers.size());
+ //}
+ maybeAnnounce(getBaseDestination());
+ }
+
+ public void maybeAnnounce(Destination d) {
+ if (getLinkedPeers().size() < MIN_DESIRED_PEERS) {
+ d.announce();
+ }
+ }
+
+}
+
diff --git a/src/main/java/org/qortal/network/RNSPeer.java b/src/main/java/org/qortal/network/RNSPeer.java
new file mode 100644
index 00000000..871bb347
--- /dev/null
+++ b/src/main/java/org/qortal/network/RNSPeer.java
@@ -0,0 +1,110 @@
+package org.qortal.network;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.isNull;
+
+import org.qortal.network.RNSNetwork;
+import io.reticulum.link.Link;
+import io.reticulum.packet.Packet;
+import io.reticulum.identity.Identity;
+import io.reticulum.channel.Channel;
+import io.reticulum.destination.Destination;
+import io.reticulum.destination.DestinationType;
+import io.reticulum.destination.Direction;
+
+import static io.reticulum.identity.IdentityKnownDestination.recall;
+//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
+import lombok.extern.slf4j.Slf4j;
+import lombok.Setter;
+import lombok.Data;
+import lombok.AccessLevel;
+
+@Data
+@Slf4j
+public class RNSPeer {
+
+ private byte[] destinationHash;
+ private Link destinationLink;
+ private Identity destinationIdentity;
+ @Setter(AccessLevel.PACKAGE) private long creationTimestamp;
+ private Long lastAccessTimestamp;
+
+ // constructors
+ public RNSPeer (byte[] dhash) {
+ this.destinationHash = dhash;
+ this.destinationIdentity = recall(dhash);
+ Link newLink = new Link(
+ new Destination(
+ this.destinationIdentity,
+ Direction.OUT,
+ DestinationType.SINGLE,
+ RNSNetwork.APP_NAME,
+ "core"
+ )
+ );
+ this.destinationLink = newLink;
+ destinationLink.setPacketCallback(this::packetCallback);
+ }
+
+ public RNSPeer (Link newLink) {
+ this.destinationHash = newLink.getDestination().getHash();
+ this.destinationLink = newLink;
+ this.destinationIdentity = newLink.getRemoteIdentity();
+ setCreationTimestamp(System.currentTimeMillis());
+ this.lastAccessTimestamp = null;
+ destinationLink.setPacketCallback(this::packetCallback);
+ }
+
+ public RNSPeer () {
+ this.destinationHash = null;
+ this.destinationLink = null;
+ this.destinationIdentity = null;
+ setCreationTimestamp(System.currentTimeMillis());
+ this.lastAccessTimestamp = null;
+ }
+
+ // utilities (change Link type, call tasks, ...)
+ //...
+
+ private void packetCallback(byte[] message, Packet packet) {
+ log.debug("Message raw {}", message);
+ log.debug("Packet {}", packet.toString());
+ // ...
+ }
+
+ public Link getLink() {
+ if (isNull(getDestinationLink())) {
+ Link newLink = new Link(
+ new Destination(
+ this.destinationIdentity,
+ Direction.OUT,
+ DestinationType.SINGLE,
+ RNSNetwork.APP_NAME,
+ "core"
+ )
+ );
+ this.destinationLink = newLink;
+ return newLink;
+ }
+ return getDestinationLink();
+ }
+
+ public Channel getChannel() {
+ if (isNull(getDestinationLink())) {
+ log.warn("link is null.");
+ return null;
+ }
+ setLastAccessTimestamp(System.currentTimeMillis());
+ return getDestinationLink().getChannel();
+ }
+
+ public void resetPeer () {
+ this.destinationHash = null;
+ this.destinationLink = null;
+ this.destinationIdentity = null;
+ this.lastAccessTimestamp = null;
+ }
+
+}
diff --git a/src/main/resources/reticulum_default_config.yml b/src/main/resources/reticulum_default_config.yml
new file mode 100644
index 00000000..18e8b729
--- /dev/null
+++ b/src/main/resources/reticulum_default_config.yml
@@ -0,0 +1,93 @@
+---
+# You should probably edit it to include any additional,
+# interfaces and settings you might need.
+
+# Only the most basic options are included in this default
+# configuration. To see a more verbose, and much longer,
+# configuration example, you can run the command:
+# rnsd --exampleconfig
+
+reticulum:
+
+ # If you enable Transport, your system will route traffic
+ # for other peers, pass announces and serve path requests.
+ # This should only be done for systems that are suited to
+ # act as transport nodes, ie. if they are stationary and
+ # always-on. This directive is optional and can be removed
+ # for brevity.
+
+ enable_transport: false
+
+ # By default, the first program to launch the Reticulum
+ # Network Stack will create a shared instance, that other
+ # programs can communicate with. Only the shared instance
+ # opens all the configured interfaces directly, and other
+ # local programs communicate with the shared instance over
+ # a local socket. This is completely transparent to the
+ # user, and should generally be turned on. This directive
+ # is optional and can be removed for brevity.
+
+ share_instance: false
+
+ # If you want to run multiple *different* shared instances
+ # on the same system, you will need to specify different
+ # shared instance ports for each. The defaults are given
+ # below, and again, these options can be left out if you
+ # don't need them.
+
+ #shared_instance_port: 37428
+ #instance_control_port: 37429
+ shared_instance_port: 37438
+ instance_control_port: 37439
+
+ # You can configure Reticulum to panic and forcibly close
+ # if an unrecoverable interface error occurs, such as the
+ # hardware device for an interface disappearing. This is
+ # an optional directive, and can be left out for brevity.
+ # This behaviour is disabled by default.
+
+ panic_on_interface_error: false
+
+
+# The interfaces section defines the physical and virtual
+# interfaces Reticulum will use to communicate on. This
+# section will contain examples for a variety of interface
+# types. You can modify these or use them as a basis for
+# your own config, or simply remove the unused ones.
+
+interfaces:
+
+ # This interface enables communication with other
+ # link-local Reticulum nodes over UDP. It does not
+ # need any functional IP infrastructure like routers
+ # or DHCP servers, but will require that at least link-
+ # local IPv6 is enabled in your operating system, which
+ # should be enabled by default in almost any OS. See
+ # the Reticulum Manual for more configuration options.
+ #"Default Interface":
+ # type: AutoInterface
+ # enabled: true
+
+ # This interface enables communication with a "backbone"
+ # server over TCP.
+ # Note: others may be added for redundancy
+ "TCP Client Interface mobilefabrik":
+ type: TCPClientInterface
+ enabled: true
+ target_host: phantom.mobilefabrik.com
+ target_port: 4242
+ #network_name: qortal
+
+ # This interface turns this Reticulum instance into a
+ # server other clients can connect to over TCP.
+ # To enable this instance to route traffic the above
+ # setting "enable_transport" needs to be set (to true).
+ # Note: this interface type is not yet supported by
+ # reticulum-network-stack.
+ #"TCP Server Interface":
+ # type: TCPServerInterface
+ # enabled: true
+ # listen_ip: 0.0.0.0
+ # listen_port: 3434
+ # #network_name: qortal
+
diff --git a/src/test/java/org/qortal/test/network/RNSNetworTest.java b/src/test/java/org/qortal/test/network/RNSNetworTest.java
new file mode 100644
index 00000000..d81b745d
--- /dev/null
+++ b/src/test/java/org/qortal/test/network/RNSNetworTest.java
@@ -0,0 +1,70 @@
+package org.qortal.test.network;
+
+import org.apache.commons.lang3.StringUtils;
+//import org.junit.Before;
+//import org.junit.Ignore;
+import org.junit.Test;
+
+//import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+//import java.util.Arrays;
+
+import static io.reticulum.constant.ReticulumConstant.ETC_DIR;
+import static org.apache.commons.lang3.SystemUtils.USER_HOME;
+//import static org.junit.Assert.assertNotNull;
+
+class ReticulumTest {
+
+ //@Test
+ //void t() throws DecoderException {
+ // System.out.println(Arrays.toString(Hex.decodeHex("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8")));
+ // System.out.println(2 + 1 + (128 / 8) * 2);
+ //}
+
+ @Test
+ void path() {
+ System.out.println(initConfig(null));
+ }
+
+ //@Test
+ //void testConfigYamlParse() throws IOException {
+ // var config = ConfigObj.initConfig(Path.of(getSystemClassLoader().getResource("reticulum.default.yml").getPath()));
+ // assertNotNull(config);
+ //}
+
+ //@Test
+ //void testHKDF() {
+ // var ifac_netname = "name";
+ // var ifac_netkey = "password";
+ // var ifacOrigin = new byte[]{};
+ // ifacOrigin = ArrayUtils.addAll(ifacOrigin, getSha256Digest().digest(ifac_netname.getBytes(UTF_8)));
+ // ifacOrigin = ArrayUtils.addAll(ifacOrigin, getSha256Digest().digest(ifac_netkey.getBytes(UTF_8)));
+ //
+ // var ifacOriginHash = getSha256Digest().digest(ifacOrigin);
+ //
+ // var HKDF = new HKDFBytesGenerator(new SHA256Digest());
+ // HKDF.init(new HKDFParameters(ifacOriginHash, IFAC_SALT, new byte[0]));
+ // var result = new byte[64];
+ // var len = HKDF.generateBytes(result, 0, result.length);
+ //
+ // assertNotNull(Hex.encodeHexString(result));
+ //}
+
+ private String initConfig(String configDir) {
+ if (StringUtils.isNotBlank(configDir)) {
+ return configDir;
+ } else {
+ if (Files.isDirectory(Path.of(ETC_DIR)) && Files.exists(Path.of(ETC_DIR, "config"))) {
+ return ETC_DIR;
+ } else if (
+ Files.isDirectory(Path.of(USER_HOME, ".config", "reticulum"))
+ && Files.exists(Path.of(USER_HOME, ".config", "reticulum", "config"))
+ ) {
+ return Path.of(USER_HOME, ".config", "reticulum").toString();
+ } else {
+ return Path.of(USER_HOME, ".reticulum").toString();
+ }
+ }
+ }
+}