Compare commits

...

71 Commits

Author SHA1 Message Date
crowetic
6b3264f51f Merge pull request #254 from jschulthess/reticulum
Reticulum branch updates
2025-07-10 13:41:07 -07:00
Jürg Schulthess
8810c883cd switch reticulum to fork with latest fixes 2025-06-22 12:24:24 +02:00
Jürg Schulthess
b1f26a4b00 activate RNSSynchronizer 2025-06-16 20:25:37 +02:00
Jürg Schulthess
b17bff8d51 update reticulum version 2025-05-31 13:24:08 +02:00
Jürg Schulthess
07eda58e9c update reticulum lib version 2025-05-28 18:13:36 +02:00
Jürg Schulthess
5d5b0ff7b2 fix RNS synchhronize call 2025-05-25 12:45:37 +02:00
Jürg Schulthess
01fb7346f9 leave interface behavior at default 2025-05-24 22:36:33 +02:00
Jürg Schulthess
9706ed166a add some methods used by api 2025-05-24 22:07:01 +02:00
Jürg Schulthess
952764d908 implement getResponse over buffer, fixes, updates to config, switch to reticulum master 2025-05-18 21:27:09 +02:00
Jürg Schulthess
c9ed42b93c improve pruning log messages 2025-05-13 08:11:39 +02:00
Jürg Schulthess
6ac0b56fa5 increase min desired peers default, improve announce 2025-05-12 08:09:32 +02:00
Jürg Schulthess
65e16896ef more pruning updates, changes in naming 2025-05-12 07:37:25 +02:00
Jürg Schulthess
8f663f2c83 fix reticulum build dependency, remove static reticulum.io dependency 2025-05-10 08:51:55 +02:00
Jürg Schulthess
5f4a192ee2 reticulum build from buffer branch 2025-05-09 23:06:38 +02:00
Jürg Schulthess
9cf3b5cde4 Merge branch 'Qortal:reticulum' into reticulum 2025-05-09 22:54:19 +02:00
Jürg Schulthess
a7e905f739 peer mgmt fixes, remove lib/io 2025-05-09 22:51:22 +02:00
crowetic
d253d7753d Merge pull request #252 from jschulthess/reticulum
Reticulum Branch Update
2025-05-01 17:05:04 -07:00
Jürg Schulthess
a75c0fc6d6 only use active peers 2025-05-01 20:06:28 +02:00
Jürg Schulthess
8a6410fb67 temporarily comment out RNSSynchronizer as long as not used 2025-04-29 19:08:44 +02:00
Jürg Schulthess
c684460168 remove unneeded lists 2025-04-27 19:27:51 +02:00
Jürg Schulthess
f5065f6049 added sting of RNSSynchronizer to Controller 2025-04-27 19:16:14 +02:00
Jürg Schulthess
2e9f7d5da4 implement RNS version of Synchronizer (not yet active), create activeLinedPeers list for tasks 2025-04-27 17:05:59 +02:00
crowetic
bafd040cb5 Merge pull request #251 from jschulthess/reticulum
Reticulum Branch Update
2025-04-24 14:13:50 -07:00
Jürg Schulthess
e6f349ca41 remove purge timeout setting, updates to pruning 2025-04-21 15:34:04 +02:00
Jürg Schulthess
ea06a7fe91 turn pruning interval into setting 2025-04-21 10:08:22 +02:00
Jürg Schulthess
3e0829260b reduce pong verbosity 2025-04-21 09:40:32 +02:00
Jürg Schulthess
c1091cf9e6 rewored pruning and implementation as task 2025-04-20 19:59:40 +02:00
Jürg Schulthess
13e3d81759 fix buffer accumulation 2025-04-18 20:06:14 +02:00
Jürg Schulthess
eb244bb45b improve prune and more 2025-04-13 10:57:26 +02:00
Jürg Schulthess
096daa691a remove unneeded 2025-04-13 07:50:11 +02:00
Jürg Schulthess
63b41e03f7 sync with official qortal 4.7.1 code base 2025-04-13 07:47:57 +02:00
Jürg Schulthess
e1e5bceb05 ping task working 2025-03-28 18:16:20 +01:00
Jürg Schulthess
da20485870 add peer flushing 2025-03-17 04:34:45 +01:00
Jürg Schulthess
52b6b79b08 RNS mostly implemented. Todo - removing bytest from buffer when reading (threading issue?) 2025-03-16 14:04:06 +01:00
Jürg Schulthess
fd1e2184b6 Merge remote-tracking branch 'origin/master' into reticulum 2025-02-09 04:03:53 +01:00
Jürg Schulthess
9db1518b46 add working state of reticulum library for buffer 2025-01-19 20:57:58 +01:00
Jürg Schulthess
b5f51aa3fd improve incoming peer management, add pruning 2025-01-12 21:02:50 +01:00
Jürg Schulthess
ef171c4d03 change to better ping task log output, some cleanup 2025-01-12 10:33:48 +01:00
Jürg Schulthess
1b4fffe0d2 fix RNS ping task - working 2025-01-11 22:16:52 +01:00
Jürg Schulthess
5b519990cd RNSPingTask working 2025-01-10 20:08:32 +01:00
Jürg Schulthess
b9c4a0c467 initial compileing/working with buffer 2024-12-30 14:11:30 +01:00
Jürg Schulthess
4a81fb1ad5 merge from 4.6.6 2024-12-09 07:48:46 +01:00
Jürg Schulthess
85e92dbfdd Merge remote-tracking branch 'origin/master' into reticulum 2024-11-30 18:25:50 +01:00
Jürg Schulthess
10a1013957 Merge remote-tracking branch 'origin/master' into reticulum 2024-11-24 10:37:52 +01:00
Jürg Schulthess
13ef82b436 Merge remote-tracking branch 'origin/master' into reticulum 2024-11-19 22:20:24 +01:00
Jürg Schulthess
bae369945d merged from master to version 4.6.1 2024-11-09 21:28:40 +01:00
Jürg Schulthess
a64d64e98f prepare for synchronize with main 2024-11-09 19:33:32 +01:00
Jürg Schulthess
df798fc486 use different config template for testnet 2024-09-23 20:21:32 +02:00
Jürg Schulthess
8b5655a120 based on isTestNet, add separete testnet app name and config directory 2024-09-23 20:06:00 +02:00
Jürg Schulthess
f6607e0f7e Merge branch 'Qortal:reticulum' into reticulum 2024-09-11 07:11:58 +02:00
Jürg Schulthess
78060face4 partial cleanup 2024-08-28 18:29:07 +02:00
crowetic
ce33abcade Merge pull request #200 from jschulthess/reticulum
Reticulum Mesh Fixes, synchronized code with qortal-4.5.2-a02d1ce.
2024-08-20 11:13:46 -07:00
Jürg Schulthess
6c5fedd456 create identities directory if it does not exist 2024-08-14 19:49:48 +02:00
Jürg Schulthess
afc2884707 add starup options for netty 2024-08-14 19:48:47 +02:00
Jürg Schulthess
18f15d8122 update reticulum library, sync up with qortal version 4.5.2 2024-08-14 16:21:33 +02:00
Jürg Schulthess
9710d67cce Merge remote-tracking branch 'origin/master' into reticulum 2024-08-14 16:02:29 +02:00
Jürg Schulthess
b2ef503fa7 Merge branch 'Qortal:reticulum' into reticulum 2024-08-01 13:49:18 +02:00
Jürg Schulthess
a497edc488 fix dependencies for reticulum using nitrited db caching 2024-08-01 13:42:51 +02:00
Jürg Schulthess
185f3f515b a few fixes to the mesh functionality 2024-07-12 19:17:47 +02:00
Jürg Schulthess
a445fdc8f2 no pinging during pruning 2024-07-10 22:16:40 +02:00
Jürg Schulthess
61e57f9672 minimize packet timeout callback 2024-07-10 22:01:39 +02:00
Jürg Schulthess
fabfed552e add receipt timeout 2024-07-10 21:28:02 +02:00
Jürg Schulthess
c79a830f2e re-introduce pingRemote during pruning 2024-07-10 21:27:41 +02:00
Jürg Schulthess
1d9347ed23 fix pingRemote 2024-07-10 20:23:29 +02:00
crowetic
c4908678be Merge pull request #199 from jschulthess/reticulum
Reticulum mesh and peer management implementation
2024-07-10 10:26:10 -07:00
Jürg Schulthess
706dc03b3e save dynamically created identity beck to file 2024-07-10 18:44:08 +02:00
Jürg Schulthess
f0d4c1e8de matching project object model 2024-07-09 13:59:17 +02:00
Jürg Schulthess
32460a1b45 initial mesh and peer management implementation 2024-07-09 13:57:32 +02:00
crowetic
4df05364f5 Merge pull request #186 from jschulthess/reticulum
Initial (almost) phase-1 Reticulum implementation
2024-03-25 09:18:55 -07:00
Jürg Schulthess
9f3c1f1cf1 fix typo in file name 2024-03-25 07:22:01 +01:00
Jürg Schulthess
0c8c722097 initial (almost) phase-1 reticulum implementation 2024-03-24 18:24:13 +01:00
28 changed files with 8422 additions and 29 deletions

View File

@@ -1,3 +0,0 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@@ -6,6 +6,34 @@ rootLogger.level = info
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
### additional debug options (in case of problems eg. 202411
#to see more QDN details - add the stuff below
#logger.arbitrary.name = org.qortal.arbitrary
#logger.arbitrary.level = trace
#to see more QDN networking details - add stuff below
#logger.arbitrarycontroller.name = org.qortal.controller.arbitrary
#logger.arbitrarycontroller.level = debug
# Support optional, Network Task debugging
#logger.networkTask.name = org.qortal.network.task
#logger.networkTask.level = debug
# Support optional, Network Task tracing
#logger.networkTask.name = org.qortal.network.task
#logger.networkTask.level = trace
# Support optional, Block debugging
#logger.block.name = org.qortal.block
#logger.block.level = debug
# Support optional, Block tracing
#logger.block.name = org.qortal.block
#logger.block.level = trace
### end additional debug options
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
@@ -18,6 +46,10 @@ logger.hsqldb.level = warn
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
logger.hsqldbRepository.level = debug
## Support optional, controller repository debugging
#logger.controllerRepository.name = org.qortal.controller.repository
#logger.controllerRepository.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = off

211
pom.xml
View File

@@ -52,12 +52,17 @@
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
<protobuf.version>3.25.3</protobuf.version>
<replacer.version>1.5.3</replacer.version>
<reticulum.version>8623ce3</reticulum.version>
<simplemagic.version>1.17</simplemagic.version>
<slf4j.version>1.7.36</slf4j.version>
<swagger-api.version>2.0.10</swagger-api.version>
<swagger-ui.version>5.18.2</swagger-ui.version>
<upnp.version>1.2</upnp.version>
<xz.version>1.10</xz.version>
<lombok.version>1.18.30</lombok.version>
<jackson.version>2.16.1</jackson.version>
<slf4j.version>2.0.12</slf4j.version>
<nitrite.version>4.3.0</nitrite.version>
<junit.version>5.9.2</junit.version>
</properties>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
@@ -426,13 +431,41 @@
<id>project.local</id>
<name>project</name>
<url>file:${project.basedir}/lib</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
<!-- jitpack for build-on-demand of altcoinj -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
<!--
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.dizitart</groupId>
<artifactId>nitrite-bom</artifactId>
<version>${nitrite.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
-->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.codehaus.mojo/build-helper-maven-plugin -->
<dependency>
@@ -480,6 +513,13 @@
<artifactId>altcoinj</artifactId>
<version>${altcoinj.version}</version>
</dependency>
<!-- Build Reticulum from Source -->
<dependency>
<!--<groupId>com.github.sergst83</groupId>-->
<groupId>com.github.jschulthess</groupId>
<artifactId>reticulum-network-stack</artifactId>
<version>${reticulum.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
@@ -558,35 +598,33 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Logging: log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- redirect slf4j to log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- redirect java.utils.logging to log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Logging: slf4j used by Jetty/Jersey -->
<!-- logging: slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Logging: log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Servlet related -->
<dependency>
<groupId>javax.servlet</groupId>
@@ -728,6 +766,11 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency><!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15to18 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
@@ -770,5 +813,123 @@
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb-runtime.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.1</version>
</dependency>
<!-- already declared earlier
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- already declared earlier
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
-->
<!-- note: covered ? -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>0.9.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>com.macasaet.fernet</groupId>
<artifactId>fernet-java8</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.igormaznitsa</groupId>
<artifactId>jbbp</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<!--<version>4.1.106.Final</version>-->
<version>5.0.0.Alpha2</version>
</dependency>
<dependency>
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.4.2</version>
</dependency>
<!-- Nitrite Modules -->
<dependency>
<groupId>org.dizitart</groupId>
<artifactId>nitrite</artifactId>
<version>${nitrite.version}</version>
</dependency>
<dependency>
<groupId>org.dizitart</groupId>
<artifactId>nitrite-mvstore-adapter</artifactId>
<version>${nitrite.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.230</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2-mvstore</artifactId>
<version>2.3.230</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -18,6 +18,7 @@ import org.qortal.controller.hsqldb.HSQLDBDataCacheManager;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.controller.repository.PruneManager;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.controller.tradebot.RNSTradeBot;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
@@ -33,6 +34,8 @@ 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.RNSPeer;
import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.network.message.*;
@@ -123,6 +126,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. */
@@ -519,6 +523,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() {
@@ -530,6 +543,9 @@ public class Controller extends Thread {
LOGGER.info("Starting synchronizer");
Synchronizer.getInstance().start();
LOGGER.info("Starting synchronizer over Reticulum");
RNSSynchronizer.getInstance().start();
LOGGER.info("Starting block minter");
blockMinter = new BlockMinter();
blockMinter.start();
@@ -727,6 +743,73 @@ public class Controller extends Thread {
}
}
}, 3*60*1000, 3*60*1000);
//Timer syncFromGenesisRNS = new Timer();
//syncFromGenesisRNS.schedule(new TimerTask() {
// @Override
// public void run() {
// LOGGER.debug("Start sync from genesis check (RNS).");
// boolean canBootstrap = Settings.getInstance().getBootstrap();
// boolean needsArchiveRebuildRNS = false;
// int checkHeightRNS = 0;
//
// try (final Repository repository = RepositoryManager.getRepository()){
// needsArchiveRebuildRNS = (repository.getBlockArchiveRepository().fromHeight(2) == null);
// checkHeightRNS = repository.getBlockRepository().getBlockchainHeight();
// } catch (DataException e) {
// throw new RuntimeException(e);
// }
//
// if (canBootstrap || !needsArchiveRebuildRNS || checkHeightRNS > 3) {
// LOGGER.debug("Bootstrapping is enabled or we have more than 2 blocks, cancel sync from genesis check.");
// syncFromGenesisRNS.cancel();
// return;
// }
//
// if (needsArchiveRebuildRNS && !canBootstrap) {
// LOGGER.info("Start syncing from genesis (RNS)!");
// List<RNSPeer> seeds = new ArrayList<>(RNSNetwork.getInstance().getActiveImmutableLinkedPeers());
//
// // Check if have a qualified peer to sync
// if (seeds.isEmpty()) {
// LOGGER.info("No connected RNSPeer(s), will try again later.");
// return;
// }
//
// int index = new SecureRandom().nextInt(seeds.size());
// RNSPeer syncPeer = seeds.get(index);
// var syncPeerLinkAsString = syncPeer.getPeerLink().toString();
// //String syncNode = String.valueOf(seeds.get(index));
// //PeerAddress peerAddress = PeerAddress.fromString(syncNode);
// //InetSocketAddress resolvedAddress = null;
// //
// //try {
// // resolvedAddress = peerAddress.toSocketAddress();
// //} catch (UnknownHostException e) {
// // throw new RuntimeException(e);
// //}
// //
// //InetSocketAddress finalResolvedAddress = resolvedAddress;
// //Peer targetPeer = seeds.stream().filter(peer -> peer.getResolvedAddress().equals(finalResolvedAddress)).findFirst().orElse(null);
// //RNSPeer targetPeerRNS = seeds.stream().findFirst().orElse(null);
// RNSPeer targetPeerRNS = seeds.stream().filter(peer -> peer.getPeerLink().toString().equals(syncPeerLinkAsString)).findFirst().orElse(null);
// RNSSynchronizer.SynchronizationResult syncResultRNS;
//
// try {
// do {
// try {
// syncResultRNS = RNSSynchronizer.getInstance().actuallySynchronize(targetPeerRNS, true);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// }
// while (syncResultRNS == RNSSynchronizer.SynchronizationResult.OK);
// } finally {
// // We are syncing now, so can cancel the check
// syncFromGenesisRNS.cancel();
// }
// }
// }
//}, 3*60*1000, 3*60*1000);
}
/** Called by AdvancedInstaller's launch EXE in single-instance mode, when an instance is already running. */
@@ -744,6 +827,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();
@@ -852,6 +937,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;
@@ -918,23 +1015,47 @@ public class Controller extends Thread {
return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
};
public static final Predicate<RNSPeer> hasNoRecentBlock2 = peer -> {
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
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 BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
};
public static final Predicate<RNSPeer> hasNoOrSameBlock2 = peer -> {
final BlockData latestBlockData = getInstance().getChainTip();
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 BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getHeight() == 1;
};
public static final Predicate<RNSPeer> hasOnlyGenesisBlock2 = peer -> {
final BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getHeight() == 1;
};
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
final BlockSummaryData peerChainTipData = peer.getChainTipData();
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
};
public static final Predicate<RNSPeer> hasInferiorChainTip2 = peer -> {
final BlockSummaryData peerChainTipData = peer.getChainTipData();
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
};
public static final Predicate<Peer> hasOldVersion = peer -> {
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
return !peer.isAtLeastVersion(minPeerVersion);
@@ -952,6 +1073,18 @@ public class Controller extends Thread {
}
};
public static final Predicate<RNSPeer> hasInvalidSigner2 = peer -> {
final BlockSummaryData peerChainTipData = peer.getChainTipData();
if (peerChainTipData == null)
return true;
try (Repository repository = RepositoryManager.getRepository()) {
return Account.getRewardShareEffectiveMintingLevel(repository, peerChainTipData.getMinterPublicKey()) == 0;
} catch (DataException e) {
return true;
}
};
public static final Predicate<Peer> wasRecentlyTooDivergent = peer -> {
Long now = NTP.getTime();
Long peerLastTooDivergentTime = peer.getLastTooDivergentTime();
@@ -1105,6 +1238,7 @@ public class Controller extends Thread {
LOGGER.info("Shutting down synchronizer");
Synchronizer.getInstance().shutdown();
RNSSynchronizer.getInstance().shutdown();
LOGGER.info("Shutting down API");
ApiService.getInstance().stop();
@@ -1150,6 +1284,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 {
@@ -1232,6 +1369,35 @@ public class Controller extends Thread {
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
}
public void doRNSNetworkBroadcast() {
if (Settings.getInstance().isLite()) {
// Lite nodes have nothing to broadcast
return;
}
RNSNetwork network = RNSNetwork.getInstance();
// Send our current height
network.broadcastOurChain();
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// if we're not up-to-dat then priority is synchronizing first
if (isUpToDateRNS()) {
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
}
}
public void doRNSPrunePeers() {
RNSNetwork network = RNSNetwork.getInstance();
try {
LOGGER.debug("Pruning peers...");
network.prunePeers();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
}
}
public void onMintingPossibleChange(boolean isMintingPossible) {
this.isMintingPossible = isMintingPossible;
requestSysTrayUpdate = true;
@@ -2163,4 +2329,688 @@ public class Controller extends Thread {
public StatsSnapshot getStatsSnapshot() {
return this.stats;
}
public void onRNSNetworkMessage(RNSPeer peer, Message message) {
LOGGER.trace(() -> String.format("Processing %s message from %s", message.getType().name(), peer));
// Ordered by message type value
switch (message.getType()) {
case GET_BLOCK:
onRNSNetworkGetBlockMessage(peer, message);
break;
case GET_BLOCK_SUMMARIES:
onRNSNetworkGetBlockSummariesMessage(peer, message);
break;
case GET_SIGNATURES_V2:
onRNSNetworkGetSignaturesV2Message(peer, message);
break;
case HEIGHT_V2:
onRNSNetworkHeightV2Message(peer, message);
break;
case BLOCK_SUMMARIES_V2:
onRNSNetworkBlockSummariesV2Message(peer, message);
break;
case GET_TRANSACTION:
RNSTransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
break;
case TRANSACTION:
RNSTransactionImporter.getInstance().onNetworkTransactionMessage(peer, message);
break;
case GET_UNCONFIRMED_TRANSACTIONS:
RNSTransactionImporter.getInstance().onNetworkGetUnconfirmedTransactionsMessage(peer, message);
break;
case TRANSACTION_SIGNATURES:
RNSTransactionImporter.getInstance().onNetworkTransactionSignaturesMessage(peer, message);
break;
//case GET_ONLINE_ACCOUNTS_V3:
// OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
// break;
//
//case ONLINE_ACCOUNTS_V3:
// OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
// break;
// TODO: Compiles but rethink for Reticulum
case GET_ARBITRARY_DATA:
// Not currently supported
break;
case ARBITRARY_DATA_FILE_LIST:
RNSArbitraryDataFileListManager.getInstance().onNetworkArbitraryDataFileListMessage(peer, message);
break;
case GET_ARBITRARY_DATA_FILE:
RNSArbitraryDataFileManager.getInstance().onNetworkGetArbitraryDataFileMessage(peer, message);
break;
case GET_ARBITRARY_DATA_FILE_LIST:
RNSArbitraryDataFileListManager.getInstance().onNetworkGetArbitraryDataFileListMessage(peer, message);
break;
//
case ARBITRARY_SIGNATURES:
// Not currently supported
break;
case GET_ARBITRARY_METADATA:
RNSArbitraryMetadataManager.getInstance().onNetworkGetArbitraryMetadataMessage(peer, message);
break;
case ARBITRARY_METADATA:
RNSArbitraryMetadataManager.getInstance().onNetworkArbitraryMetadataMessage(peer, message);
break;
case GET_TRADE_PRESENCES:
RNSTradeBot.getInstance().onGetTradePresencesMessage(peer, message);
break;
case TRADE_PRESENCES:
RNSTradeBot.getInstance().onTradePresencesMessage(peer, message);
break;
case GET_ACCOUNT:
onRNSNetworkGetAccountMessage(peer, message);
break;
case GET_ACCOUNT_BALANCE:
onRNSNetworkGetAccountBalanceMessage(peer, message);
break;
case GET_ACCOUNT_TRANSACTIONS:
onRNSNetworkGetAccountTransactionsMessage(peer, message);
break;
case GET_ACCOUNT_NAMES:
onRNSNetworkGetAccountNamesMessage(peer, message);
break;
case GET_NAME:
onRNSNetworkGetNameMessage(peer, message);
break;
default:
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
break;
}
}
private void onRNSNetworkGetBlockMessage(RNSPeer peer, Message message) {
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
byte[] signature = getBlockMessage.getSignature();
this.stats.getBlockMessageStats.requests.incrementAndGet();
ByteArray signatureAsByteArray = ByteArray.wrap(signature);
CachedBlockMessage cachedBlockMessage = this.blockMessageCache.get(signatureAsByteArray);
int blockCacheSize = Settings.getInstance().getBlockCacheSize();
// Check cached latest block message
if (cachedBlockMessage != null) {
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
// We need to duplicate it to prevent multiple threads setting ID on the same message
CachedBlockMessage clonedBlockMessage = Message.cloneWithNewId(cachedBlockMessage, message.getId());
//if (!peer.sendMessage(clonedBlockMessage))
// peer.disconnect("failed to send block");
peer.sendMessage(clonedBlockMessage);
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData != null) {
if (PruneManager.getInstance().isBlockPruned(blockData.getHeight())) {
// If this is a pruned block, we likely only have partial data, so best not to sent it
blockData = null;
}
}
// If we have no block data, we should check the archive in case it's there
if (blockData == null) {
if (Settings.getInstance().isArchiveEnabled()) {
Triple<byte[], Integer, Integer> serializedBlock = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, true, repository);
if (serializedBlock != null) {
byte[] bytes = serializedBlock.getA();
Integer serializationVersion = serializedBlock.getB();
Message blockMessage;
switch (serializationVersion) {
case 1:
blockMessage = new CachedBlockMessage(bytes);
break;
case 2:
blockMessage = new CachedBlockV2Message(bytes);
break;
default:
return;
}
blockMessage.setId(message.getId());
// This call also causes the other needed data to be pulled in from repository
//if (!peer.sendMessage(blockMessage)) {
// peer.disconnect("failed to send block");
// // Don't fall-through to caching because failure to send might be from failure to build message
// return;
//}
peer.sendMessage(blockMessage);
// Sent successfully from archive, so nothing more to do
return;
}
}
}
if (blockData == null) {
// We don't have this block
this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
// 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)));
// Send generic 'unknown' message as it's very short
//Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
// ? new GenericUnknownMessage()
// : new BlockSummariesMessage(Collections.emptyList());
Message blockUnknownMessage = new GenericUnknownMessage();
blockUnknownMessage.setId(message.getId());
//if (!peer.sendMessage(blockUnknownMessage))
// peer.disconnect("failed to send block-unknown response");
peer.sendMessage(blockUnknownMessage);
return;
}
Block block = new Block(repository, blockData);
//// V2 support
//if (peer.getPeersVersion() >= BlockV2Message.MIN_PEER_VERSION) {
// Message blockMessage = new BlockV2Message(block);
// blockMessage.setId(message.getId());
// if (!peer.sendMessage(blockMessage)) {
// peer.disconnect("failed to send block");
// // Don't fall-through to caching because failure to send might be from failure to build message
// return;
// }
// return;
//}
CachedBlockMessage blockMessage = new CachedBlockMessage(block);
blockMessage.setId(message.getId());
//if (!peer.sendMessage(blockMessage)) {
// peer.disconnect("failed to send block");
// // Don't fall-through to caching because failure to send might be from failure to build message
// return;
//}
peer.sendMessage(blockMessage);
// If request is for a recent block, cache it
if (getChainHeight() - blockData.getHeight() <= blockCacheSize) {
this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
this.blockMessageCache.put(ByteArray.wrap(blockData.getSignature()), blockMessage);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
} catch (TransformationException e) {
LOGGER.error(String.format("Serialization issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
}
}
private void onRNSNetworkGetBlockSummariesMessage(RNSPeer peer, Message message) {
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
this.stats.getBlockSummariesStats.requests.incrementAndGet();
// If peer's parent signature matches our latest block signature
// 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 = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
// ? new BlockSummariesV2Message(Collections.emptyList())
// : new BlockSummariesMessage(Collections.emptyList());
Message blockSummariesMessage = new BlockSummariesV2Message(Collections.emptyList());
blockSummariesMessage.setId(message.getId());
//if (!peer.sendMessage(blockSummariesMessage))
// peer.disconnect("failed to send block summaries");
peer.sendMessage(blockSummariesMessage);
return;
}
List<BlockSummaryData> blockSummaries = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
blockSummaries = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockSummaryData::new)
.collect(Collectors.toList());
}
if (blockSummaries.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null) {
// Try the archive
blockData = repository.getBlockArchiveRepository().fromReference(parentSignature);
}
if (blockData != null) {
if (PruneManager.getInstance().isBlockPruned(blockData.getHeight())) {
// If this request contains a pruned block, we likely only have partial data, so best not to sent anything
// We always prune from the oldest first, so it's fine to just check the first block requested
blockData = null;
}
}
while (blockData != null && blockSummaries.size() < numberRequested) {
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
byte[] previousSignature = blockData.getSignature();
blockData = repository.getBlockRepository().fromReference(previousSignature);
if (blockData == null) {
// Try the archive
blockData = repository.getBlockArchiveRepository().fromReference(previousSignature);
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
//Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
// ? new BlockSummariesV2Message(blockSummaries)
// : new BlockSummariesMessage(blockSummaries);
Message blockSummariesMessage = new BlockSummariesV2Message(blockSummaries);
blockSummariesMessage.setId(message.getId());
//if (!peer.sendMessage(blockSummariesMessage))
// peer.disconnect("failed to send block summaries");
peer.sendMessage(blockSummariesMessage);
}
private void onRNSNetworkGetSignaturesV2Message(RNSPeer peer, Message message) {
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
signaturesMessage.setId(message.getId());
//if (!peer.sendMessage(signaturesMessage))
// peer.disconnect("failed to send signatures (v2)");
peer.sendMessage(signaturesMessage);
return;
}
List<byte[]> signatures = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
signatures = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockData::getSignature)
.collect(Collectors.toList());
}
if (signatures.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = getSignaturesMessage.getNumberRequested();
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null) {
// Try the archive
blockData = repository.getBlockArchiveRepository().fromReference(parentSignature);
}
while (blockData != null && signatures.size() < numberRequested) {
signatures.add(blockData.getSignature());
byte[] previousSignature = blockData.getSignature();
blockData = repository.getBlockRepository().fromReference(previousSignature);
if (blockData == null) {
// Try the archive
blockData = repository.getBlockArchiveRepository().fromReference(previousSignature);
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
if (signatures.size() >= getSignaturesMessage.getNumberRequested())
this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
}
Message signaturesMessage = new SignaturesMessage(signatures);
signaturesMessage.setId(message.getId());
//if (!peer.sendMessage(signaturesMessage))
// peer.disconnect("failed to send signatures (v2)");
peer.sendMessage(signaturesMessage);
}
private void onRNSNetworkHeightV2Message(RNSPeer peer, Message message) {
HeightV2Message heightV2Message = (HeightV2Message) message;
if (!Settings.getInstance().isLite()) {
// 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.getIsInitiator() && peer.getChainTipData() == null) {
Message responseMessage = RNSNetwork.getInstance().buildHeightOrChainTipInfo(peer);
peer.sendMessage(responseMessage);
}
}
// Update peer chain tip data
BlockSummaryData newChainTipData = new BlockSummaryData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getMinterPublicKey(), heightV2Message.getTimestamp());
peer.setChainTipData(newChainTipData);
// Potentially synchronize
RNSSynchronizer.getInstance().requestSync();
}
private void onRNSNetworkBlockSummariesV2Message(RNSPeer 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.getIsInitiator() && peer.getChainTipData() == null) {
Message responseMessage = RNSNetwork.getInstance().buildHeightOrChainTipInfo(peer);
peer.sendMessage(responseMessage);
}
}
if (message.hasId()) {
/*
* Experimental proof-of-concept: discard messages with ID
* These are 'late' reply messages received after timeout has expired,
* having been passed upwards from Peer to Network to Controller.
* Hence, these are NOT simple "here's my chain tip" broadcasts from other peers.
*/
LOGGER.debug("Discarding late {} message with ID {} from {}", message.getType().name(), message.getId(), peer);
return;
}
// Update peer chain tip data
peer.setChainTipSummaries(blockSummariesV2Message.getBlockSummaries());
// Potentially synchronize
RNSSynchronizer.getInstance().requestSync();
}
// ************
private void onRNSNetworkGetAccountMessage(RNSPeer peer, Message message) {
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
String address = getAccountMessage.getAddress();
this.stats.getAccountMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
if (accountData == null) {
// We don't have this account
this.stats.getAccountMessageStats.unknownAccounts.getAndIncrement();
// 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));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
peer.sendMessage(accountUnknownMessage);
return;
}
AccountMessage accountMessage = new AccountMessage(accountData);
accountMessage.setId(message.getId());
// handle in timeout callback instead
//if (!peer.sendMessage(accountMessage)) {
// peer.disconnect("failed to send account");
//}
peer.sendMessage(accountMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send account %s to peer %s", address, peer), e);
}
}
private void onRNSNetworkGetAccountBalanceMessage(RNSPeer peer, Message message) {
GetAccountBalanceMessage getAccountBalanceMessage = (GetAccountBalanceMessage) message;
String address = getAccountBalanceMessage.getAddress();
long assetId = getAccountBalanceMessage.getAssetId();
this.stats.getAccountBalanceMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(address, assetId);
if (accountBalanceData == null) {
// We don't have this account
this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement();
// 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));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
peer.sendMessage(accountUnknownMessage);
return;
}
AccountBalanceMessage accountMessage = new AccountBalanceMessage(accountBalanceData);
accountMessage.setId(message.getId());
// handle in timeout callback instead
//if (!peer.sendMessage(accountMessage)) {
// peer.disconnect("failed to send account balance");
//}
peer.sendMessage(accountMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send balance for account %s and asset ID %d to peer %s", address, assetId, peer), e);
}
}
private void onRNSNetworkGetAccountTransactionsMessage(RNSPeer peer, Message message) {
GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message;
String address = getAccountTransactionsMessage.getAddress();
int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100);
int offset = getAccountTransactionsMessage.getOffset();
this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false);
// Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
if (transactions == null) {
// We don't have this account
this.stats.getAccountTransactionsMessageStats.unknownAccounts.getAndIncrement();
// 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));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
peer.sendMessage(accountUnknownMessage);
return;
}
TransactionsMessage transactionsMessage = new TransactionsMessage(transactions);
transactionsMessage.setId(message.getId());
// handle in timeout callback instead
//if (!peer.sendMessage(transactionsMessage)) {
// peer.disconnect("failed to send account transactions");
//}
peer.sendMessage(transactionsMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending transactions for account %s %d to peer %s", address, peer), e);
} catch (MessageException e) {
LOGGER.error(String.format("Message serialization issue while sending transactions for account %s %d to peer %s", address, peer), e);
}
}
private void onRNSNetworkGetAccountNamesMessage(RNSPeer peer, Message message) {
GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message;
String address = getAccountNamesMessage.getAddress();
this.stats.getAccountNamesMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> namesDataList = repository.getNameRepository().getNamesByOwner(address);
if (namesDataList == null) {
// We don't have this account
this.stats.getAccountNamesMessageStats.unknownAccounts.getAndIncrement();
// 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));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
peer.sendMessage(accountUnknownMessage);
return;
}
NamesMessage namesMessage = new NamesMessage(namesDataList);
namesMessage.setId(message.getId());
// handle in timeout callback instead
//if (!peer.sendMessage(namesMessage)) {
// peer.disconnect("failed to send account names");
//}
peer.sendMessage(namesMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e);
}
}
private void onRNSNetworkGetNameMessage(RNSPeer peer, Message message) {
GetNameMessage getNameMessage = (GetNameMessage) message;
String name = getNameMessage.getName();
this.stats.getNameMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
NameData nameData = repository.getNameRepository().fromName(name);
if (nameData == null) {
// We don't have this account
this.stats.getNameMessageStats.unknownAccounts.getAndIncrement();
// 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));
// Send generic 'unknown' message as it's very short
Message nameUnknownMessage = new GenericUnknownMessage();
nameUnknownMessage.setId(message.getId());
if (!peer.sendMessage(nameUnknownMessage))
peer.sendMessage(nameUnknownMessage);
return;
}
NamesMessage namesMessage = new NamesMessage(Arrays.asList(nameData));
namesMessage.setId(message.getId());
// handle in timeout callback instead
//if (!peer.sendMessage(namesMessage)) {
// peer.disconnect("failed to send name data");
//}
peer.sendMessage(namesMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send name %s to peer %s", name, peer), e);
}
}
/**
* Returns whether we think our node has up-to-date blockchain based on our info about other peers.
* @param minLatestBlockTimestamp - the minimum block timestamp to be considered recent
* @return boolean - whether our node's blockchain is up to date or not
*/
public boolean isUpToDateRNS(Long minLatestBlockTimestamp) {
if (Settings.getInstance().isLite()) {
// Lite nodes are always "up to date"
return true;
}
// Do we even have a vaguely recent block?
if (minLatestBlockTimestamp == null)
return false;
final BlockData latestBlockData = getChainTip();
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
return false;
if (Settings.getInstance().isSingleNodeTestnet())
// Single node testnets won't have peers, so we can assume up to date from this point
return true;
// Needs a mutable copy of the unmodifiableList
List<RNSPeer> peers = new ArrayList<>(RNSNetwork.getInstance().getActiveImmutableLinkedPeers());
if (peers == null)
return false;
//// Disregard peers that have "misbehaved" recently
//peers.removeIf(hasMisbehaved);
//
//// Disregard peers that don't have a recent block
//peers.removeIf(hasNoRecentBlock);
// Check we have enough peers to potentially synchronize/mint
if (peers.size() < Settings.getInstance().getReticulumMinDesiredPeers())
return false;
// If we don't have any peers left then can't synchronize, therefore consider ourself not up to date
return !peers.isEmpty();
}
/**
* Returns whether we think our node has up-to-date blockchain based on our info about other peers.
* Uses the default minLatestBlockTimestamp value.
* @return boolean - whether our node's blockchain is up to date or not
*/
public boolean isUpToDateRNS() {
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
return this.isUpToDate(minLatestBlockTimestamp);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,460 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.GetTransactionMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.TransactionMessage;
import org.qortal.network.message.TransactionSignaturesMessage;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class RNSTransactionImporter extends Thread {
private static final Logger LOGGER = LogManager.getLogger(RNSTransactionImporter.class);
private static RNSTransactionImporter instance;
private volatile boolean isStopping = false;
private static final int MAX_INCOMING_TRANSACTIONS = 5000;
/** Minimum time before considering an invalid unconfirmed transaction as "stale" */
public static final long INVALID_TRANSACTION_STALE_TIMEOUT = 30 * 60 * 1000L; // ms
/** Minimum frequency to re-request stale unconfirmed transactions from peers, to recheck validity */
public static final long INVALID_TRANSACTION_RECHECK_INTERVAL = 60 * 60 * 1000L; // ms\
/** Minimum frequency to re-request expired unconfirmed transactions from peers, to recheck validity
* This mainly exists to stop expired transactions from bloating the list */
public static final long EXPIRED_TRANSACTION_RECHECK_INTERVAL = 10 * 60 * 1000L; // ms
/** Map of incoming transaction that are in the import queue. Key is transaction data, value is whether signature has been validated. */
private final Map<TransactionData, Boolean> incomingTransactions = Collections.synchronizedMap(new HashMap<>());
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */
private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
public static List<TransactionData> unconfirmedTransactionsCache = null;
public static synchronized RNSTransactionImporter getInstance() {
if (instance == null) {
instance = new RNSTransactionImporter();
}
return instance;
}
@Override
public void run() {
Thread.currentThread().setName("Transaction Importer");
try {
while (!Controller.isStopping()) {
Thread.sleep(500L);
// Process incoming transactions queue
validateTransactionsInQueue();
importTransactionsInQueue();
// Clean up invalid incoming transactions list
cleanupInvalidTransactionsList(NTP.getTime());
}
} catch (InterruptedException e) {
// Fall through to exit thread
}
}
public void shutdown() {
isStopping = true;
this.interrupt();
}
// Incoming transactions queue
private boolean incomingTransactionQueueContains(byte[] signature) {
synchronized (incomingTransactions) {
return incomingTransactions.keySet().stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature));
}
}
private void removeIncomingTransaction(byte[] signature) {
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature));
}
/**
* Retrieve all pending unconfirmed transactions that have had their signatures validated.
* @return a list of TransactionData objects, with valid signatures.
*/
private List<TransactionData> getCachedSigValidTransactions() {
synchronized (this.incomingTransactions) {
return this.incomingTransactions.entrySet().stream()
.filter(t -> Boolean.TRUE.equals(t.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
/**
* Validate the signatures of any transactions pending import, then update their
* entries in the queue to mark them as valid/invalid.
*
* No database lock is required.
*/
private void validateTransactionsInQueue() {
if (this.incomingTransactions.isEmpty()) {
// Nothing to do?
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
// Take a snapshot of incomingTransactions, so we don't need to lock it while processing
Map<TransactionData, Boolean> incomingTransactionsCopy = Map.copyOf(this.incomingTransactions);
int unvalidatedCount = Collections.frequency(incomingTransactionsCopy.values(), Boolean.FALSE);
int validatedCount = 0;
if (unvalidatedCount > 0) {
LOGGER.debug("Validating signatures in incoming transactions queue (size {})...", unvalidatedCount);
}
// A list of all currently pending transactions that have valid signatures
List<Transaction> sigValidTransactions = new ArrayList<>();
// A list of signatures that became valid in this round
List<byte[]> newlyValidSignatures = new ArrayList<>();
boolean isLiteNode = Settings.getInstance().isLite();
// We need the latest block in order to check for expired transactions
BlockData latestBlock = Controller.getInstance().getChainTip();
// Signature validation round - does not require blockchain lock
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
// Quick exit?
if (isStopping) {
return;
}
TransactionData transactionData = transactionEntry.getKey();
Transaction transaction = Transaction.fromData(repository, transactionData);
String signature58 = Base58.encode(transactionData.getSignature());
Long now = NTP.getTime();
if (now == null) {
return;
}
// Drop expired transactions before they are considered "sig valid"
if (latestBlock != null && transaction.getDeadline() <= latestBlock.getTimestamp()) {
LOGGER.debug("Removing expired {} transaction {} from import queue", transactionData.getType().name(), signature58);
removeIncomingTransaction(transactionData.getSignature());
invalidUnconfirmedTransactions.put(signature58, (now + EXPIRED_TRANSACTION_RECHECK_INTERVAL));
continue;
}
// Only validate signature if we haven't already done so
Boolean isSigValid = transactionEntry.getValue();
if (!Boolean.TRUE.equals(isSigValid)) {
if (isLiteNode) {
// Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid
sigValidTransactions.add(transaction);
newlyValidSignatures.add(transactionData.getSignature());
// Add mark signature as valid if transaction still exists in import queue
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
continue;
}
if (!transaction.isSignatureValid()) {
LOGGER.debug("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
removeIncomingTransaction(transactionData.getSignature());
// Also add to invalidIncomingTransactions map
now = NTP.getTime();
if (now != null) {
Long expiry = now + INVALID_TRANSACTION_RECHECK_INTERVAL;
LOGGER.trace("Adding invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
// Add to invalidUnconfirmedTransactions so that we don't keep requesting it
invalidUnconfirmedTransactions.put(signature58, expiry);
}
// We're done with this transaction
continue;
}
// Count the number that were validated in this round, for logging purposes
validatedCount++;
// Add mark signature as valid if transaction still exists in import queue
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
// Signature validated in this round
newlyValidSignatures.add(transactionData.getSignature());
} else {
LOGGER.trace(() -> String.format("Transaction %s known to have valid signature", Base58.encode(transactionData.getSignature())));
}
// Signature valid - add to shortlist
sigValidTransactions.add(transaction);
}
if (unvalidatedCount > 0) {
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
}
} catch (DataException e) {
LOGGER.error("Repository issue while processing incoming transactions", e);
}
}
/**
* Import any transactions in the queue that have valid signatures.
*
* A database lock is required.
*/
private void importTransactionsInQueue() {
List<TransactionData> sigValidTransactions = this.getCachedSigValidTransactions();
if (sigValidTransactions.isEmpty()) {
// Don't bother locking if there are no new transactions to process
return;
}
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
// Prioritize syncing, and don't attempt to lock
return;
}
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock()) {
LOGGER.debug("Too busy to import incoming transactions queue");
return;
}
LOGGER.debug("Importing incoming transactions queue (size {})...", sigValidTransactions.size());
int processedCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
// Use a single copy of the unconfirmed transactions list for each cycle, to speed up constant lookups
// when counting unconfirmed transactions by creator.
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
unconfirmedTransactions.removeIf(t -> t.getType() == Transaction.TransactionType.CHAT);
unconfirmedTransactionsCache = unconfirmedTransactions;
// A list of signatures were imported in this round
List<byte[]> newlyImportedSignatures = new ArrayList<>();
// Import transactions with valid signatures
try {
for (int i = 0; i < sigValidTransactions.size(); ++i) {
if (isStopping) {
return;
}
if (Synchronizer.getInstance().isSyncRequestPending()) {
LOGGER.debug("Breaking out of transaction importing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
return;
}
TransactionData transactionData = sigValidTransactions.get(i);
Transaction transaction = Transaction.fromData(repository, transactionData);
Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed();
processedCount++;
switch (validationResult) {
case TRANSACTION_ALREADY_EXISTS: {
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature())));
break;
}
case NO_BLOCKCHAIN_LOCK: {
// Is this even possible considering we acquired blockchain lock above?
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
break;
}
case OK: {
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
// Add to the unconfirmed transactions cache
if (transactionData.getType() != Transaction.TransactionType.CHAT && unconfirmedTransactionsCache != null) {
unconfirmedTransactionsCache.add(transactionData);
}
// Signature imported in this round
newlyImportedSignatures.add(transactionData.getSignature());
break;
}
// All other invalid cases:
default: {
final String signature58 = Base58.encode(transactionData.getSignature());
LOGGER.debug(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58));
Long now = NTP.getTime();
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) {
Long expiryLength = INVALID_TRANSACTION_RECHECK_INTERVAL;
if (validationResult == Transaction.ValidationResult.TIMESTAMP_TOO_OLD) {
// Use shorter recheck interval for expired transactions
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL;
}
Long expiry = now + expiryLength;
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
// Invalid, unconfirmed transaction has become stale - add to invalidUnconfirmedTransactions so that we don't keep requesting it
invalidUnconfirmedTransactions.put(signature58, expiry);
}
}
}
// Transaction has been processed, even if only to reject it
removeIncomingTransaction(transactionData.getSignature());
}
if (!newlyImportedSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly imported signatures", newlyImportedSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyImportedSignatures);
RNSNetwork.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
} finally {
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock();
// Clear the unconfirmed transaction cache so new data can be populated in the next cycle
unconfirmedTransactionsCache = null;
}
} catch (DataException e) {
LOGGER.error("Repository issue while importing incoming transactions", e);
}
}
private void cleanupInvalidTransactionsList(Long now) {
if (now == null) {
return;
}
// Periodically remove invalid unconfirmed transactions from the list, so that they can be fetched again
invalidUnconfirmedTransactions.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < now);
}
// Network handlers
public void onNetworkTransactionMessage(RNSPeer peer, Message message) {
TransactionMessage transactionMessage = (TransactionMessage) message;
TransactionData transactionData = transactionMessage.getTransactionData();
if (this.incomingTransactions.size() < MAX_INCOMING_TRANSACTIONS) {
synchronized (this.incomingTransactions) {
if (!incomingTransactionQueueContains(transactionData.getSignature())) {
this.incomingTransactions.put(transactionData, Boolean.FALSE);
}
}
}
}
public void onNetworkGetTransactionMessage(RNSPeer peer, Message message) {
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) message;
byte[] signature = getTransactionMessage.getSignature();
try (final Repository repository = RepositoryManager.getRepository()) {
// Firstly check the sig-valid transactions that are currently queued for import
TransactionData transactionData = this.getCachedSigValidTransactions().stream()
.filter(t -> Arrays.equals(signature, t.getSignature()))
.findFirst().orElse(null);
if (transactionData == null) {
// Not found in import queue, so try the database
transactionData = repository.getTransactionRepository().fromSignature(signature);
}
if (transactionData == null) {
// Still not found - so we don't have this transaction
LOGGER.debug(() -> String.format("Ignoring GET_TRANSACTION request from peer %s for unknown transaction %s", peer, Base58.encode(signature)));
// Send no response at all???
return;
}
Message transactionMessage = new TransactionMessage(transactionData);
transactionMessage.setId(message.getId());
peer.sendMessage(transactionMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
} catch (TransformationException e) {
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
}
}
public void onNetworkGetUnconfirmedTransactionsMessage(RNSPeer peer, Message message) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = Collections.emptyList();
// If we're NOT up-to-date then don't send out unconfirmed transactions
// as it's possible they are already included in a later block that we don't have.
if (Controller.getInstance().isUpToDate())
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
peer.sendMessage(transactionSignaturesMessage);
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending unconfirmed transaction signatures to peer %s", peer), e);
}
}
public void onNetworkTransactionSignaturesMessage(RNSPeer peer, Message message) {
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) message;
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
try (final Repository repository = RepositoryManager.getRepository()) {
for (byte[] signature : signatures) {
String signature58 = Base58.encode(signature);
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
// Previously invalid transaction - don't keep requesting it
// It will be periodically removed from invalidUnconfirmedTransactions to allow for rechecks
continue;
}
// Ignore if this transaction is in the queue
if (incomingTransactionQueueContains(signature)) {
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peer));
continue;
}
// Do we have it already? (Before requesting transaction data itself)
if (repository.getTransactionRepository().exists(signature)) {
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
continue;
}
// Check isInterrupted() here and exit fast
if (Thread.currentThread().isInterrupted())
return;
// Fetch actual transaction data from peer
Message getTransactionMessage = new GetTransactionMessage(signature);
peer.sendMessage(getTransactionMessage);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer %s", peer), e);
}
}
}

View File

@@ -0,0 +1,731 @@
package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataFileChunk;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.RNSArbitraryDirectConnectionInfo;
import org.qortal.data.arbitrary.RNSArbitraryFileListResponseInfo;
import org.qortal.data.arbitrary.RNSArbitraryRelayInfo;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.ArbitraryDataFileListMessage;
import org.qortal.network.message.GetArbitraryDataFileListMessage;
import org.qortal.network.message.Message;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.ListUtils;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
import java.util.*;
import static org.qortal.controller.arbitrary.RNSArbitraryDataFileManager.MAX_FILE_HASH_RESPONSES;
public class RNSArbitraryDataFileListManager {
private static final Logger LOGGER = LogManager.getLogger(RNSArbitraryDataFileListManager.class);
private static RNSArbitraryDataFileListManager instance;
private static String MIN_PEER_VERSION_FOR_FILE_LIST_STATS = "3.2.0";
/**
* Map of recent incoming requests for ARBITRARY transaction data file lists.
* <p>
* Key is original request's message ID<br>
* Value is Triple&lt;transaction signature in base58, first requesting peer, first request's timestamp&gt;
* <p>
* If peer is null then either:<br>
* <ul>
* <li>we are the original requesting peer</li>
* <li>we have already sent data payload to original requesting peer.</li>
* </ul>
* If signature is null then we have already received the file list and either:<br>
* <ul>
* <li>we are the original requesting peer and have processed it</li>
* <li>we have forwarded the file list</li>
* </ul>
*/
public Map<Integer, Triple<String, RNSPeer, Long>> arbitraryDataFileListRequests = Collections.synchronizedMap(new HashMap<>());
/**
* Map to keep track of in progress arbitrary data signature requests
* Key: string - the signature encoded in base58
* Value: Triple<networkBroadcastCount, directPeerRequestCount, lastAttemptTimestamp>
*/
private Map<String, Triple<Integer, Integer, Long>> arbitraryDataSignatureRequests = Collections.synchronizedMap(new HashMap<>());
/** Maximum number of seconds that a file list relay request is able to exist on the network */
public static long RELAY_REQUEST_MAX_DURATION = 5000L;
/** Maximum number of hops that a file list relay request is allowed to make */
public static int RELAY_REQUEST_MAX_HOPS = 4;
/** Minimum peer version to use relay */
public static String RELAY_MIN_PEER_VERSION = "3.4.0";
private RNSArbitraryDataFileListManager() {
}
public static RNSArbitraryDataFileListManager getInstance() {
if (instance == null)
instance = new RNSArbitraryDataFileListManager();
return instance;
}
public void cleanupRequestCache(Long now) {
if (now == null) {
return;
}
final long requestMinimumTimestamp = now - ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT;
arbitraryDataFileListRequests.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < requestMinimumTimestamp);
}
// Track file list lookups by signature
private boolean shouldMakeFileListRequestForSignature(String signature58) {
Triple<Integer, Integer, Long> request = arbitraryDataSignatureRequests.get(signature58);
if (request == null) {
// Not attempted yet
return true;
}
// Extract the components
Integer networkBroadcastCount = request.getA();
// Integer directPeerRequestCount = request.getB();
Long lastAttemptTimestamp = request.getC();
if (lastAttemptTimestamp == null) {
// Not attempted yet
return true;
}
long timeSinceLastAttempt = NTP.getTime() - lastAttemptTimestamp;
// Allow a second attempt after 15 seconds, and another after 30 seconds
if (timeSinceLastAttempt > 15 * 1000L) {
// We haven't tried for at least 15 seconds
if (networkBroadcastCount < 3) {
// We've made less than 3 total attempts
return true;
}
}
// Then allow another 5 attempts, each 1 minute apart
if (timeSinceLastAttempt > 60 * 1000L) {
// We haven't tried for at least 1 minute
if (networkBroadcastCount < 8) {
// We've made less than 8 total attempts
return true;
}
}
// Then allow another 8 attempts, each 15 minutes apart
if (timeSinceLastAttempt > 15 * 60 * 1000L) {
// We haven't tried for at least 15 minutes
if (networkBroadcastCount < 16) {
// We've made less than 16 total attempts
return true;
}
}
// From then on, only try once every 6 hours, to reduce network spam
if (timeSinceLastAttempt > 6 * 60 * 60 * 1000L) {
// We haven't tried for at least 6 hours
return true;
}
return false;
}
private boolean shouldMakeDirectFileRequestsForSignature(String signature58) {
if (!Settings.getInstance().isDirectDataRetrievalEnabled()) {
// Direct connections are disabled in the settings
return false;
}
Triple<Integer, Integer, Long> request = arbitraryDataSignatureRequests.get(signature58);
if (request == null) {
// Not attempted yet
return true;
}
// Extract the components
//Integer networkBroadcastCount = request.getA();
Integer directPeerRequestCount = request.getB();
Long lastAttemptTimestamp = request.getC();
if (lastAttemptTimestamp == null) {
// Not attempted yet
return true;
}
if (directPeerRequestCount == 0) {
// We haven't tried asking peers directly yet, so we should
return true;
}
long timeSinceLastAttempt = NTP.getTime() - lastAttemptTimestamp;
if (timeSinceLastAttempt > 10 * 1000L) {
// We haven't tried for at least 10 seconds
if (directPeerRequestCount < 5) {
// We've made less than 5 total attempts
return true;
}
}
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
// We haven't tried for at least 5 minutes
if (directPeerRequestCount < 10) {
// We've made less than 10 total attempts
return true;
}
}
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
// We haven't tried for at least 1 hour
return true;
}
return false;
}
public boolean isSignatureRateLimited(byte[] signature) {
String signature58 = Base58.encode(signature);
return !this.shouldMakeFileListRequestForSignature(signature58)
&& !this.shouldMakeDirectFileRequestsForSignature(signature58);
}
public long lastRequestForSignature(byte[] signature) {
String signature58 = Base58.encode(signature);
Triple<Integer, Integer, Long> request = arbitraryDataSignatureRequests.get(signature58);
if (request == null) {
// Not attempted yet
return 0;
}
// Extract the components
Long lastAttemptTimestamp = request.getC();
if (lastAttemptTimestamp != null) {
return lastAttemptTimestamp;
}
return 0;
}
public void addToSignatureRequests(String signature58, boolean incrementNetworkRequests, boolean incrementPeerRequests) {
Triple<Integer, Integer, Long> request = arbitraryDataSignatureRequests.get(signature58);
Long now = NTP.getTime();
if (request == null) {
// No entry yet
Triple<Integer, Integer, Long> newRequest = new Triple<>(0, 0, now);
arbitraryDataSignatureRequests.put(signature58, newRequest);
}
else {
// There is an existing entry
if (incrementNetworkRequests) {
request.setA(request.getA() + 1);
}
if (incrementPeerRequests) {
request.setB(request.getB() + 1);
}
request.setC(now);
arbitraryDataSignatureRequests.put(signature58, request);
}
}
public void removeFromSignatureRequests(String signature58) {
arbitraryDataSignatureRequests.remove(signature58);
}
// Lookup file lists by signature (and optionally hashes)
public boolean fetchArbitraryDataFileList(ArbitraryTransactionData arbitraryTransactionData) {
byte[] signature = arbitraryTransactionData.getSignature();
String signature58 = Base58.encode(signature);
// Require an NTP sync
Long now = NTP.getTime();
if (now == null) {
return false;
}
// If we've already tried too many times in a short space of time, make sure to give up
if (!this.shouldMakeFileListRequestForSignature(signature58)) {
// Check if we should make direct connections to peers
if (this.shouldMakeDirectFileRequestsForSignature(signature58)) {
return RNSArbitraryDataFileManager.getInstance().fetchDataFilesFromPeersForSignature(signature);
}
LOGGER.trace("Skipping file list request for signature {} due to rate limit", signature58);
return false;
}
this.addToSignatureRequests(signature58, true, false);
//List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
List<RNSPeer> handshakedPeers = RNSNetwork.getInstance().getLinkedPeers();
List<byte[]> missingHashes = null;
// Find hashes that we are missing
try {
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
missingHashes = arbitraryDataFile.missingHashes();
} catch (DataException e) {
// Leave missingHashes as null, so that all hashes are requested
}
int hashCount = missingHashes != null ? missingHashes.size() : 0;
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
//// Send our address as requestingPeer, to allow for potential direct connections with seeds/peers
//String requestingPeer = Network.getInstance().getOurExternalIpAddressAndPort();
String requestingPeer = null;
// Build request
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, missingHashes, now, 0, requestingPeer);
// Save our request into requests map
Triple<String, RNSPeer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
// Assign random ID to this message
int id;
do {
id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
// Put queue into map (keyed by message ID) so we can poll for a response
// If putIfAbsent() doesn't return null, then this ID is already taken
} while (arbitraryDataFileListRequests.put(id, requestEntry) != null);
getArbitraryDataFileListMessage.setId(id);
// Broadcast request
RNSNetwork.getInstance().broadcast(peer -> getArbitraryDataFileListMessage);
// Poll to see if data has arrived
final long singleWait = 100;
long totalWait = 0;
while (totalWait < ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT) {
try {
Thread.sleep(singleWait);
} catch (InterruptedException e) {
break;
}
requestEntry = arbitraryDataFileListRequests.get(id);
if (requestEntry == null)
return false;
if (requestEntry.getA() == null)
break;
totalWait += singleWait;
}
return true;
}
public boolean fetchArbitraryDataFileList(RNSPeer peer, byte[] signature) {
String signature58 = Base58.encode(signature);
// Require an NTP sync
Long now = NTP.getTime();
if (now == null) {
return false;
}
int hashCount = 0;
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to peer %s...", signature58, hashCount, peer));
// Build request
// Use a time in the past, so that the recipient peer doesn't try and relay it
// Also, set hashes to null since it's easier to request all hashes than it is to determine which ones we need
// This could be optimized in the future
long timestamp = now - 60000L;
List<byte[]> hashes = null;
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, timestamp, 0, null);
// Save our request into requests map
Triple<String, RNSPeer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
// Assign random ID to this message
int id;
do {
id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
// Put queue into map (keyed by message ID) so we can poll for a response
// If putIfAbsent() doesn't return null, then this ID is already taken
} while (arbitraryDataFileListRequests.put(id, requestEntry) != null);
getArbitraryDataFileListMessage.setId(id);
// Send the request
peer.sendMessage(getArbitraryDataFileListMessage);
// Poll to see if data has arrived
final long singleWait = 100;
long totalWait = 0;
while (totalWait < ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT) {
try {
Thread.sleep(singleWait);
} catch (InterruptedException e) {
break;
}
requestEntry = arbitraryDataFileListRequests.get(id);
if (requestEntry == null)
return false;
if (requestEntry.getA() == null)
break;
totalWait += singleWait;
}
return true;
}
public void deleteFileListRequestsForSignature(byte[] signature) {
String signature58 = Base58.encode(signature);
for (Iterator<Map.Entry<Integer, Triple<String, RNSPeer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
Map.Entry<Integer, Triple<String, RNSPeer, Long>> entry = it.next();
if (entry == null || entry.getKey() == null || entry.getValue() != null) {
continue;
}
if (Objects.equals(entry.getValue().getA(), signature58)) {
// Update requests map to reflect that we've received all chunks
Triple<String, RNSPeer, Long> newEntry = new Triple<>(null, null, entry.getValue().getC());
arbitraryDataFileListRequests.put(entry.getKey(), newEntry);
}
}
}
// Network handlers
public void onNetworkArbitraryDataFileListMessage(RNSPeer peer, Message message) {
// Don't process if QDN is disabled
if (!Settings.getInstance().isQdnEnabled()) {
return;
}
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
if (LOGGER.isDebugEnabled() && arbitraryDataFileListMessage.getRequestTime() != null) {
long totalRequestTime = NTP.getTime() - arbitraryDataFileListMessage.getRequestTime();
LOGGER.debug("totalRequestTime: {}, requestHops: {}, peerAddress: {}, isRelayPossible: {}",
totalRequestTime, arbitraryDataFileListMessage.getRequestHops(),
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
}
// Do we have a pending request for this data?
Triple<String, RNSPeer, Long> request = arbitraryDataFileListRequests.get(message.getId());
if (request == null || request.getA() == null) {
return;
}
boolean isRelayRequest = (request.getB() != null);
// Does this message's signature match what we're expecting?
byte[] signature = arbitraryDataFileListMessage.getSignature();
String signature58 = Base58.encode(signature);
if (!request.getA().equals(signature58)) {
return;
}
List<byte[]> hashes = arbitraryDataFileListMessage.getHashes();
if (hashes == null || hashes.isEmpty()) {
return;
}
ArbitraryTransactionData arbitraryTransactionData = null;
// Check transaction exists and hashes are correct
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (!(transactionData instanceof ArbitraryTransactionData))
return;
arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
// // Load data file(s)
// ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
//
// // Check all hashes exist
// for (byte[] hash : hashes) {
// //LOGGER.debug("Received hash {}", Base58.encode(hash));
// if (!arbitraryDataFile.containsChunk(hash)) {
// // Check the hash against the complete file
// if (!Arrays.equals(arbitraryDataFile.getHash(), hash)) {
// LOGGER.info("Received non-matching chunk hash {} for signature {}. This could happen if we haven't obtained the metadata file yet.", Base58.encode(hash), signature58);
// return;
// }
// }
// }
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
Long now = NTP.getTime();
if (RNSArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.size() < MAX_FILE_HASH_RESPONSES) {
// Keep track of the hashes this peer reports to have access to
for (byte[] hash : hashes) {
String hash58 = Base58.encode(hash);
// Treat null request hops as 100, so that they are able to be sorted (and put to the end of the list)
int requestHops = arbitraryDataFileListMessage.getRequestHops() != null ? arbitraryDataFileListMessage.getRequestHops() : 100;
RNSArbitraryFileListResponseInfo responseInfo = new RNSArbitraryFileListResponseInfo(hash58, signature58,
peer, now, arbitraryDataFileListMessage.getRequestTime(), requestHops);
RNSArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.add(responseInfo);
}
}
// Keep track of the source peer, for direct connections
if (arbitraryDataFileListMessage.getPeerAddress() != null) {
RNSArbitraryDataFileManager.getInstance().addDirectConnectionInfoIfUnique(
new RNSArbitraryDirectConnectionInfo(signature, arbitraryDataFileListMessage.getPeerAddress(), hashes, now));
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while finding arbitrary transaction data list for peer %s", peer), e);
}
// Forwarding
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
if (!isBlocked) {
RNSPeer requestingPeer = request.getB();
if (requestingPeer != null) {
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
Integer requestHops = arbitraryDataFileListMessage.getRequestHops();
// Add each hash to our local mapping so we know who to ask later
Long now = NTP.getTime();
for (byte[] hash : hashes) {
String hash58 = Base58.encode(hash);
RNSArbitraryRelayInfo relayInfo = new RNSArbitraryRelayInfo(hash58, signature58, peer, now, requestTime, requestHops);
RNSArbitraryDataFileManager.getInstance().addToRelayMap(relayInfo);
}
// Bump requestHops if it exists
if (requestHops != null) {
requestHops++;
}
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
//// TODO - rework for Reticulum
//// Remove optional parameters if the requesting peer doesn't support it yet
//// A message with less statistical data is better than no message at all
//if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
// forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
//} else {
// forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
// arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
//}
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
forwardArbitraryDataFileListMessage.setId(message.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
//if (!requestingPeer.sendMessage(forwardArbitraryDataFileListMessage)) {
// requestingPeer.disconnect("failed to forward arbitrary data file list");
//}
requestingPeer.sendMessage(forwardArbitraryDataFileListMessage);
}
}
}
}
public void onNetworkGetArbitraryDataFileListMessage(RNSPeer peer, Message message) {
// Don't respond if QDN is disabled
if (!Settings.getInstance().isQdnEnabled()) {
return;
}
Controller.getInstance().stats.getArbitraryDataFileListMessageStats.requests.incrementAndGet();
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
byte[] signature = getArbitraryDataFileListMessage.getSignature();
String signature58 = Base58.encode(signature);
Long now = NTP.getTime();
Triple<String, RNSPeer, Long> newEntry = new Triple<>(signature58, peer, now);
// If we've seen this request recently, then ignore
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
return;
}
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
int hashCount = requestedHashes != null ? requestedHashes.size() : 0;
String requestingPeer = getArbitraryDataFileListMessage.getRequestingPeer();
if (requestingPeer != null) {
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
}
else {
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
}
List<byte[]> hashes = new ArrayList<>();
ArbitraryTransactionData transactionData = null;
boolean allChunksExist = false;
boolean hasMetadata = false;
try (final Repository repository = RepositoryManager.getRepository()) {
// Firstly we need to lookup this file on chain to get a list of its hashes
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
if (transactionData instanceof ArbitraryTransactionData) {
// Check if we're even allowed to serve data for this transaction
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
// Load file(s) and add any that exist to the list of hashes
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
if (requestedHashes == null || requestedHashes.isEmpty()) {
requestedHashes = new ArrayList<>();
// Add the metadata file
if (arbitraryDataFile.getMetadataHash() != null) {
requestedHashes.add(arbitraryDataFile.getMetadataHash());
hasMetadata = true;
}
// Add the chunk hashes
if (!arbitraryDataFile.getChunkHashes().isEmpty()) {
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
}
// Add complete file if there are no hashes
else {
requestedHashes.add(arbitraryDataFile.getHash());
}
}
// Assume all chunks exists, unless one can't be found below
allChunksExist = true;
for (byte[] requestedHash : requestedHashes) {
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(requestedHash, signature);
if (chunk.exists()) {
hashes.add(chunk.getHash());
//LOGGER.trace("Added hash {}", chunk.getHash58());
} else {
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
allChunksExist = false;
}
}
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer %s", peer), e);
}
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
// or can use the separate metadata protocol to fetch it. This should greatly reduce network spam.
if (hasMetadata && hashes.size() == 1) {
hashes.clear();
}
// We should only respond if we have at least one hash
if (!hashes.isEmpty()) {
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
RNSArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
// We have all the chunks, so update requests map to reflect that we've sent it
// There is no need to keep track of the request, as we can serve all the chunks
if (allChunksExist) {
newEntry = new Triple<>(null, null, now);
arbitraryDataFileListRequests.put(message.getId(), newEntry);
}
//String ourAddress = RNSNetwork.getInstance().getOurExternalIpAddressAndPort();
String ourAddress = RNSNetwork.getInstance().getBaseDestination().getHexHash();
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
// TODO: rework for Reticulum
// Remove optional parameters if the requesting peer doesn't support it yet
// A message with less statistical data is better than no message at all
//if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
// arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
//} else {
// arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
// hashes, NTP.getTime(), 0, ourAddress, true);
//}
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
arbitraryDataFileListMessage.setId(message.getId());
//if (!peer.sendMessage(arbitraryDataFileListMessage)) {
// LOGGER.debug("Couldn't send list of hashes");
// peer.disconnect("failed to send list of hashes");
// return;
//}
peer.sendMessage(arbitraryDataFileListMessage);
LOGGER.debug("Sent list of hashes (count: {})", hashes.size());
if (allChunksExist) {
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
LOGGER.debug("No need for any forwarding because file list request is fully served");
return;
}
}
// We may need to forward this request on
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
// In relay mode - so ask our other peers if they have it
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
long totalRequestTime = now - requestTime;
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
// Relay request hasn't timed out yet, so can potentially be rebroadcast
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
relayGetArbitraryDataFileListMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
//Network.getInstance().broadcast(
// broadcastPeer ->
// !broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
// broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
//);
RNSNetwork.getInstance().broadcast(broadcastPeer -> relayGetArbitraryDataFileListMessage);
}
else {
// This relay request has reached the maximum number of allowed hops
}
}
else {
// This relay request has timed out
}
}
}
}

View File

@@ -0,0 +1,639 @@
package org.qortal.controller.arbitrary;
import com.google.common.net.InetAddresses;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.RNSArbitraryDirectConnectionInfo;
import org.qortal.data.arbitrary.RNSArbitraryFileListResponseInfo;
import org.qortal.data.arbitrary.RNSArbitraryRelayInfo;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
public class RNSArbitraryDataFileManager extends Thread {
private static final Logger LOGGER = LogManager.getLogger(RNSArbitraryDataFileManager.class);
private static RNSArbitraryDataFileManager instance;
private volatile boolean isStopping = false;
/**
* Map to keep track of our in progress (outgoing) arbitrary data file requests
*/
public Map<String, Long> arbitraryDataFileRequests = Collections.synchronizedMap(new HashMap<>());
/**
* Map to keep track of hashes that we might need to relay
*/
public final List<RNSArbitraryRelayInfo> arbitraryRelayMap = Collections.synchronizedList(new ArrayList<>());
/**
* List to keep track of any arbitrary data file hash responses
*/
public final List<RNSArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
/**
* List to keep track of peers potentially available for direct connections, based on recent requests
*/
private final List<RNSArbitraryDirectConnectionInfo> directConnectionInfo = Collections.synchronizedList(new ArrayList<>());
/**
* Map to keep track of peers requesting QDN data that we hold.
* Key = peer address string, value = time of last request.
* This allows for additional "burst" connections beyond existing limits.
*/
private Map<String, Long> recentDataRequests = Collections.synchronizedMap(new HashMap<>());
public static int MAX_FILE_HASH_RESPONSES = 1000;
private RNSArbitraryDataFileManager() {
}
public static RNSArbitraryDataFileManager getInstance() {
if (instance == null)
instance = new RNSArbitraryDataFileManager();
return instance;
}
@Override
public void run() {
Thread.currentThread().setName("Arbitrary Data File Manager");
try {
// Use a fixed thread pool to execute the arbitrary data file requests
int threadCount = 5;
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
}
while (!isStopping) {
// Nothing to do yet
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// Fall-through to exit thread...
}
}
public void shutdown() {
isStopping = true;
this.interrupt();
}
public void cleanupRequestCache(Long now) {
if (now == null) {
return;
}
final long requestMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_REQUEST_TIMEOUT;
arbitraryDataFileRequests.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < requestMinimumTimestamp);
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
arbitraryDataFileHashResponses.removeIf(entry -> entry.getTimestamp() < relayMinimumTimestamp);
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
final long recentDataRequestMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RECENT_DATA_REQUESTS_TIMEOUT;
recentDataRequests.entrySet().removeIf(entry -> entry.getValue() < recentDataRequestMinimumTimestamp);
}
// Fetch data files by hash
public boolean fetchArbitraryDataFiles(Repository repository,
RNSPeer peer,
byte[] signature,
ArbitraryTransactionData arbitraryTransactionData,
List<byte[]> hashes) throws DataException {
// Load data file(s)
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
boolean receivedAtLeastOneFile = false;
// Now fetch actual data from this peer
for (byte[] hash : hashes) {
if (isStopping) {
return false;
}
String hash58 = Base58.encode(hash);
if (!arbitraryDataFile.chunkExists(hash)) {
// Only request the file if we aren't already requesting it from someone else
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
LOGGER.debug("Requesting data file {} from peer {}", hash58, peer);
Long startTime = NTP.getTime();
ArbitraryDataFile receivedArbitraryDataFile = fetchArbitraryDataFile(peer, null, arbitraryTransactionData, signature, hash, null);
Long endTime = NTP.getTime();
if (receivedArbitraryDataFile != null) {
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFile.getHash58(), peer, (endTime-startTime));
receivedAtLeastOneFile = true;
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
arbitraryDataFileHashResponses.remove(hash58);
}
else {
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}. Time taken: {} ms", peer, Base58.encode(hash), Base58.encode(signature), (endTime-startTime));
// Remove this hash from arbitraryDataFileHashResponses now that we have failed to receive it
arbitraryDataFileHashResponses.remove(hash58);
// Stop asking for files from this peer
break;
}
}
else {
LOGGER.trace("Already requesting data file {} for signature {} from peer {}", arbitraryDataFile, Base58.encode(signature), peer);
}
}
else {
// Remove this hash from arbitraryDataFileHashResponses because we have a local copy
arbitraryDataFileHashResponses.remove(hash58);
}
}
if (receivedAtLeastOneFile) {
// Invalidate the hosted transactions cache as we are now hosting something new
ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache();
// Check if we have all the files we need for this transaction
if (arbitraryDataFile.allFilesExist()) {
// We have all the chunks for this transaction, so we should invalidate the transaction's name's
// data cache so that it is rebuilt the next time we serve it
ArbitraryDataManager.getInstance().invalidateCache(arbitraryTransactionData);
}
}
return receivedAtLeastOneFile;
}
private ArbitraryDataFile fetchArbitraryDataFile(RNSPeer peer, RNSPeer requestingPeer, ArbitraryTransactionData arbitraryTransactionData, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
boolean fileAlreadyExists = existingFile.exists();
String hash58 = Base58.encode(hash);
ArbitraryDataFile arbitraryDataFile;
// Fetch the file if it doesn't exist locally
if (!fileAlreadyExists) {
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
arbitraryDataFileRequests.put(hash58, NTP.getTime());
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
Message response = null;
// TODO - revisit with RNS
try {
response = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
} catch (InterruptedException e) {
// Will return below due to null response
}
arbitraryDataFileRequests.remove(hash58);
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
// We may need to remove the file list request, if we have all the files for this transaction
this.handleFileListRequests(signature);
if (response == null) {
LOGGER.debug("Received null response from peer {}", peer);
return null;
}
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
return null;
}
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
arbitraryDataFile = peersArbitraryDataFileMessage.getArbitraryDataFile();
} else {
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
arbitraryDataFile = existingFile;
}
if (arbitraryDataFile == null) {
// We don't have a file, so give up here
return null;
}
// We might want to forward the request to the peer that originally requested it
this.handleArbitraryDataFileForwarding(requestingPeer, new ArbitraryDataFileMessage(signature, arbitraryDataFile), originalMessage);
boolean isRelayRequest = (requestingPeer != null);
if (isRelayRequest) {
if (!fileAlreadyExists) {
// File didn't exist locally before the request, and it's a forwarding request, so delete it if it exists.
// It shouldn't exist on the filesystem yet, but leaving this here just in case.
arbitraryDataFile.delete(10);
}
}
else {
arbitraryDataFile.save();
}
// If this is a metadata file then we need to update the cache
if (arbitraryTransactionData != null && arbitraryTransactionData.getMetadataHash() != null) {
if (Arrays.equals(arbitraryTransactionData.getMetadataHash(), hash)) {
ArbitraryDataCacheManager.getInstance().addToUpdateQueue(arbitraryTransactionData);
}
}
return arbitraryDataFile;
}
private void handleFileListRequests(byte[] signature) {
try (final Repository repository = RepositoryManager.getRepository()) {
// Fetch the transaction data
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
if (arbitraryTransactionData == null) {
return;
}
boolean allChunksExist = ArbitraryTransactionUtils.allChunksExist(arbitraryTransactionData);
if (allChunksExist) {
// Update requests map to reflect that we've received all chunks
RNSArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature);
}
} catch (DataException e) {
LOGGER.debug("Unable to handle file list requests: {}", e.getMessage());
}
}
public void handleArbitraryDataFileForwarding(RNSPeer requestingPeer, Message message, Message originalMessage) {
// Return if there is no originally requesting peer to forward to
if (requestingPeer == null) {
return;
}
// Return if we're not in relay mode or if this request doesn't need forwarding
if (!Settings.getInstance().isRelayModeEnabled()) {
return;
}
LOGGER.debug("Received arbitrary data file - forwarding is needed");
// The ID needs to match that of the original request
message.setId(originalMessage.getId());
//if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
// LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
// requestingPeer.disconnect("failed to forward arbitrary data file");
//}
//else {
// LOGGER.debug("Forwarded arbitrary data file to peer {}", requestingPeer);
//}
requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
}
// Fetch data directly from peers
private List<RNSArbitraryDirectConnectionInfo> getDirectConnectionInfoForSignature(byte[] signature) {
synchronized (directConnectionInfo) {
return directConnectionInfo.stream().filter(i -> Arrays.equals(i.getSignature(), signature)).collect(Collectors.toList());
}
}
/**
* Add an ArbitraryDirectConnectionInfo item, but only if one with this peer-signature combination
* doesn't already exist.
* @param connectionInfo - the direct connection info to add
*/
public void addDirectConnectionInfoIfUnique(RNSArbitraryDirectConnectionInfo connectionInfo) {
boolean peerAlreadyExists;
synchronized (directConnectionInfo) {
peerAlreadyExists = directConnectionInfo.stream()
.anyMatch(i -> Arrays.equals(i.getSignature(), connectionInfo.getSignature())
&& Objects.equals(i.getPeerAddress(), connectionInfo.getPeerAddress()));
}
if (!peerAlreadyExists) {
directConnectionInfo.add(connectionInfo);
}
}
private void removeDirectConnectionInfo(RNSArbitraryDirectConnectionInfo connectionInfo) {
this.directConnectionInfo.remove(connectionInfo);
}
public boolean fetchDataFilesFromPeersForSignature(byte[] signature) {
String signature58 = Base58.encode(signature);
boolean success = false;
try {
while (!success) {
if (isStopping) {
return false;
}
Thread.sleep(500L);
// Firstly fetch peers that claim to be hosting files for this signature
List<RNSArbitraryDirectConnectionInfo> connectionInfoList = getDirectConnectionInfoForSignature(signature);
if (connectionInfoList == null || connectionInfoList.isEmpty()) {
LOGGER.debug("No remaining direct connection peers found for signature {}", signature58);
return false;
}
LOGGER.debug("Attempting a direct peer connection for signature {}...", signature58);
// Peers found, so pick one with the highest number of chunks
Comparator<RNSArbitraryDirectConnectionInfo> highestChunkCountFirstComparator =
Comparator.comparingInt(RNSArbitraryDirectConnectionInfo::getHashCount).reversed();
RNSArbitraryDirectConnectionInfo directConnectionInfo = connectionInfoList.stream()
.sorted(highestChunkCountFirstComparator).findFirst().orElse(null);
if (directConnectionInfo == null) {
return false;
}
// Remove from the list so that a different peer is tried next time
removeDirectConnectionInfo(directConnectionInfo);
//// TODO - rework this section (RNS network address?)
//String peerAddressString = directConnectionInfo.getPeerAddress();
//
//// Parse the peer address to find the host and port
//String host = null;
//int port = -1;
//String[] parts = peerAddressString.split(":");
//if (parts.length > 1) {
// host = parts[0];
// port = Integer.parseInt(parts[1]);
//} else {
// // Assume no port included
// host = peerAddressString;
// // Use default listen port
// port = Settings.getInstance().getDefaultListenPort();
//}
//
//String peerAddressStringWithPort = String.format("%s:%d", host, port);
//success = Network.getInstance().requestDataFromPeer(peerAddressStringWithPort, signature);
//
//int defaultPort = Settings.getInstance().getDefaultListenPort();
//
//// If unsuccessful, and using a non-standard port, try a second connection with the default listen port,
//// since almost all nodes use that. This is a workaround to account for any ephemeral ports that may
//// have made it into the dataset.
//if (!success) {
// if (host != null && port > 0) {
// if (port != defaultPort) {
// String newPeerAddressString = String.format("%s:%d", host, defaultPort);
// success = Network.getInstance().requestDataFromPeer(newPeerAddressString, signature);
// }
// }
//}
//
//// If _still_ unsuccessful, try matching the peer's IP address with some known peers, and then connect
//// to each of those in turn until one succeeds.
//if (!success) {
// if (host != null) {
// final String finalHost = host;
// List<PeerData> knownPeers = Network.getInstance().getAllKnownPeers().stream()
// .filter(knownPeerData -> knownPeerData.getAddress().getHost().equals(finalHost))
// .collect(Collectors.toList());
// // Loop through each match and attempt a connection
// for (PeerData matchingPeer : knownPeers) {
// String matchingPeerAddress = matchingPeer.getAddress().toString();
// int matchingPeerPort = matchingPeer.getAddress().getPort();
// // Make sure that it's not a port we've already tried
// if (matchingPeerPort != port && matchingPeerPort != defaultPort) {
// success = Network.getInstance().requestDataFromPeer(matchingPeerAddress, signature);
// if (success) {
// // Successfully connected, so stop making connections
// break;
// }
// }
// }
// }
//}
if (success) {
// We were able to connect with a peer, so track the request
RNSArbitraryDataFileListManager.getInstance().addToSignatureRequests(signature58, false, true);
}
}
} catch (InterruptedException e) {
// Do nothing
}
return success;
}
// Relays
private List<RNSArbitraryRelayInfo> getRelayInfoListForHash(String hash58) {
synchronized (arbitraryRelayMap) {
return arbitraryRelayMap.stream()
.filter(relayInfo -> Objects.equals(relayInfo.getHash58(), hash58))
.collect(Collectors.toList());
}
}
private RNSArbitraryRelayInfo getOptimalRelayInfoEntryForHash(String hash58) {
LOGGER.trace("Fetching relay info for hash: {}", hash58);
List<RNSArbitraryRelayInfo> relayInfoList = this.getRelayInfoListForHash(hash58);
if (relayInfoList != null && !relayInfoList.isEmpty()) {
// Remove any with null requestHops
relayInfoList.removeIf(r -> r.getRequestHops() == null);
// If list is now empty, then just return one at random
if (relayInfoList.isEmpty()) {
return this.getRandomRelayInfoEntryForHash(hash58);
}
// Sort by number of hops (lowest first)
relayInfoList.sort(Comparator.comparingInt(RNSArbitraryRelayInfo::getRequestHops));
// FUTURE: secondary sort by requestTime?
RNSArbitraryRelayInfo relayInfo = relayInfoList.get(0);
LOGGER.trace("Returning optimal relay info for hash: {} (requestHops {})", hash58, relayInfo.getRequestHops());
return relayInfo;
}
LOGGER.trace("No relay info exists for hash: {}", hash58);
return null;
}
private RNSArbitraryRelayInfo getRandomRelayInfoEntryForHash(String hash58) {
LOGGER.trace("Fetching random relay info for hash: {}", hash58);
List<RNSArbitraryRelayInfo> relayInfoList = this.getRelayInfoListForHash(hash58);
if (relayInfoList != null && !relayInfoList.isEmpty()) {
// Pick random item
int index = new SecureRandom().nextInt(relayInfoList.size());
LOGGER.trace("Returning random relay info for hash: {} (index {})", hash58, index);
return relayInfoList.get(index);
}
LOGGER.trace("No relay info exists for hash: {}", hash58);
return null;
}
public void addToRelayMap(RNSArbitraryRelayInfo newEntry) {
if (newEntry == null || !newEntry.isValid()) {
return;
}
// Remove existing entry for this peer if it exists, to renew the timestamp
this.removeFromRelayMap(newEntry);
// Re-add
arbitraryRelayMap.add(newEntry);
LOGGER.debug("Added entry to relay map: {}", newEntry);
}
private void removeFromRelayMap(RNSArbitraryRelayInfo entry) {
arbitraryRelayMap.removeIf(relayInfo -> relayInfo.equals(entry));
}
// Peers requesting QDN data from us
/**
* Add an address string of a peer that is trying to request data from us.
* @param peerAddress
*/
public void addRecentDataRequest(String peerAddress) {
if (peerAddress == null) {
return;
}
Long now = NTP.getTime();
if (now == null) {
return;
}
// Make sure to remove the port, since it isn't guaranteed to match next time
String[] parts = peerAddress.split(":");
if (parts.length == 0) {
return;
}
String host = parts[0];
if (!InetAddresses.isInetAddress(host)) {
// Invalid host
return;
}
this.recentDataRequests.put(host, now);
}
public boolean isPeerRequestingData(String peerAddressWithoutPort) {
return this.recentDataRequests.containsKey(peerAddressWithoutPort);
}
public boolean hasPendingDataRequest() {
return !this.recentDataRequests.isEmpty();
}
// Network handlers
public void onNetworkGetArbitraryDataFileMessage(RNSPeer peer, Message message) {
// Don't respond if QDN is disabled
if (!Settings.getInstance().isQdnEnabled()) {
return;
}
GetArbitraryDataFileMessage getArbitraryDataFileMessage = (GetArbitraryDataFileMessage) message;
byte[] hash = getArbitraryDataFileMessage.getHash();
String hash58 = Base58.encode(hash);
byte[] signature = getArbitraryDataFileMessage.getSignature();
Controller.getInstance().stats.getArbitraryDataFileMessageStats.requests.incrementAndGet();
LOGGER.debug("Received GetArbitraryDataFileMessage from peer {} for hash {}", peer, Base58.encode(hash));
try {
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
RNSArbitraryRelayInfo relayInfo = this.getOptimalRelayInfoEntryForHash(hash58);
if (arbitraryDataFile.exists()) {
LOGGER.trace("Hash {} exists", hash58);
// We can serve the file directly as we already have it
LOGGER.debug("Sending file {}...", arbitraryDataFile);
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
arbitraryDataFileMessage.setId(message.getId());
//if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
// LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
// peer.disconnect("failed to send file");
//}
//else {
// LOGGER.debug("Sent file {}", arbitraryDataFile);
//}
peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
}
//// TODO: rework (doesn't work with Reticulum)
//else if (relayInfo != null) {
// LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
// // We need to ask this peer for the file
// Peer peerToAsk = relayInfo.getPeer();
// if (peerToAsk != null) {
//
// // Forward the message to this peer
// LOGGER.debug("Asking peer {} for hash {}", peerToAsk, hash58);
// // No need to pass arbitraryTransactionData below because this is only used for metadata caching,
// // and metadata isn't retained when relaying.
// this.fetchArbitraryDataFile(peerToAsk, peer, null, signature, hash, message);
// }
// else {
// LOGGER.debug("Peer {} not found in relay info", peer);
// }
//}
else {
LOGGER.debug("Hash {} doesn't exist and we don't have relay info", hash58);
// We don't have this file
Controller.getInstance().stats.getArbitraryDataFileMessageStats.unknownFiles.getAndIncrement();
// 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));
//// 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");
// peer.disconnect("failed to send file-unknown response");
//}
//else {
// LOGGER.debug("Sent file-unknown response for file {}", arbitraryDataFile);
//}
Message fileUnknownMessage = new GenericUnknownMessage();
peer.sendMessage(fileUnknownMessage);
}
}
catch (DataException e) {
LOGGER.debug("Unable to handle request for arbitrary data file: {}", hash58);
}
}
}

View File

@@ -0,0 +1,130 @@
package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.RNSArbitraryFileListResponseInfo;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.EventBus;
import org.qortal.network.RNSPeer;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import static java.lang.Thread.NORM_PRIORITY;
public class RNSArbitraryDataFileRequestThread implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
public RNSArbitraryDataFileRequestThread() {
}
@Override
public void run() {
Thread.currentThread().setName("Arbitrary Data File Request Thread");
Thread.currentThread().setPriority(NORM_PRIORITY);
try {
while (!Controller.isStopping()) {
Long now = NTP.getTime();
this.processFileHashes(now);
}
} catch (InterruptedException e) {
// Fall-through to exit thread...
}
}
private void processFileHashes(Long now) throws InterruptedException {
if (Controller.isStopping()) {
return;
}
RNSArbitraryDataFileManager arbitraryDataFileManager = RNSArbitraryDataFileManager.getInstance();
String signature58 = null;
String hash58 = null;
RNSPeer peer = null;
boolean shouldProcess = false;
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
if (!arbitraryDataFileManager.arbitraryDataFileHashResponses.isEmpty()) {
// Sort by lowest number of node hops first
Comparator<RNSArbitraryFileListResponseInfo> lowestHopsFirstComparator =
Comparator.comparingInt(RNSArbitraryFileListResponseInfo::getRequestHops);
arbitraryDataFileManager.arbitraryDataFileHashResponses.sort(lowestHopsFirstComparator);
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.iterator();
while (iterator.hasNext()) {
if (Controller.isStopping()) {
return;
}
RNSArbitraryFileListResponseInfo responseInfo = (RNSArbitraryFileListResponseInfo) iterator.next();
if (responseInfo == null) {
iterator.remove();
continue;
}
hash58 = responseInfo.getHash58();
peer = responseInfo.getPeer();
signature58 = responseInfo.getSignature58();
Long timestamp = responseInfo.getTimestamp();
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
// Ignore - to be deleted
iterator.remove();
continue;
}
// Skip if already requesting, but don't remove, as we might want to retry later
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(hash58)) {
// Already requesting - leave this attempt for later
continue;
}
// We want to process this file
shouldProcess = true;
iterator.remove();
break;
}
}
}
if (!shouldProcess) {
// Nothing to do
Thread.sleep(1000L);
return;
}
byte[] hash = Base58.decode(hash58);
byte[] signature = Base58.decode(signature58);
// Fetch the transaction data
try (final Repository repository = RepositoryManager.getRepository()) {
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
if (arbitraryTransactionData == null) {
return;
}
if (signature == null || hash == null || peer == null || arbitraryTransactionData == null) {
return;
}
LOGGER.trace("Fetching file {} from peer {} via request thread...", hash58, peer);
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
} catch (DataException e) {
LOGGER.debug("Unable to process file hashes: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,481 @@
package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataResource;
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
import org.qortal.controller.Controller;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.ArbitraryMetadataMessage;
import org.qortal.network.message.GetArbitraryMetadataMessage;
import org.qortal.network.message.Message;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.ListUtils;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
import java.io.IOException;
import java.util.*;
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
public class RNSArbitraryMetadataManager {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryMetadataManager.class);
private static RNSArbitraryMetadataManager instance;
/**
* Map of recent incoming requests for ARBITRARY transaction metadata.
* <p>
* Key is original request's message ID<br>
* Value is Triple&lt;transaction signature in base58, first requesting peer, first request's timestamp&gt;
* <p>
* If peer is null then either:<br>
* <ul>
* <li>we are the original requesting peer</li>
* <li>we have already sent data payload to original requesting peer.</li>
* </ul>
* If signature is null then we have already received the file list and either:<br>
* <ul>
* <li>we are the original requesting peer and have processed it</li>
* <li>we have forwarded the metadata</li>
* </ul>
*/
public Map<Integer, Triple<String, RNSPeer, Long>> arbitraryMetadataRequests = Collections.synchronizedMap(new HashMap<>());
/**
* Map to keep track of in progress arbitrary metadata requests
* Key: string - the signature encoded in base58
* Value: Triple<networkBroadcastCount, directPeerRequestCount, lastAttemptTimestamp>
*/
private Map<String, Triple<Integer, Integer, Long>> arbitraryMetadataSignatureRequests = Collections.synchronizedMap(new HashMap<>());
private RNSArbitraryMetadataManager() {
}
public static RNSArbitraryMetadataManager getInstance() {
if (instance == null)
instance = new RNSArbitraryMetadataManager();
return instance;
}
public void cleanupRequestCache(Long now) {
if (now == null) {
return;
}
final long requestMinimumTimestamp = now - ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT;
arbitraryMetadataRequests.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < requestMinimumTimestamp);
}
public ArbitraryDataTransactionMetadata fetchMetadata(ArbitraryDataResource arbitraryDataResource, boolean useRateLimiter) {
try (final Repository repository = RepositoryManager.getRepository()) {
// Find latest transaction
ArbitraryTransactionData latestTransaction = repository.getArbitraryRepository()
.getLatestTransaction(arbitraryDataResource.getResourceId(), arbitraryDataResource.getService(),
null, arbitraryDataResource.getIdentifier());
if (latestTransaction != null) {
byte[] signature = latestTransaction.getSignature();
byte[] metadataHash = latestTransaction.getMetadataHash();
if (metadataHash == null) {
// This resource doesn't have metadata
throw new IllegalArgumentException("This resource doesn't have metadata");
}
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
if (!metadataFile.exists()) {
// Request from network
this.fetchArbitraryMetadata(latestTransaction, useRateLimiter);
}
// Now check again as it may have been downloaded above
if (metadataFile.exists()) {
// Use local copy
ArbitraryDataTransactionMetadata transactionMetadata = new ArbitraryDataTransactionMetadata(metadataFile.getFilePath());
try {
transactionMetadata.read();
} catch (DataException e) {
// Invalid file, so delete it
LOGGER.info("Deleting invalid metadata file due to exception: {}", e.getMessage());
transactionMetadata.delete();
return null;
}
return transactionMetadata;
}
}
} catch (DataException | IOException e) {
LOGGER.error("Repository issue when fetching arbitrary transaction metadata", e);
}
return null;
}
// Request metadata from network
public byte[] fetchArbitraryMetadata(ArbitraryTransactionData arbitraryTransactionData, boolean useRateLimiter) {
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
if (metadataHash == null) {
return null;
}
byte[] signature = arbitraryTransactionData.getSignature();
String signature58 = Base58.encode(signature);
// Require an NTP sync
Long now = NTP.getTime();
if (now == null) {
return null;
}
// If we've already tried too many times in a short space of time, make sure to give up
if (useRateLimiter && !this.shouldMakeMetadataRequestForSignature(signature58)) {
LOGGER.trace("Skipping metadata request for signature {} due to rate limit", signature58);
return null;
}
this.addToSignatureRequests(signature58, true, false);
//List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
List<RNSPeer> handshakedPeers = RNSNetwork.getInstance().getLinkedPeers();
LOGGER.debug(String.format("Sending metadata request for signature %s to %d peers...", signature58, handshakedPeers.size()));
// Build request
Message getArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, now, 0);
// Save our request into requests map
Triple<String, RNSPeer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
// Assign random ID to this message
int id;
do {
id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
// Put queue into map (keyed by message ID) so we can poll for a response
// If putIfAbsent() doesn't return null, then this ID is already taken
} while (arbitraryMetadataRequests.put(id, requestEntry) != null);
getArbitraryMetadataMessage.setId(id);
// Broadcast request
RNSNetwork.getInstance().broadcast(peer -> getArbitraryMetadataMessage);
// Poll to see if data has arrived
final long singleWait = 100;
long totalWait = 0;
while (totalWait < ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT) {
try {
Thread.sleep(singleWait);
} catch (InterruptedException e) {
break;
}
requestEntry = arbitraryMetadataRequests.get(id);
if (requestEntry == null)
return null;
if (requestEntry.getA() == null)
break;
totalWait += singleWait;
}
try {
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
if (metadataFile.exists()) {
return metadataFile.getBytes();
}
} catch (DataException e) {
// Do nothing
}
return null;
}
// Track metadata lookups by signature
private boolean shouldMakeMetadataRequestForSignature(String signature58) {
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
if (request == null) {
// Not attempted yet
return true;
}
// Extract the components
Integer networkBroadcastCount = request.getA();
// Integer directPeerRequestCount = request.getB();
Long lastAttemptTimestamp = request.getC();
if (lastAttemptTimestamp == null) {
// Not attempted yet
return true;
}
long timeSinceLastAttempt = NTP.getTime() - lastAttemptTimestamp;
// Allow a second attempt after 60 seconds
if (timeSinceLastAttempt > 60 * 1000L) {
// We haven't tried for at least 60 seconds
if (networkBroadcastCount < 2) {
// We've made less than 2 total attempts
return true;
}
}
// Then allow another attempt after 60 minutes
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
// We haven't tried for at least 60 minutes
if (networkBroadcastCount < 3) {
// We've made less than 3 total attempts
return true;
}
}
return false;
}
public boolean isSignatureRateLimited(byte[] signature) {
String signature58 = Base58.encode(signature);
return !this.shouldMakeMetadataRequestForSignature(signature58);
}
public long lastRequestForSignature(byte[] signature) {
String signature58 = Base58.encode(signature);
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
if (request == null) {
// Not attempted yet
return 0;
}
// Extract the components
Long lastAttemptTimestamp = request.getC();
if (lastAttemptTimestamp != null) {
return lastAttemptTimestamp;
}
return 0;
}
public void addToSignatureRequests(String signature58, boolean incrementNetworkRequests, boolean incrementPeerRequests) {
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
Long now = NTP.getTime();
if (request == null) {
// No entry yet
Triple<Integer, Integer, Long> newRequest = new Triple<>(0, 0, now);
arbitraryMetadataSignatureRequests.put(signature58, newRequest);
}
else {
// There is an existing entry
if (incrementNetworkRequests) {
request.setA(request.getA() + 1);
}
if (incrementPeerRequests) {
request.setB(request.getB() + 1);
}
request.setC(now);
arbitraryMetadataSignatureRequests.put(signature58, request);
}
}
public void removeFromSignatureRequests(String signature58) {
arbitraryMetadataSignatureRequests.remove(signature58);
}
// Network handlers
public void onNetworkArbitraryMetadataMessage(RNSPeer peer, Message message) {
// Don't process if QDN is disabled
if (!Settings.getInstance().isQdnEnabled()) {
return;
}
ArbitraryMetadataMessage arbitraryMetadataMessage = (ArbitraryMetadataMessage) message;
LOGGER.debug("Received metadata from peer {}", peer);
// Do we have a pending request for this data?
Triple<String, RNSPeer, Long> request = arbitraryMetadataRequests.get(message.getId());
if (request == null || request.getA() == null) {
return;
}
boolean isRelayRequest = (request.getB() != null);
// Does this message's signature match what we're expecting?
byte[] signature = arbitraryMetadataMessage.getSignature();
String signature58 = Base58.encode(signature);
if (!request.getA().equals(signature58)) {
return;
}
// Update requests map to reflect that we've received this metadata
Triple<String, RNSPeer, Long> newEntry = new Triple<>(null, null, request.getC());
arbitraryMetadataRequests.put(message.getId(), newEntry);
// Get transaction info
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (!(transactionData instanceof ArbitraryTransactionData)) {
return;
}
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
// Check if the name is blocked
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
// Save if not blocked
ArbitraryDataFile arbitraryMetadataFile = arbitraryMetadataMessage.getArbitraryMetadataFile();
if (!isBlocked && arbitraryMetadataFile != null) {
arbitraryMetadataFile.save();
}
// Forwarding
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
if (!isBlocked) {
RNSPeer requestingPeer = request.getB();
if (requestingPeer != null) {
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
forwardArbitraryMetadataMessage.setId(arbitraryMetadataMessage.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
//if (!requestingPeer.sendMessage(forwardArbitraryMetadataMessage)) {
// requestingPeer.disconnect("failed to forward arbitrary metadata");
//}
requestingPeer.sendMessage(forwardArbitraryMetadataMessage);
}
}
}
// Add to resource queue to update arbitrary resource caches
if (arbitraryTransactionData != null) {
ArbitraryDataCacheManager.getInstance().addToUpdateQueue(arbitraryTransactionData);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while saving arbitrary transaction metadata from peer %s", peer), e);
}
}
public void onNetworkGetArbitraryMetadataMessage(RNSPeer peer, Message message) {
// Don't respond if QDN is disabled
if (!Settings.getInstance().isQdnEnabled()) {
return;
}
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) message;
byte[] signature = getArbitraryMetadataMessage.getSignature();
String signature58 = Base58.encode(signature);
Long now = NTP.getTime();
Triple<String, RNSPeer, Long> newEntry = new Triple<>(signature58, peer, now);
// If we've seen this request recently, then ignore
if (arbitraryMetadataRequests.putIfAbsent(message.getId(), newEntry) != null) {
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peer, signature58);
return;
}
LOGGER.debug("Received metadata request from peer {} for signature {}", peer, signature58);
ArbitraryTransactionData transactionData = null;
ArbitraryDataFile metadataFile = null;
try (final Repository repository = RepositoryManager.getRepository()) {
// Firstly we need to lookup this file on chain to get its metadata hash
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
if (transactionData instanceof ArbitraryTransactionData) {
// Check if we're even allowed to serve metadata for this transaction
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
byte[] metadataHash = transactionData.getMetadataHash();
if (metadataHash != null) {
// Load metadata file
metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
}
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata for peer %s", peer), e);
}
// We should only respond if we have the metadata file
if (metadataFile != null && metadataFile.exists()) {
// We have the metadata file, so update requests map to reflect that we've sent it
newEntry = new Triple<>(null, null, now);
arbitraryMetadataRequests.put(message.getId(), newEntry);
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, metadataFile);
arbitraryMetadataMessage.setId(message.getId());
//if (!peer.sendMessage(arbitraryMetadataMessage)) {
// LOGGER.debug("Couldn't send metadata");
// peer.disconnect("failed to send metadata");
// return;
//}
peer.sendMessage(arbitraryMetadataMessage);
LOGGER.debug("Sent metadata");
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
LOGGER.debug("No need for any forwarding because metadata request is fully served");
return;
}
// We may need to forward this request on
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
// In relay mode - so ask our other peers if they have it
long requestTime = getArbitraryMetadataMessage.getRequestTime();
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
long totalRequestTime = now - requestTime;
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
// Relay request hasn't timed out yet, so can potentially be rebroadcast
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
relayGetArbitraryMetadataMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
//Network.getInstance().broadcast(
// broadcastPeer ->
// !broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
// broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
RNSNetwork.getInstance().broadcast(broadcastPeer -> relayGetArbitraryMetadataMessage);
}
else {
// This relay request has reached the maximum number of allowed hops
}
}
else {
// This relay request has timed out
}
}
}
}

View File

@@ -0,0 +1,778 @@
package org.qortal.controller.tradebot;
import com.google.common.primitives.Longs;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.ECKey;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.controller.Controller;
import org.qortal.controller.Synchronizer;
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.network.TradePresenceData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.gui.SysTray;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.GetTradePresencesMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.TradePresencesMessage;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBImportExport;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.utils.ByteArray;
import org.qortal.utils.NTP;
import java.awt.TrayIcon.MessageType;
import java.security.SecureRandom;
import java.util.*;
import java.util.function.Supplier;
/**
* Performing cross-chain trading steps on behalf of user.
* <p>
* We deal with three different independent state-spaces here:
* <ul>
* <li>Qortal blockchain</li>
* <li>Foreign blockchain</li>
* <li>Trade-bot entries</li>
* </ul>
*/
public class RNSTradeBot implements Listener {
private static final Logger LOGGER = LogManager.getLogger(TradeBot.class);
private static final Random RANDOM = new SecureRandom();
/** Maximum lifetime of trade presence timestamp. 30 mins in ms. */
private static final long PRESENCE_LIFETIME = 30 * 60 * 1000L;
/** How soon before expiry of our own trade presence timestamp that we want to trigger renewal. 5 mins in ms. */
private static final long EARLY_RENEWAL_PERIOD = 5 * 60 * 1000L;
/** Trade presence timestamps are rounded up to this nearest interval. Bigger values improve grouping of entries in [GET_]TRADE_PRESENCES network messages. 15 mins in ms. */
private static final long EXPIRY_ROUNDING = 15 * 60 * 1000L;
/** How often we want to broadcast our list of all known trade presences to peers. 5 mins in ms. */
private static final long PRESENCE_BROADCAST_INTERVAL = 5 * 60 * 1000L;
public interface StateNameAndValueSupplier {
public String getState();
public int getStateValue();
}
public static class StateChangeEvent implements Event {
private final TradeBotData tradeBotData;
public StateChangeEvent(TradeBotData tradeBotData) {
this.tradeBotData = tradeBotData;
}
public TradeBotData getTradeBotData() {
return this.tradeBotData;
}
}
public static class TradePresenceEvent implements Event {
private final TradePresenceData tradePresenceData;
public TradePresenceEvent(TradePresenceData tradePresenceData) {
this.tradePresenceData = tradePresenceData;
}
public TradePresenceData getTradePresenceData() {
return this.tradePresenceData;
}
}
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
static {
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(PirateChainACCTv3.class, PirateChainACCTv3TradeBot::getInstance);
}
private static RNSTradeBot instance;
private final Map<ByteArray, Long> ourTradePresenceTimestampsByPubkey = Collections.synchronizedMap(new HashMap<>());
private final List<TradePresenceData> pendingTradePresences = Collections.synchronizedList(new ArrayList<>());
private final Map<ByteArray, TradePresenceData> allTradePresencesByPubkey = Collections.synchronizedMap(new HashMap<>());
private Map<ByteArray, TradePresenceData> safeAllTradePresencesByPubkey = Collections.emptyMap();
private long nextTradePresenceBroadcastTimestamp = 0L;
private Map<String, Long> failedTrades = new HashMap<>();
private Map<String, Long> validTrades = new HashMap<>();
private RNSTradeBot() {
EventBus.INSTANCE.addListener(event -> RNSTradeBot.getInstance().listen(event));
}
public static synchronized RNSTradeBot getInstance() {
if (instance == null)
instance = new RNSTradeBot();
return instance;
}
public ACCT getAcctUsingAtData(ATData atData) {
byte[] codeHash = atData.getCodeHash();
if (codeHash == null)
return null;
return SupportedBlockchain.getAcctByCodeHash(codeHash);
}
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ACCT acct = this.getAcctUsingAtData(atData);
if (acct == null)
return null;
return acct.populateTradeData(repository, atData);
}
/**
* Creates a new trade-bot entry from the "Bob" viewpoint,
* i.e. OFFERing QORT in exchange for foreign blockchain currency.
* <p>
* Generates:
* <ul>
* <li>new 'trade' private key</li>
* <li>secret(s)</li>
* </ul>
* Derives:
* <ul>
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
* <li>'foreign' public key, public key hash</li>
* <li>hash(es) of secret(s)</li>
* </ul>
* A Qortal AT is then constructed including the following as constants in the 'data segment':
* <ul>
* <li>'native' (Qortal) 'trade' address - used to MESSAGE AT</li>
* <li>'foreign' public key hash - used by Alice's to allow redeem of currency on foreign blockchain</li>
* <li>hash(es) of secret(s) - used by AT (optional) and foreign blockchain as needed</li>
* <li>QORT amount on offer by Bob</li>
* <li>foreign currency amount expected in return by Bob (from Alice)</li>
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
* </ul>
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
* <p>
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
* <p>
* @param repository
* @param tradeBotCreateRequest
* @return raw, unsigned DEPLOY_AT transaction
* @throws DataException
*/
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
// Fetch latest ACCT version for requested foreign blockchain
ACCT acct = tradeBotCreateRequest.foreignBlockchain.getLatestAcct();
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
if (acctTradeBot == null)
return null;
return acctTradeBot.createTrade(repository, tradeBotCreateRequest);
}
/**
* Creates a trade-bot entry from the 'Alice' viewpoint,
* i.e. matching foreign blockchain currency to an existing QORT offer.
* <p>
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a foreign blockchain wallet via <tt>foreignKey</tt>.
* <p>
* @param repository
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
* @param foreignKey foreign blockchain wallet key
* @throws DataException
*/
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct,
CrossChainTradeData crossChainTradeData, String foreignKey, String receivingAddress) throws DataException {
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
if (acctTradeBot == null) {
LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot for AT %s", atData.getATAddress()));
return ResponseResult.NETWORK_ISSUE;
}
// Check Alice doesn't already have an existing, on-going trade-bot entry for this AT.
if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(atData.getATAddress(), acctTradeBot.getEndStates()))
return ResponseResult.TRADE_ALREADY_EXISTS;
return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress);
}
/**
* Creates a trade-bot entries from the 'Alice' viewpoint,
* i.e. matching foreign blockchain currency to existing QORT offers.
* <p>
* Requires chosen trade offers from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a foreign blockchain wallet via <tt>foreignKey</tt>.
* <p>
* @param repository
* @param crossChainTradeDataList chosen trade OFFERs that Alice wants to match
* @param receiveAddress Alice's Qortal address to receive her QORT
* @param foreignKey foreign blockchain wallet key
* @param bitcoiny
* @throws DataException
*/
public ResponseResult startResponseMultiple(
Repository repository,
ACCT acct,
List<CrossChainTradeData> crossChainTradeDataList,
String receiveAddress,
String foreignKey,
Bitcoiny bitcoiny) throws DataException {
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
if (acctTradeBot == null) {
LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot for %s", acct.getBlockchain()));
return ResponseResult.NETWORK_ISSUE;
}
for( CrossChainTradeData tradeData : crossChainTradeDataList) {
// Check Alice doesn't already have an existing, on-going trade-bot entry for this AT.
if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(tradeData.qortalAtAddress, acctTradeBot.getEndStates()))
return ResponseResult.TRADE_ALREADY_EXISTS;
}
return TradeBotUtils.startResponseMultiple(repository, acct, crossChainTradeDataList, receiveAddress, foreignKey, bitcoiny);
}
public boolean deleteEntry(Repository repository, byte[] tradePrivateKey) throws DataException {
TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
if (tradeBotData == null)
// Can't delete what we don't have!
return false;
boolean canDelete = false;
ACCT acct = SupportedBlockchain.getAcctByName(tradeBotData.getAcctName());
if (acct == null)
// We can't/no longer support this ACCT
canDelete = true;
else {
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
canDelete = acctTradeBot == null || acctTradeBot.canDelete(repository, tradeBotData);
}
if (canDelete) {
repository.getCrossChainRepository().delete(tradePrivateKey);
repository.saveChanges();
}
return canDelete;
}
@Override
public void listen(Event event) {
if (!(event instanceof Synchronizer.NewChainTipEvent))
return;
// Don't process trade bots or broadcast presence timestamps if our chain is more than 60 minutes old
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
return;
synchronized (this) {
expireOldPresenceTimestamps();
List<TradeBotData> allTradeBotData;
try (final Repository repository = RepositoryManager.getRepository()) {
allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
} catch (DataException e) {
LOGGER.error("Couldn't run trade bot due to repository issue", e);
return;
}
for (TradeBotData tradeBotData : allTradeBotData)
try (final Repository repository = RepositoryManager.getRepository()) {
// Find ACCT-specific trade-bot for this entry
ACCT acct = SupportedBlockchain.getAcctByName(tradeBotData.getAcctName());
if (acct == null) {
LOGGER.debug(() -> String.format("Couldn't find ACCT matching name %s", tradeBotData.getAcctName()));
continue;
}
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
if (acctTradeBot == null) {
LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot matching name %s", tradeBotData.getAcctName()));
continue;
}
acctTradeBot.progress(repository, tradeBotData);
} catch (DataException e) {
LOGGER.error("Couldn't run trade bot due to repository issue", e);
} catch (ForeignBlockchainException e) {
LOGGER.warn(() -> String.format("Foreign blockchain issue processing trade-bot entry for AT %s: %s", tradeBotData.getAtAddress(), e.getMessage()));
}
broadcastPresenceTimestamps();
}
}
public static byte[] generateTradePrivateKey() {
// The private key is used for both Curve25519 and secp256k1 so needs to be valid for both.
// Curve25519 accepts any seed, so generate a valid secp256k1 key and use that.
return new ECKey().getPrivKeyBytes();
}
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
return Crypto.toPublicKey(privateKey);
}
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
return ECKey.fromPrivate(privateKey).getPubKey();
}
/*package*/ public static byte[] generateSecret() {
byte[] secret = new byte[32];
RANDOM.nextBytes(secret);
return secret;
}
/*package*/ static void backupTradeBotData(Repository repository, List<TradeBotData> additional) {
// Attempt to backup the trade bot data. This an optional step and doesn't impact trading, so don't throw an exception on failure
try {
LOGGER.info("About to backup trade bot data...");
HSQLDBImportExport.backupTradeBotStates(repository, additional);
} catch (DataException e) {
LOGGER.info(String.format("Repository issue when exporting trade bot data: %s", e.getMessage()));
}
}
/** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
/*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData,
String newState, int newStateValue, Supplier<String> logMessageSupplier) throws DataException {
tradeBotData.setState(newState);
tradeBotData.setStateValue(newStateValue);
tradeBotData.setTimestamp(NTP.getTime());
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
if (Settings.getInstance().isTradebotSystrayEnabled())
SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO);
if (logMessageSupplier != null)
LOGGER.info(logMessageSupplier.get());
LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState));
notifyStateChange(tradeBotData);
}
/** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
/*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData, StateNameAndValueSupplier newStateSupplier, Supplier<String> logMessageSupplier) throws DataException {
updateTradeBotState(repository, tradeBotData, newStateSupplier.getState(), newStateSupplier.getStateValue(), logMessageSupplier);
}
/** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
/*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData, Supplier<String> logMessageSupplier) throws DataException {
updateTradeBotState(repository, tradeBotData, tradeBotData.getState(), tradeBotData.getStateValue(), logMessageSupplier);
}
/*package*/ static void notifyStateChange(TradeBotData tradeBotData) {
StateChangeEvent stateChangeEvent = new StateChangeEvent(tradeBotData);
EventBus.INSTANCE.notify(stateChangeEvent);
}
/*package*/ static AcctTradeBot findTradeBotForAcct(ACCT acct) {
Supplier<AcctTradeBot> acctTradeBotSupplier = acctTradeBotSuppliers.get(acct.getClass());
if (acctTradeBotSupplier == null)
return null;
return acctTradeBotSupplier.get();
}
// PRESENCE-related
public Collection<TradePresenceData> getAllTradePresences() {
return this.safeAllTradePresencesByPubkey.values();
}
/** Trade presence timestamps expire in the 'future' so any that reach 'now' have expired and are removed. */
private void expireOldPresenceTimestamps() {
long now = NTP.getTime();
int allRemovedCount = 0;
synchronized (this.allTradePresencesByPubkey) {
int preRemoveCount = this.allTradePresencesByPubkey.size();
this.allTradePresencesByPubkey.values().removeIf(tradePresenceData -> tradePresenceData.getTimestamp() <= now);
allRemovedCount = this.allTradePresencesByPubkey.size() - preRemoveCount;
}
int ourRemovedCount = 0;
synchronized (this.ourTradePresenceTimestampsByPubkey) {
int preRemoveCount = this.ourTradePresenceTimestampsByPubkey.size();
this.ourTradePresenceTimestampsByPubkey.values().removeIf(timestamp -> timestamp < now);
ourRemovedCount = this.ourTradePresenceTimestampsByPubkey.size() - preRemoveCount;
}
if (allRemovedCount > 0)
LOGGER.debug("Removed {} expired trade presences, of which {} ours", allRemovedCount, ourRemovedCount);
}
/*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData, CrossChainTradeData tradeData)
throws DataException {
String atAddress = tradeBotData.getAtAddress();
PrivateKeyAccount tradeNativeAccount = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
String signerAddress = tradeNativeAccount.getAddress();
/*
* There's no point in Alice trying to broadcast presence for an AT that isn't locked to her,
* as other peers won't be able to verify as signing public key isn't yet in the AT's data segment.
*/
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
// Signer is neither Bob, nor trade locked to Alice
LOGGER.trace("Can't provide trade presence for our AT {} as it's not yet locked to Alice", atAddress);
return;
}
long now = NTP.getTime();
long newExpiry = generateExpiry(now);
ByteArray pubkeyByteArray = ByteArray.wrap(tradeNativeAccount.getPublicKey());
// If map entry's timestamp is missing, or within early renewal period, use the new expiry - otherwise use existing timestamp.
synchronized (this.ourTradePresenceTimestampsByPubkey) {
Long currentTimestamp = this.ourTradePresenceTimestampsByPubkey.get(pubkeyByteArray);
if (currentTimestamp != null && currentTimestamp - now > EARLY_RENEWAL_PERIOD) {
// timestamp still good
LOGGER.trace("Current trade presence timestamp {} still good for our trade {}", currentTimestamp, atAddress);
return;
}
this.ourTradePresenceTimestampsByPubkey.put(pubkeyByteArray, newExpiry);
}
// Create signature
byte[] signature = tradeNativeAccount.sign(Longs.toByteArray(newExpiry));
// Add new trade presence to queue to be broadcast around network
TradePresenceData tradePresenceData = new TradePresenceData(newExpiry, tradeNativeAccount.getPublicKey(), signature, atAddress);
this.pendingTradePresences.add(tradePresenceData);
this.allTradePresencesByPubkey.put(pubkeyByteArray, tradePresenceData);
rebuildSafeAllTradePresences();
LOGGER.trace("New trade presence timestamp {} for our trade {}", newExpiry, atAddress);
EventBus.INSTANCE.notify(new TradePresenceEvent(tradePresenceData));
}
private void rebuildSafeAllTradePresences() {
synchronized (this.allTradePresencesByPubkey) {
// Collect into a *new* unmodifiable map.
this.safeAllTradePresencesByPubkey = Map.copyOf(this.allTradePresencesByPubkey);
}
}
private void broadcastPresenceTimestamps() {
// If we have new trade presences that are pending broadcast, send those as a priority
if (!this.pendingTradePresences.isEmpty()) {
// Create a copy for Network to safely use in another thread
List<TradePresenceData> safeTradePresences;
synchronized (this.pendingTradePresences) {
safeTradePresences = List.copyOf(this.pendingTradePresences);
this.pendingTradePresences.clear();
}
LOGGER.debug("Broadcasting {} new trade presences", safeTradePresences.size());
TradePresencesMessage tradePresencesMessage = new TradePresencesMessage(safeTradePresences);
RNSNetwork.getInstance().broadcast(peer -> tradePresencesMessage);
return;
}
// As we have no new trade presences, check whether it's time to do a general broadcast
Long now = NTP.getTime();
if (now == null || now < nextTradePresenceBroadcastTimestamp)
return;
nextTradePresenceBroadcastTimestamp = now + PRESENCE_BROADCAST_INTERVAL;
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
);
GetTradePresencesMessage getTradePresencesMessage = new GetTradePresencesMessage(safeTradePresences);
RNSNetwork.getInstance().broadcast(peer -> getTradePresencesMessage);
}
// Network message processing
public void onGetTradePresencesMessage(RNSPeer peer, Message message) {
GetTradePresencesMessage getTradePresencesMessage = (GetTradePresencesMessage) message;
List<TradePresenceData> peersTradePresences = getTradePresencesMessage.getTradePresences();
// Create mutable copy from safe snapshot
Map<ByteArray, TradePresenceData> entriesUnknownToPeer = new HashMap<>(this.safeAllTradePresencesByPubkey);
int knownCount = entriesUnknownToPeer.size();
for (TradePresenceData peersTradePresence : peersTradePresences) {
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
TradePresenceData ourEntry = entriesUnknownToPeer.get(pubkeyByteArray);
if (ourEntry != null && ourEntry.getTimestamp() == peersTradePresence.getTimestamp())
entriesUnknownToPeer.remove(pubkeyByteArray);
}
if (entriesUnknownToPeer.isEmpty())
return;
LOGGER.debug("Sending {} trade presences to peer {} after excluding their {} from known {}",
entriesUnknownToPeer.size(), peer, peersTradePresences.size(), knownCount
);
// Send complement to peer
List<TradePresenceData> safeTradePresences = List.copyOf(entriesUnknownToPeer.values());
Message responseMessage = new TradePresencesMessage(safeTradePresences);
//if (!peer.sendMessage(responseMessage)) {
// peer.disconnect("failed to send TRADE_PRESENCES response");
// return;
//}
peer.sendMessage(responseMessage);
}
public void onTradePresencesMessage(RNSPeer peer, Message message) {
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) message;
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
long now = NTP.getTime();
// Timestamps before this are too far into the past
long pastThreshold = now;
// Timestamps after this are too far into the future
long futureThreshold = now + PRESENCE_LIFETIME;
Map<ByteArray, Supplier<ACCT>> acctSuppliersByCodeHash = SupportedBlockchain.getAcctMap();
int newCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
for (TradePresenceData peersTradePresence : peersTradePresences) {
long timestamp = peersTradePresence.getTimestamp();
// Ignore if timestamp is out of bounds
if (timestamp < pastThreshold || timestamp > futureThreshold) {
if (timestamp < pastThreshold)
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too old vs {}",
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
);
else
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too new vs {}",
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
);
continue;
}
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
// Ignore if we've previously verified this timestamp+publickey combo or sent timestamp is older
TradePresenceData existingTradeData = this.safeAllTradePresencesByPubkey.get(pubkeyByteArray);
if (existingTradeData != null && timestamp <= existingTradeData.getTimestamp()) {
if (timestamp == existingTradeData.getTimestamp())
LOGGER.trace("Ignoring trade presence {} from peer {} as we have verified timestamp {} before",
peersTradePresence.getAtAddress(), peer, timestamp
);
else
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is older than latest {}",
peersTradePresence.getAtAddress(), peer, timestamp, existingTradeData.getTimestamp()
);
continue;
}
// Check timestamp signature
byte[] timestampSignature = peersTradePresence.getSignature();
byte[] timestampBytes = Longs.toByteArray(timestamp);
byte[] publicKey = peersTradePresence.getPublicKey();
if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) {
LOGGER.trace("Ignoring trade presence {} from peer {} as signature failed to verify",
peersTradePresence.getAtAddress(), peer
);
continue;
}
ATData atData = repository.getATRepository().fromATAddress(peersTradePresence.getAtAddress());
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
if (atData == null)
LOGGER.trace("Ignoring trade presence {} from peer {} as AT doesn't exist",
peersTradePresence.getAtAddress(), peer
);
else
LOGGER.trace("Ignoring trade presence {} from peer {} as AT is frozen or finished",
peersTradePresence.getAtAddress(), peer
);
continue;
}
ByteArray atCodeHash = ByteArray.wrap(atData.getCodeHash());
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(atCodeHash);
if (acctSupplier == null) {
LOGGER.trace("Ignoring trade presence {} from peer {} as AT isn't a known ACCT?",
peersTradePresence.getAtAddress(), peer
);
continue;
}
CrossChainTradeData tradeData = acctSupplier.get().populateTradeData(repository, atData);
if (tradeData == null) {
LOGGER.trace("Ignoring trade presence {} from peer {} as trade data not found?",
peersTradePresence.getAtAddress(), peer
);
continue;
}
// Convert signer's public key to address form
String signerAddress = peersTradePresence.getTradeAddress();
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
LOGGER.trace("Ignoring trade presence {} from peer {} as signer isn't Alice or Bob?",
peersTradePresence.getAtAddress(), peer
);
continue;
}
// This is new to us
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
++newCount;
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
peersTradePresence.getAtAddress(), peer, timestamp
);
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
}
} catch (DataException e) {
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
}
if (newCount > 0) {
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
rebuildSafeAllTradePresences();
}
}
public void bridgePresence(long timestamp, byte[] publicKey, byte[] signature, String atAddress) {
long expiry = generateExpiry(timestamp);
ByteArray pubkeyByteArray = ByteArray.wrap(publicKey);
TradePresenceData fakeTradePresenceData = new TradePresenceData(expiry, publicKey, signature, atAddress);
// Only bridge if trade presence expiry timestamp is newer
TradePresenceData computedTradePresenceData = this.allTradePresencesByPubkey.compute(pubkeyByteArray, (k, v) ->
v == null || v.getTimestamp() < expiry ? fakeTradePresenceData : v
);
if (computedTradePresenceData == fakeTradePresenceData) {
LOGGER.trace("Bridged PRESENCE transaction for trade {} with timestamp {}", atAddress, expiry);
rebuildSafeAllTradePresences();
EventBus.INSTANCE.notify(new TradePresenceEvent(fakeTradePresenceData));
}
}
/** Decorates a CrossChainTradeData object with Alice / Bob trade-bot presence timestamp, if available. */
public void decorateTradeDataWithPresence(CrossChainTradeData crossChainTradeData) {
// Match by AT address, then check for Bob vs Alice
this.safeAllTradePresencesByPubkey.values().stream()
.filter(tradePresenceData -> tradePresenceData.getAtAddress().equals(crossChainTradeData.qortalAtAddress))
.forEach(tradePresenceData -> {
String signerAddress = tradePresenceData.getTradeAddress();
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
if (signerAddress.equals(crossChainTradeData.qortalCreatorTradeAddress))
crossChainTradeData.creatorPresenceExpiry = tradePresenceData.getTimestamp();
else if (signerAddress.equals(crossChainTradeData.qortalPartnerAddress))
crossChainTradeData.partnerPresenceExpiry = tradePresenceData.getTimestamp();
});
}
/** Removes any trades that have had multiple failures */
public List<CrossChainTradeData> removeFailedTrades(Repository repository, List<CrossChainTradeData> crossChainTrades) {
Long now = NTP.getTime();
if (now == null) {
return crossChainTrades;
}
List<CrossChainTradeData> updatedCrossChainTrades = new ArrayList<>(crossChainTrades);
int getMaxTradeOfferAttempts = Settings.getInstance().getMaxTradeOfferAttempts();
for (CrossChainTradeData crossChainTradeData : crossChainTrades) {
// We only care about trades in the OFFERING state
if (crossChainTradeData.mode != AcctMode.OFFERING) {
failedTrades.remove(crossChainTradeData.qortalAtAddress);
validTrades.remove(crossChainTradeData.qortalAtAddress);
continue;
}
// Return recently cached values if they exist
Long failedTimestamp = failedTrades.get(crossChainTradeData.qortalAtAddress);
if (failedTimestamp != null && now - failedTimestamp < 60 * 60 * 1000L) {
updatedCrossChainTrades.remove(crossChainTradeData);
//LOGGER.info("Removing cached failed trade AT {}", crossChainTradeData.qortalAtAddress);
continue;
}
Long validTimestamp = validTrades.get(crossChainTradeData.qortalAtAddress);
if (validTimestamp != null && now - validTimestamp < 60 * 60 * 1000L) {
//LOGGER.info("NOT removing cached valid trade AT {}", crossChainTradeData.qortalAtAddress);
continue;
}
try {
List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions(Arrays.asList(Transaction.TransactionType.MESSAGE), null, null, null, null);
for (TransactionData transactionData : transactions) {
// Treat as failed if buy attempt was more than 60 mins ago (as it's still in the OFFERING state)
if (transactionData.getRecipient().equals(crossChainTradeData.qortalCreatorTradeAddress) && now - transactionData.getTimestamp() > 60*60*1000L) {
failedTrades.put(crossChainTradeData.qortalAtAddress, now);
updatedCrossChainTrades.remove(crossChainTradeData);
} else {
validTrades.put(crossChainTradeData.qortalAtAddress, now);
}
}
} catch (DataException e) {
LOGGER.info("Unable to determine failed state of AT {}", crossChainTradeData.qortalAtAddress);
}
}
return updatedCrossChainTrades;
}
public boolean isFailedTrade(Repository repository, CrossChainTradeData crossChainTradeData) {
List<CrossChainTradeData> results = removeFailedTrades(repository, Arrays.asList(crossChainTradeData));
return results.isEmpty();
}
private long generateExpiry(long timestamp) {
return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME;
}
}

View File

@@ -0,0 +1,59 @@
package org.qortal.data.arbitrary;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class RNSArbitraryDirectConnectionInfo {
private final byte[] signature;
private final String peerAddress;
private final List<byte[]> hashes;
private final long timestamp;
public RNSArbitraryDirectConnectionInfo(byte[] signature, String peerAddress, List<byte[]> hashes, long timestamp) {
this.signature = signature;
this.peerAddress = peerAddress;
this.hashes = hashes;
this.timestamp = timestamp;
}
public byte[] getSignature() {
return this.signature;
}
public String getPeerAddress() {
return this.peerAddress;
}
public List<byte[]> getHashes() {
return this.hashes;
}
public long getTimestamp() {
return this.timestamp;
}
public int getHashCount() {
if (this.hashes == null) {
return 0;
}
return this.hashes.size();
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof ArbitraryDirectConnectionInfo))
return false;
ArbitraryDirectConnectionInfo otherDirectConnectionInfo = (ArbitraryDirectConnectionInfo) other;
return Arrays.equals(this.signature, otherDirectConnectionInfo.getSignature())
&& Objects.equals(this.peerAddress, otherDirectConnectionInfo.getPeerAddress())
&& Objects.equals(this.hashes, otherDirectConnectionInfo.getHashes())
&& Objects.equals(this.timestamp, otherDirectConnectionInfo.getTimestamp());
}
}

View File

@@ -0,0 +1,11 @@
package org.qortal.data.arbitrary;
import org.qortal.network.RNSPeer;
public class RNSArbitraryFileListResponseInfo extends RNSArbitraryRelayInfo {
public RNSArbitraryFileListResponseInfo(String hash58, String signature58, RNSPeer peer, Long timestamp, Long requestTime, Integer requestHops) {
super(hash58, signature58, peer, timestamp, requestTime, requestHops);
}
}

View File

@@ -0,0 +1,73 @@
package org.qortal.data.arbitrary;
import org.qortal.network.RNSPeer;
import java.util.Objects;
public class RNSArbitraryRelayInfo {
private final String hash58;
private final String signature58;
private final RNSPeer peer;
private final Long timestamp;
private final Long requestTime;
private final Integer requestHops;
public RNSArbitraryRelayInfo(String hash58, String signature58, RNSPeer peer, Long timestamp, Long requestTime, Integer requestHops) {
this.hash58 = hash58;
this.signature58 = signature58;
this.peer = peer;
this.timestamp = timestamp;
this.requestTime = requestTime;
this.requestHops = requestHops;
}
public boolean isValid() {
return this.getHash58() != null && this.getSignature58() != null
&& this.getPeer() != null && this.getTimestamp() != null;
}
public String getHash58() {
return this.hash58;
}
public String getSignature58() {
return signature58;
}
public RNSPeer getPeer() {
return peer;
}
public Long getTimestamp() {
return timestamp;
}
public Long getRequestTime() {
return this.requestTime;
}
public Integer getRequestHops() {
return this.requestHops;
}
@Override
public String toString() {
return String.format("%s = %s, %s, %d", this.hash58, this.signature58, this.peer, this.timestamp);
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof RNSArbitraryRelayInfo))
return false;
RNSArbitraryRelayInfo otherRelayInfo = (RNSArbitraryRelayInfo) other;
return this.peer == otherRelayInfo.getPeer()
&& Objects.equals(this.hash58, otherRelayInfo.getHash58())
&& Objects.equals(this.signature58, otherRelayInfo.getSignature58());
}
}

View File

@@ -0,0 +1,117 @@
package org.qortal.data.network;
import io.swagger.v3.oas.annotations.media.Schema;
import org.qortal.network.PeerAddress;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import static org.apache.commons.codec.binary.Hex.encodeHexString;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class RNSPeerData {
//public static final int MAX_PEER_ADDRESS_SIZE = 255;
// Properties
//// Don't expose this via JAXB - use pretty getter instead
//@XmlTransient
//@Schema(hidden = true)
//private PeerAddress peerAddress;
private byte[] peerAddress;
private Long lastAttempted;
private Long lastConnected;
private Long lastMisbehaved;
private Long addedWhen;
private String addedBy;
/** The number of consecutive times we failed to sync with this peer */
private int failedSyncCount = 0;
// Constructors
// necessary for JAXB serialization
protected RNSPeerData() {
}
public RNSPeerData(byte[] peerAddress, Long lastAttempted, Long lastConnected, Long lastMisbehaved, Long addedWhen, String addedBy) {
this.peerAddress = peerAddress;
this.lastAttempted = lastAttempted;
this.lastConnected = lastConnected;
this.lastMisbehaved = lastMisbehaved;
this.addedWhen = addedWhen;
this.addedBy = addedBy;
}
public RNSPeerData(byte[] peerAddress, Long addedWhen, String addedBy) {
this(peerAddress, null, null, null, addedWhen, addedBy);
}
public RNSPeerData(byte[] peerAddress) {
this(peerAddress, null, null, null, null, null);
}
// Getters / setters
// Don't let JAXB use this getter
@XmlTransient
@Schema(hidden = true)
public byte[] getAddress() {
return this.peerAddress;
}
public Long getLastAttempted() {
return this.lastAttempted;
}
public void setLastAttempted(Long lastAttempted) {
this.lastAttempted = lastAttempted;
}
public Long getLastConnected() {
return this.lastConnected;
}
public void setLastConnected(Long lastConnected) {
this.lastConnected = lastConnected;
}
public Long getLastMisbehaved() {
return this.lastMisbehaved;
}
public void setLastMisbehaved(Long lastMisbehaved) {
this.lastMisbehaved = lastMisbehaved;
}
public Long getAddedWhen() {
return this.addedWhen;
}
public String getAddedBy() {
return this.addedBy;
}
public int getFailedSyncCount() {
return this.failedSyncCount;
}
public void setFailedSyncCount(int failedSyncCount) {
this.failedSyncCount = failedSyncCount;
}
public void incrementFailedSyncCount() {
this.failedSyncCount++;
}
// Pretty peerAddress getter for JAXB
@XmlElement(name = "address")
protected String getPrettyAddress() {
return encodeHexString(this.peerAddress);
}
}

View File

@@ -0,0 +1,31 @@
package org.qortal.network;
public class RNSCommon {
/**
* Destination application name
*/
public static String MAINNET_APP_NAME = "qortal"; // production
public static String TESTNET_APP_NAME = "qortaltest"; // test net
/**
* Configuration path relative to the Qortal launch directory
*/
public static String defaultRNSConfigPath = ".reticulum";
public static String defaultRNSConfigPathTestnet = ".reticulum_test";
/**
* Default config
*/
public static String defaultRNSConfig = "reticulum_default_config.yml";
public static String defaultRNSConfigTestnet = "reticulum_default_testnet_config.yml";
///**
// * Qortal RNS Destinations
// */
//public enum RNSDestinations {
// BASE,
// QDN;
//}
}

View File

@@ -0,0 +1,845 @@
package org.qortal.network;
import io.reticulum.Reticulum;
import io.reticulum.Transport;
import io.reticulum.interfaces.ConnectionInterface;
import io.reticulum.destination.Destination;
import io.reticulum.destination.DestinationType;
import io.reticulum.destination.Direction;
import io.reticulum.destination.ProofStrategy;
import io.reticulum.identity.Identity;
import io.reticulum.link.Link;
import io.reticulum.link.LinkStatus;
//import io.reticulum.constant.LinkConstant;
//import static io.reticulum.constant.ReticulumConstant.MTU;
import io.reticulum.buffer.Buffer;
import io.reticulum.buffer.BufferedRWPair;
import io.reticulum.packet.Packet;
import io.reticulum.packet.PacketReceipt;
import io.reticulum.packet.PacketReceiptStatus;
import io.reticulum.transport.AnnounceHandler;
//import static io.reticulum.link.TeardownSession.DESTINATION_CLOSED;
//import static io.reticulum.link.TeardownSession.INITIATOR_CLOSED;
import static io.reticulum.link.TeardownSession.TIMEOUT;
import static io.reticulum.link.LinkStatus.ACTIVE;
import static io.reticulum.link.LinkStatus.STALE;
import static io.reticulum.link.LinkStatus.CLOSED;
import static io.reticulum.link.LinkStatus.PENDING;
import static io.reticulum.link.LinkStatus.HANDSHAKE;
//import static io.reticulum.packet.PacketContextType.LINKCLOSE;
//import static io.reticulum.identity.IdentityKnownDestination.recall;
import static io.reticulum.utils.IdentityUtils.concatArrays;
//import static io.reticulum.constant.ReticulumConstant.TRUNCATED_HASHLENGTH;
import static io.reticulum.constant.ReticulumConstant.CONFIG_FILE_NAME;
import lombok.Data;
//import lombok.Setter;
//import lombok.Getter;
import lombok.Synchronized;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardCopyOption;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.channels.SelectionKey;
import static java.nio.charset.StandardCharsets.UTF_8;
//import static java.util.Objects.isNull;
//import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
//import static org.apache.commons.lang3.BooleanUtils.isTrue;
//import static org.apache.commons.lang3.BooleanUtils.isFalse;
import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Iterator;
//import java.util.Random;
//import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
//import java.util.concurrent.locks.Lock;
//import java.util.concurrent.locks.ReentrantLock;
import java.util.Objects;
import java.util.function.Function;
import java.time.Instant;
import static org.apache.commons.codec.binary.Hex.encodeHexString;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
import org.qortal.network.message.Message;
import org.qortal.network.message.BlockSummariesV2Message;
import org.qortal.network.message.TransactionSignaturesMessage;
import org.qortal.network.message.GetUnconfirmedTransactionsMessage;
import org.qortal.network.task.RNSBroadcastTask;
import org.qortal.network.task.RNSPrunePeersTask;
import org.qortal.data.network.RNSPeerData;
import org.qortal.controller.Controller;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.transaction.TransactionData;
// logging
import lombok.extern.slf4j.Slf4j;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
@Data
@Slf4j
public class RNSNetwork {
Reticulum reticulum;
//private static final String APP_NAME = "qortal";
static final String APP_NAME = Settings.getInstance().isTestNet() ? RNSCommon.TESTNET_APP_NAME: RNSCommon.MAINNET_APP_NAME;
//static final String defaultConfigPath = ".reticulum"; // if empty will look in Reticulums default paths
static final String defaultConfigPath = Settings.getInstance().isTestNet() ? RNSCommon.defaultRNSConfigPathTestnet: RNSCommon.defaultRNSConfigPath;
private final int MAX_PEERS = Settings.getInstance().getReticulumMaxPeers();
private final int MIN_DESIRED_PEERS = Settings.getInstance().getReticulumMinDesiredPeers();
// How long [ms] between pruning of peers
private long PRUNE_INTERVAL = 1 * 64 * 1000L; // ms;
Identity serverIdentity;
public Destination baseDestination;
private volatile boolean isShuttingDown = false;
/**
* Maintain two lists for each subset of peers
* => a synchronizedList, modified when peers are added/removed
* => an immutable List, automatically rebuild to mirror synchronizedList, served to consumers
* linkedPeers are "initiators" (containing initiator reticulum Link), actively doing work.
* incomimgPeers are "non-initiators", the passive end of bidirectional Reticulum Buffers.
*/
private final List<RNSPeer> linkedPeers = Collections.synchronizedList(new ArrayList<>());
private List<RNSPeer> immutableLinkedPeers = Collections.emptyList();
private final List<RNSPeer> incomingPeers = Collections.synchronizedList(new ArrayList<>());
private List<RNSPeer> immutableIncomingPeers = Collections.emptyList();
private final ExecuteProduceConsume rnsNetworkEPC;
private static final long NETWORK_EPC_KEEPALIVE = 1000L; // 1 second
private int totalThreadCount = 0;
private final int reticulumMaxNetworkThreadPoolSize = Settings.getInstance().getReticulumMaxNetworkThreadPoolSize();
// replicating a feature from Network.class needed in for base Message.java,
// just in case the classic TCP/IP Networking is turned off.
private static final byte[] MAINNET_MESSAGE_MAGIC = new byte[]{0x51, 0x4f, 0x52, 0x54}; // QORT
private static final byte[] TESTNET_MESSAGE_MAGIC = new byte[]{0x71, 0x6f, 0x72, 0x54}; // qorT
private static final int BROADCAST_CHAIN_TIP_DEPTH = 7; // (~1440 bytes)
/**
* How long between informational broadcasts to all ACTIVE peers, in milliseconds.
*/
private static final long BROADCAST_INTERVAL = 30 * 1000L; // ms
/**
* Link low-level ping interval and timeout
*/
private static final long LINK_PING_INTERVAL = 55 * 1000L; // ms
private static final long LINK_UNREACHABLE_TIMEOUT = 3 * LINK_PING_INTERVAL;
//private static final Logger logger = LoggerFactory.getLogger(RNSNetwork.class);
// Constructor
private RNSNetwork () {
log.info("RNSNetwork constructor");
try {
//String configPath = new java.io.File(defaultConfigPath).getCanonicalPath();
log.info("creating config from {}", defaultConfigPath);
initConfig(defaultConfigPath);
//reticulum = new Reticulum(configPath);
reticulum = new Reticulum(defaultConfigPath);
var identitiesPath = reticulum.getStoragePath().resolve("identities");
if (Files.notExists(identitiesPath)) {
Files.createDirectories(identitiesPath);
}
} catch (IOException e) {
log.error("unable to create Reticulum network", e);
}
log.info("reticulum instance created");
log.info("reticulum instance created: {}", reticulum);
// Settings.getInstance().getMaxRNSNetworkThreadPoolSize(), // statically set to 5 below
ExecutorService RNSNetworkExecutor = new ThreadPoolExecutor(1,
reticulumMaxNetworkThreadPoolSize,
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new NamedThreadFactory("RNSNetwork-EPC", Settings.getInstance().getNetworkThreadPriority()));
rnsNetworkEPC = new RNSNetworkProcessor(RNSNetworkExecutor);
}
// Note: potentially create persistent serverIdentity (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("identities/"+APP_NAME);
if (Files.isReadable(serverIdentityPath)) {
serverIdentity = Identity.fromFile(serverIdentityPath);
log.info("server identity loaded from file {}", serverIdentityPath);
} else {
serverIdentity = new Identity();
log.info("APP_NAME: {}, storage path: {}", APP_NAME, serverIdentityPath);
log.info("new server identity created dynamically.");
// save it back to file by default for next start (possibly add setting to override)
try {
Files.write(serverIdentityPath, serverIdentity.getPrivateKey(), CREATE, WRITE);
log.info("serverIdentity written back to file");
} catch (IOException e) {
log.error("Error while saving serverIdentity to {}", serverIdentityPath, e);
}
}
log.debug("Server Identity: {}", serverIdentity.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(
serverIdentity,
Direction.IN,
DestinationType.SINGLE,
APP_NAME,
"core"
);
//// idea for other entry point (needs AnnounceHandler with appropriate aspect)
//dataDestination = new Destination(
// serverIdentity,
// Direction.IN,
// DestinationType.SINGLE,
// APP_NAME,
// "qdn"
//);
log.info("Destination {} {} running", encodeHexString(baseDestination.getHash()), baseDestination.getName());
baseDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
baseDestination.setAcceptLinkRequests(true);
baseDestination.setLinkEstablishedCallback(this::clientConnected);
Transport.getInstance().registerAnnounceHandler(new QAnnounceHandler());
log.debug("announceHandlers: {}", Transport.getInstance().getAnnounceHandlers());
// do a first announce
baseDestination.announce();
log.debug("Sent initial announce from {} ({})", encodeHexString(baseDestination.getHash()), baseDestination.getName());
// Start up first networking thread (the "server loop", the "Tasks engine")
rnsNetworkEPC.start();
}
private void initConfig(String configDir) throws IOException {
File configDir1 = new File(configDir);
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(RNSCommon.defaultRNSConfig);
if (Settings.getInstance().isTestNet()) {
defaultConfig = this.getClass().getClassLoader().getResourceAsStream(RNSCommon.defaultRNSConfigTestnet);
}
Files.copy(defaultConfig, configFile, StandardCopyOption.REPLACE_EXISTING);
}
}
public void broadcast(Function<RNSPeer, Message> peerMessageBuilder) {
for (RNSPeer peer : getActiveImmutableLinkedPeers()) {
if (this.isShuttingDown) {
return;
}
Message message = peerMessageBuilder.apply(peer);
if (message == null) {
continue;
}
var pl = peer.getPeerLink();
if (nonNull(pl) && (pl.getStatus() == ACTIVE)) {
peer.sendMessage(message);
}
}
}
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);
broadcast(broadcastPeer -> latestBlockSummariesMessage);
} catch (DataException e) {
log.warn("Couldn't broadcast our chain tip info", e);
}
}
public Message buildNewTransactionMessage(RNSPeer peer, TransactionData transactionData) {
// In V2 we send out transaction signature only and peers can decide whether to request the full transaction
return new TransactionSignaturesMessage(Collections.singletonList(transactionData.getSignature()));
}
public Message buildGetUnconfirmedTransactionsMessage(RNSPeer peer) {
return new GetUnconfirmedTransactionsMessage();
}
public void shutdown() {
this.isShuttingDown = true;
log.info("shutting down Reticulum");
// gracefully close links of peers that point to us
for (RNSPeer p: incomingPeers) {
var pl = p.getPeerLink();
if (nonNull(pl) & (pl.getStatus() == ACTIVE)) {
p.sendCloseToRemote(pl);
}
}
// Disconnect peers gracefully and terminate Reticulum
for (RNSPeer p: linkedPeers) {
log.info("shutting down peer: {}", encodeHexString(p.getDestinationHash()));
//log.debug("peer: {}", p);
p.shutdown();
try {
TimeUnit.SECONDS.sleep(1); // allow for peers to disconnect gracefully
} catch (InterruptedException e) {
log.error("exception: ", e);
}
//var pl = p.getPeerLink();
//if (nonNull(pl) & (pl.getStatus() == ACTIVE)) {
// pl.teardown();
//}
}
// Stop processing threads (the "server loop")
try {
if (!this.rnsNetworkEPC.shutdown(5000)) {
log.warn("RNSNetwork threads failed to terminate");
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for RNS networking threads to terminate");
}
// Note: we still need to get the packet timeout callback to work...
reticulum.exitHandler();
}
public void sendCloseToRemote(Link link) {
if (nonNull(link)) {
var data = concatArrays("close::".getBytes(UTF_8),link.getDestination().getHash());
Packet closePacket = new Packet(link, data);
var packetReceipt = closePacket.send();
packetReceipt.setDeliveryCallback(this::closePacketDelivered);
packetReceipt.setTimeoutCallback(this::packetTimedOut);
} else {
log.debug("can't send to null link");
}
}
public void closePacketDelivered(PacketReceipt receipt) {
var rttString = "";
if (receipt.getStatus() == PacketReceiptStatus.DELIVERED) {
var rtt = receipt.getRtt(); // rtt (Java) is in miliseconds
//log.info("qqp - packetDelivered - rtt: {}", rtt);
if (rtt >= 1000) {
rtt = Math.round((float) rtt / 1000);
rttString = String.format("%d seconds", rtt);
} else {
rttString = String.format("%d miliseconds", rtt);
}
log.info("Shutdown packet confirmation received from {}, round-trip time is {}",
encodeHexString(receipt.getDestination().getHash()), rttString);
}
}
public void packetTimedOut(PacketReceipt receipt) {
log.info("packet timed out, receipt status: {}", receipt.getStatus());
}
public void clientConnected(Link link) {
//link.setLinkClosedCallback(this::clientDisconnected);
//link.setPacketCallback(this::serverPacketReceived);
log.info("clientConnected - link hash: {}, {}", link.getHash(), encodeHexString(link.getHash()));
RNSPeer newPeer = new RNSPeer(link);
newPeer.setPeerLinkHash(link.getHash());
newPeer.setMessageMagic(getMessageMagic());
// make sure the peer has a channel and buffer
newPeer.getOrInitPeerBuffer();
addIncomingPeer(newPeer);
log.info("***> Client connected, link: {}", encodeHexString(link.getLinkId()));
}
public void clientDisconnected(Link link) {
log.info("***> Client disconnected");
}
public void serverPacketReceived(byte[] message, Packet packet) {
var msgText = new String(message, StandardCharsets.UTF_8);
log.info("Received data on link - message: {}, destinationHash: {}", msgText, encodeHexString(packet.getDestinationHash()));
}
//public void announceBaseDestination () {
// getBaseDestination().announce();
//}
private class QAnnounceHandler implements AnnounceHandler {
@Override
public String getAspectFilter() {
return "qortal.core";
}
@Override
@Synchronized
public void receivedAnnounce(byte[] destinationHash, Identity announcedIdentity, byte[] appData) {
var peerExists = false;
var activePeerCount = 0;
log.info("Received an announce from {}", encodeHexString(destinationHash));
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) {
var lps = RNSNetwork.getInstance().getImmutableLinkedPeers();
for (RNSPeer p: lps) {
var pl = p.getPeerLink();
if ((nonNull(pl) && (pl.getStatus() == ACTIVE))) {
activePeerCount = activePeerCount + 1;
}
}
if (activePeerCount < MAX_PEERS) {
for (RNSPeer p: lps) {
if (Arrays.equals(p.getDestinationHash(), destinationHash)) {
log.info("QAnnounceHandler - peer exists - found peer matching destinationHash");
if (nonNull(p.getPeerLink())) {
log.info("peer link: {}, status: {}",
encodeHexString(p.getPeerLink().getLinkId()), p.getPeerLink().getStatus());
}
peerExists = true;
if (p.getPeerLink().getStatus() != ACTIVE) {
p.getOrInitPeerLink();
}
break;
} else {
if (nonNull(p.getPeerLink())) {
log.info("QAnnounceHandler - other peer - link: {}, status: {}",
encodeHexString(p.getPeerLink().getLinkId()), p.getPeerLink().getStatus());
if (p.getPeerLink().getStatus() == CLOSED) {
// mark peer for deletion on nexe pruning
p.setDeleteMe(true);
}
} else {
log.info("QAnnounceHandler - peer link is null");
}
}
}
if (!peerExists) {
RNSPeer newPeer = new RNSPeer(destinationHash);
newPeer.setServerIdentity(announcedIdentity);
newPeer.setIsInitiator(true);
newPeer.setMessageMagic(getMessageMagic());
addLinkedPeer(newPeer);
log.info("added new RNSPeer, destinationHash: {}", encodeHexString(destinationHash));
}
}
// Chance to announce instead of waiting for next pruning.
// Note: good in theory but leads to ping-pong of announces => not a good idea!
//maybeAnnounce(getBaseDestination());
}
}
// 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 final AtomicLong nextPingTimestamp = new AtomicLong(0L); // ms - try first low-level Ping
private final AtomicLong nextPruneTimestamp = new AtomicLong(0L); // ms - try first low-level Ping
private Iterator<SelectionKey> channelIterator = null;
RNSNetworkProcessor(ExecutorService executor) {
super(executor);
final Long now = NTP.getTime();
nextPruneTimestamp.set(now + PRUNE_INTERVAL/2);
}
@Override
protected void onSpawnFailure() {
// For debugging:
// ExecutorDumper.dump(this.executor, 3, ExecuteProduceConsume.class);
}
@Override
protected Task produceTask(boolean canBlock) throws InterruptedException {
Task task;
//// TODO: Needed? Figure out how to add pending messages in RNSPeer
//// (RNSPeer: pendingMessages.offer(message))
//task = maybeProducePeerMessageTask();
//if (task != null) {
// return task;
//}
final Long now = NTP.getTime();
// ping task (Link+Channel+Buffer)
task = maybeProducePeerPingTask(now);
if (task != null) {
return task;
}
task = maybeProduceBroadcastTask(now);
if (task != null) {
return task;
}
//// Prune stuck/slow/old peers (moved from Controller)
//task = maybeProduceRNSPrunePeersTask(now);
//if (task != null) {
// return task;
//}
return null;
}
////private Task maybeProducePeerMessageTask() {
//// return getImmutableConnectedPeers().stream()
//// .map(Peer::getMessageTask)
//// .filter(Objects::nonNull)
//// .findFirst()
//// .orElse(null);
////}
////private Task maybeProducePeerMessageTask() {
//// return getImmutableIncomingPeers().stream()
//// .map(RNSPeer::getMessageTask)
//// .filter(RNSPeer::isAvailable)
//// .findFirst()
//// .orElse(null);
////}
//// Note: we might not need this. All messages handled asynchronously in Reticulum
//// (RNSPeer peerBufferReady callback)
//private Task maybeProducePeerMessageTask() {
// return getActiveImmutableLinkedPeers().stream()
// .map(RNSPeer::getMessageTask)
// .filter(Objects::nonNull)
// .findFirst()
// .orElse(null);
//}
//private Task maybeProducePeerPingTask(Long now) {
// return getImmutableHandshakedPeers().stream()
// .map(peer -> peer.getPingTask(now))
// .filter(Objects::nonNull)
// .findFirst()
// .orElse(null);
//}
private Task maybeProducePeerPingTask(Long now) {
//var ilp = getImmutableLinkedPeers().stream()
// .map(peer -> peer.getPingTask(now))
// .filter(Objects::nonNull)
// .findFirst()
// .orElse(null);
//if (nonNull(ilp)) {
// log.info("ilp - {}", ilp);
//}
//return ilp;
return getActiveImmutableLinkedPeers().stream()
.map(peer -> peer.getPingTask(now))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private Task maybeProduceBroadcastTask(Long now) {
if (now == null || now < nextBroadcastTimestamp.get()) {
return null;
}
nextBroadcastTimestamp.set(now + BROADCAST_INTERVAL);
return new RNSBroadcastTask();
}
private Task maybeProduceRNSPrunePeersTask(Long now) {
if (now == null || now < nextPruneTimestamp.get()) {
return null;
}
nextPruneTimestamp.set(now + PRUNE_INTERVAL);
return new RNSPrunePeersTask();
}
}
private static class SingletonContainer {
private static final RNSNetwork INSTANCE = new RNSNetwork();
}
public static RNSNetwork getInstance() {
return SingletonContainer.INSTANCE;
}
public List<RNSPeer> getActiveImmutableLinkedPeers() {
List<RNSPeer> activePeers = Collections.synchronizedList(new ArrayList<>());
for (RNSPeer p: this.immutableLinkedPeers) {
if (nonNull(p.getPeerLink()) && (p.getPeerLink().getStatus() == ACTIVE)) {
activePeers.add(p);
}
}
return activePeers;
}
// note: we already have a lobok getter for this
//public List<RNSPeer> getImmutableLinkedPeers() {
// return this.immutableLinkedPeers;
//}
public void addLinkedPeer(RNSPeer peer) {
this.linkedPeers.add(peer);
this.immutableLinkedPeers = List.copyOf(this.linkedPeers); // thread safe
}
public void removeLinkedPeer(RNSPeer peer) {
//if (nonNull(peer.getPeerBuffer())) {
// peer.getPeerBuffer().close();
//}
if (nonNull(peer.getPeerLink())) {
peer.getPeerLink().teardown();
}
var p = this.linkedPeers.remove(this.linkedPeers.indexOf(peer)); // thread safe
this.immutableLinkedPeers = List.copyOf(this.linkedPeers);
}
// note: we already have a lobok getter for this
//public List<RNSPeer> getLinkedPeers() {
// //synchronized(this.linkedPeers) {
// //return new ArrayList<>(this.linkedPeers);
// return this.linkedPeers;
// //}
//}
public void addIncomingPeer(RNSPeer peer) {
this.incomingPeers.add(peer);
this.immutableIncomingPeers = List.copyOf(this.incomingPeers);
}
public void removeIncomingPeer(RNSPeer peer) {
if (nonNull(peer.getPeerLink())) {
peer.getPeerLink().teardown();
}
var p = this.incomingPeers.remove(this.incomingPeers.indexOf(peer));
this.immutableIncomingPeers = List.copyOf(this.incomingPeers);
}
// note: we already have a lobok getter for this
//public List<RNSPeer> getIncomingPeers() {
// return this.incomingPeers;
//}
//public List<RNSPeer> getImmutableIncomingPeers() {
// return this.immutableIncomingPeers;
//}
// TODO, methods for: getAvailablePeer
private Boolean isUnreachable(RNSPeer peer) {
var result = peer.getDeleteMe();
var now = Instant.now();
var peerLastAccessTimestamp = peer.getLastAccessTimestamp();
if (peerLastAccessTimestamp.isBefore(now.minusMillis(LINK_UNREACHABLE_TIMEOUT))) {
result = true;
}
return result;
}
public void peerMisbehaved(RNSPeer peer) {
RNSPeerData peerData = peer.getPeerData();
peerData.setLastMisbehaved(NTP.getTime());
//// Only update repository if outbound/initiator peer
//if (peer.getIsInitiator()) {
// try (Repository repository = RepositoryManager.getRepository()) {
// synchronized (this.allKnownPeers) {
// repository.getNetworkRepository().save(peerData);
// repository.saveChanges();
// }
// } catch (DataException e) {
// log.warn("Repository issue while updating peer synchronization info", e);
// }
//}
}
public List<RNSPeer> getNonActiveIncomingPeers() {
var ips = getIncomingPeers();
List<RNSPeer> result = Collections.synchronizedList(new ArrayList<>());
Link pl;
for (RNSPeer p: ips) {
pl = p.getPeerLink();
if (nonNull(pl)) {
if (pl.getStatus() != ACTIVE) {
result.add(p);
}
} else {
result.add(p);
}
}
return result;
}
//@Synchronized
public void prunePeers() throws DataException {
// prune initiator peers
//var peerList = getImmutableLinkedPeers();
var initiatorPeerList = getImmutableLinkedPeers();
var initiatorActivePeerList = getActiveImmutableLinkedPeers();
var incomingPeerList = getImmutableIncomingPeers();
var numActiveIncomingPeers = incomingPeerList.size() - getNonActiveIncomingPeers().size();
log.info("number of links (linkedPeers (active) / incomingPeers (active) before prunig: {} ({}), {} ({})",
initiatorPeerList.size(), getActiveImmutableLinkedPeers().size(),
incomingPeerList.size(), numActiveIncomingPeers);
for (RNSPeer p: initiatorActivePeerList) {
var pLink = p.getOrInitPeerLink();
p.pingRemote();
}
for (RNSPeer p : initiatorPeerList) {
var pLink = p.getPeerLink();
if (nonNull(pLink)) {
if (p.getPeerTimedOut()) {
// options: keep in case peer reconnects or remove => we'll remove it
removeLinkedPeer(p);
continue;
}
if (pLink.getStatus() == ACTIVE) {
continue;
}
if ((pLink.getStatus() == CLOSED) || (p.getDeleteMe())) {
removeLinkedPeer(p);
continue;
}
if (pLink.getStatus() == PENDING) {
pLink.teardown();
removeLinkedPeer(p);
continue;
}
}
}
// prune non-initiator peers
List<RNSPeer> inaps = getNonActiveIncomingPeers();
incomingPeerList = this.incomingPeers;
for (RNSPeer p: incomingPeerList) {
var pLink = p.getOrInitPeerLink();
if (nonNull(pLink) && (pLink.getStatus() == ACTIVE)) {
// make false active links to timeout (and teardown in timeout callback)
// note: actual removal of peer happens on the following pruning run.
p.pingRemote();
}
}
for (RNSPeer p: inaps) {
var pLink = p.getPeerLink();
if (nonNull(pLink)) {
// could be eg. PENDING
pLink.teardown();
}
removeIncomingPeer(p);
}
initiatorPeerList = getImmutableLinkedPeers();
initiatorActivePeerList = getActiveImmutableLinkedPeers();
incomingPeerList = getImmutableIncomingPeers();
numActiveIncomingPeers = incomingPeerList.size() - getNonActiveIncomingPeers().size();
log.info("number of links (linkedPeers (active) / incomingPeers (active) after prunig: {} ({}), {} ({})",
initiatorPeerList.size(), getActiveImmutableLinkedPeers().size(),
incomingPeerList.size(), numActiveIncomingPeers);
maybeAnnounce(getBaseDestination());
}
public void maybeAnnounce(Destination d) {
var activePeers = getActiveImmutableLinkedPeers().size();
if (activePeers <= MIN_DESIRED_PEERS) {
log.info("Active peers ({}) <= desired peers ({}). Announcing", activePeers, MIN_DESIRED_PEERS);
d.announce();
}
}
/**
* Helper methods
*/
public RNSPeer findPeerByLink(Link link) {
//List<RNSPeer> lps = RNSNetwork.getInstance().getLinkedPeers();
List<RNSPeer> lps = RNSNetwork.getInstance().getImmutableLinkedPeers();
RNSPeer peer = null;
for (RNSPeer p : lps) {
var pLink = p.getPeerLink();
if (nonNull(pLink)) {
if (Arrays.equals(pLink.getDestination().getHash(),link.getDestination().getHash())) {
log.info("found peer matching destinationHash: {}", encodeHexString(link.getDestination().getHash()));
peer = p;
break;
}
}
}
return peer;
}
public RNSPeer findPeerByDestinationHash(byte[] dhash) {
//List<RNSPeer> lps = RNSNetwork.getInstance().getLinkedPeers();
List<RNSPeer> lps = RNSNetwork.getInstance().getImmutableLinkedPeers();
RNSPeer peer = null;
for (RNSPeer p : lps) {
if (Arrays.equals(p.getDestinationHash(), dhash)) {
log.info("found peer matching destinationHash: {}", encodeHexString(dhash));
peer = p;
break;
}
}
return peer;
}
//public void removePeer(RNSPeer peer) {
// List<RNSPeer> peerList = this.linkedPeers;
// if (nonNull(peer)) {
// peerList.remove(peer);
// }
//}
public byte[] getMessageMagic() {
return Settings.getInstance().isTestNet() ? TESTNET_MESSAGE_MAGIC : MAINNET_MESSAGE_MAGIC;
}
public String getOurNodeId() {
return this.serverIdentity.toString();
}
protected byte[] getOurPublicKey() {
return this.serverIdentity.getPublicKey();
}
// Network methods Reticulum implementation
/** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
*
* @return Message, or null if DataException was thrown.
*/
public Message buildHeightOrChainTipInfo(RNSPeer peer) {
// peer only used for version check
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;
}
}
}

View File

@@ -0,0 +1,894 @@
package org.qortal.network;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
//import java.io.IOException;
import java.time.Instant;
import java.util.*;
//import io.reticulum.Reticulum;
//import org.qortal.network.RNSNetwork;
import io.reticulum.link.Link;
import io.reticulum.link.RequestReceipt;
import io.reticulum.packet.PacketReceiptStatus;
import io.reticulum.packet.Packet;
import io.reticulum.packet.PacketReceipt;
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 io.reticulum.destination.ProofStrategy;
import io.reticulum.resource.Resource;
import static io.reticulum.link.TeardownSession.INITIATOR_CLOSED;
import static io.reticulum.link.TeardownSession.DESTINATION_CLOSED;
import static io.reticulum.link.TeardownSession.TIMEOUT;
import static io.reticulum.link.LinkStatus.ACTIVE;
//import static io.reticulum.link.LinkStatus.CLOSED;
import static io.reticulum.identity.IdentityKnownDestination.recall;
//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
import io.reticulum.buffer.Buffer;
import io.reticulum.buffer.BufferedRWPair;
import static io.reticulum.utils.IdentityUtils.concatArrays;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
import org.qortal.data.network.RNSPeerData;
import org.qortal.network.message.Message;
import org.qortal.network.message.MessageType;
import org.qortal.network.message.PingMessage;
import org.qortal.network.message.*;
import org.qortal.network.message.MessageException;
import org.qortal.network.task.RNSMessageTask;
import org.qortal.network.task.RNSPingTask;
import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.util.concurrent.*;
import java.util.Arrays;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.codec.binary.Hex.encodeHexString;
import static org.apache.commons.lang3.ArrayUtils.subarray;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import lombok.extern.slf4j.Slf4j;
import lombok.Setter;
import lombok.Data;
import lombok.AccessLevel;
//import lombok.Synchronized;
//
//import org.qortal.network.message.Message;
//import org.qortal.network.message.MessageException;
import java.util.concurrent.atomic.LongAdder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.lang.IllegalStateException;
@Data
@Slf4j
public class RNSPeer {
static final String APP_NAME = Settings.getInstance().isTestNet() ? RNSCommon.TESTNET_APP_NAME: RNSCommon.MAINNET_APP_NAME;
//static final String defaultConfigPath = new String(".reticulum");
//static final String defaultConfigPath = RNSCommon.defaultRNSConfigPath;
private byte[] destinationHash; // remote destination hash
Destination peerDestination; // OUT destination created for this
private Identity serverIdentity;
@Setter(AccessLevel.PACKAGE) private Instant creationTimestamp;
@Setter(AccessLevel.PACKAGE) private Instant lastAccessTimestamp;
@Setter(AccessLevel.PACKAGE) private Instant lastLinkProbeTimestamp;
Link peerLink;
byte[] peerLinkHash;
BufferedRWPair peerBuffer;
int receiveStreamId = 0;
int sendStreamId = 0;
private Boolean isInitiator;
private Boolean deleteMe = false;
//private Boolean isVacant = true;
private Long lastPacketRtt = null;
//private byte[] emptyBuffer = {0,0,0,0,0};
private Double requestResponseProgress;
@Setter(AccessLevel.PACKAGE) private Boolean peerTimedOut = false;
// for qortal networking
private static final int RESPONSE_TIMEOUT = 3000; // [ms]
private static final int PING_INTERVAL = 55_000; // [ms]
private static final long LINK_PING_INTERVAL = 55 * 1000L; // ms
private byte[] messageMagic; // set in message creating classes
private Long lastPing = null; // last (packet) ping roundtrip time [ms]
private Long lastPingSent = null; // time last (packet) ping was sent, or null if not started.
@Setter(AccessLevel.PACKAGE) private Instant lastPingResponseReceived = null; // time last (packet) ping succeeded
private Map<Integer, BlockingQueue<Message>> replyQueues;
private LinkedBlockingQueue<Message> pendingMessages;
private boolean syncInProgress = false;
private RNSPeerData peerData = null;
private long linkEstablishedTime = -1L; // equivalent of (tcpip) Peer 'handshakeComplete'
// Versioning
public static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX
+ "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
/* Pending signature requests */
private List<byte[]> pendingSignatureRequests = Collections.synchronizedList(new ArrayList<>());
/**
* Latest block info as reported by peer.
*/
private List<BlockSummaryData> peersChainTipData = Collections.emptyList();
/**
* Our common block with this peer
*/
private CommonBlockData commonBlockData;
/**
* Last time we detected this peer as TOO_DIVERGENT
*/
private Long lastTooDivergentTime;
///**
// * Known starting sequences for data received over buffer
// */
//private byte[] SEQ_REQUEST_CONFIRM_ID = new byte[]{0x53, 0x52, 0x65, 0x71, 0x43, 0x49, 0x44}; // SReqCID
//private byte[] SEQ_RESPONSE_CONFIRM_ID = new byte[]{0x53, 0x52, 0x65, 0x73, 0x70, 0x43, 0x49, 0x44}; // SRespCID
// Message stats
private static class MessageStats {
public final LongAdder count = new LongAdder();
public final LongAdder totalBytes = new LongAdder();
}
private final Map<MessageType, RNSPeer.MessageStats> receivedMessageStats = new ConcurrentHashMap<>();
private final Map<MessageType, RNSPeer.MessageStats> sentMessageStats = new ConcurrentHashMap<>();
/**
* Constructor for initiator peers
*/
public RNSPeer(byte[] dhash) {
this.destinationHash = dhash;
this.serverIdentity = recall(dhash);
initPeerLink();
//setCreationTimestamp(System.currentTimeMillis());
this.creationTimestamp = Instant.now();
//this.isVacant = true;
this.replyQueues = new ConcurrentHashMap<>();
this.pendingMessages = new LinkedBlockingQueue<>();
this.peerData = new RNSPeerData(dhash);
}
/**
* Constructor for non-initiator peers
*/
public RNSPeer(Link link) {
this.peerLink = link;
//this.peerLinkId = link.getLinkId();
this.peerDestination = link.getDestination();
this.destinationHash = link.getDestination().getHash();
this.serverIdentity = link.getRemoteIdentity();
this.creationTimestamp = Instant.now();
this.lastAccessTimestamp = Instant.now();
this.lastLinkProbeTimestamp = null;
this.isInitiator = false;
//this.isVacant = false;
//this.peerLink.setLinkEstablishedCallback(this::linkEstablished);
//this.peerLink.setLinkClosedCallback(this::linkClosed);
//this.peerLink.setPacketCallback(this::linkPacketReceived);
this.peerData = new RNSPeerData(this.destinationHash);
}
public void initPeerLink() {
peerDestination = new Destination(
this.serverIdentity,
Direction.OUT,
DestinationType.SINGLE,
APP_NAME,
"core"
);
peerDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
this.creationTimestamp = Instant.now();
this.lastAccessTimestamp = Instant.now();
this.lastLinkProbeTimestamp = null;
this.isInitiator = true;
this.peerLink = new Link(peerDestination);
this.peerLink.setLinkEstablishedCallback(this::linkEstablished);
this.peerLink.setLinkClosedCallback(this::linkClosed);
this.peerLink.setPacketCallback(this::linkPacketReceived);
}
@Override
public String toString() {
// for messages we want an address-like string representation
if (nonNull(this.peerLink)) {
return this.getPeerLink().toString();
} else {
return encodeHexString(this.getDestinationHash());
}
}
public BufferedRWPair getOrInitPeerBuffer() {
var channel = this.peerLink.getChannel();
if (nonNull(this.peerBuffer)) {
//log.info("peerBuffer exists: {}, link status: {}", this.peerBuffer, this.peerLink.getStatus());
try {
log.trace("peerBuffer exists: {}, link status: {}", this.peerBuffer, this.peerLink.getStatus());
} catch (IllegalStateException e) {
// Exception thrown by Reticulum if the buffer is unusable (Channel, Link, etc)
// This is a chance to correct links status when doing a RNSPingTask
log.warn("can't establish Channel/Buffer (remote peer down?), closing link: {}");
this.peerBuffer.close();
this.peerLink.teardown();
this.peerLink = null;
//log.error("(handled) IllegalStateException - can't establish Channel/Buffer: {}", e);
}
}
else {
log.info("creating buffer - peerLink status: {}, channel: {}", this.peerLink.getStatus(), channel);
this.peerBuffer = Buffer.createBidirectionalBuffer(receiveStreamId, sendStreamId, channel, this::peerBufferReady);
}
return getPeerBuffer();
}
public Link getOrInitPeerLink() {
if (this.peerLink.getStatus() == ACTIVE) {
lastAccessTimestamp = Instant.now();
//return this.peerLink;
} else {
initPeerLink();
}
return this.peerLink;
}
public void shutdown() {
if (nonNull(this.peerLink)) {
log.info("shutdown - peerLink: {}, status: {}", peerLink, peerLink.getStatus());
if (peerLink.getStatus() == ACTIVE) {
if (nonNull(this.peerBuffer)) {
this.peerBuffer.close();
this.peerBuffer = null;
}
this.peerLink.teardown();
} else {
log.info("shutdown - status (non-ACTIVE): {}", peerLink.getStatus());
}
this.peerLink = null;
}
this.deleteMe = true;
}
public Channel getChannel() {
if (isNull(getPeerLink())) {
log.warn("link is null.");
return null;
}
setLastAccessTimestamp(Instant.now());
return getPeerLink().getChannel();
}
public Boolean getIsInitiator() {
return this.isInitiator;
}
/** Link callbacks */
public void linkEstablished(Link link) {
this.linkEstablishedTime = System.currentTimeMillis();
link.setLinkClosedCallback(this::linkClosed);
log.info("peerLink {} established (link: {}) with peer: hash - {}, link destination hash: {}",
encodeHexString(peerLink.getLinkId()), encodeHexString(link.getLinkId()), encodeHexString(destinationHash),
encodeHexString(link.getDestination().getHash()));
if (isInitiator) {
startPings();
}
}
public void linkClosed(Link link) {
if (link.getTeardownReason() == TIMEOUT) {
log.info("The link timed out");
this.peerTimedOut = true;
this.peerBuffer = null;
} else if (link.getTeardownReason() == INITIATOR_CLOSED) {
log.info("Link closed callback: The initiator closed the link");
log.info("peerLink {} closed (link: {}), link destination hash: {}",
encodeHexString(peerLink.getLinkId()), encodeHexString(link.getLinkId()), encodeHexString(link.getDestination().getHash()));
this.peerBuffer = null;
} else if (link.getTeardownReason() == DESTINATION_CLOSED) {
log.info("Link closed callback: The link was closed by the peer, removing peer");
log.info("peerLink {} closed (link: {}), link destination hash: {}",
encodeHexString(peerLink.getLinkId()), encodeHexString(link.getLinkId()), encodeHexString(link.getDestination().getHash()));
this.peerBuffer = null;
} else {
log.info("Link closed callback");
}
}
public void linkPacketReceived(byte[] message, Packet packet) {
var msgText = new String(message, StandardCharsets.UTF_8);
if (msgText.equals("ping")) {
log.info("received ping on link");
this.lastLinkProbeTimestamp = Instant.now();
} else if (msgText.startsWith("close::")) {
var targetPeerHash = subarray(message, 7, message.length);
log.info("peer dest hash: {}, target hash: {}",
encodeHexString(destinationHash),
encodeHexString(targetPeerHash));
if (Arrays.equals(destinationHash, targetPeerHash)) {
log.info("closing link: {}", peerLink.getDestination().getHexHash());
if (nonNull(this.peerBuffer)) {
this.peerBuffer.close();
this.peerBuffer = null;
}
this.peerLink.teardown();
}
} else if (msgText.startsWith("open::")) {
var targetPeerHash = subarray(message, 7, message.length);
log.info("peer dest hash: {}, target hash: {}",
encodeHexString(destinationHash),
encodeHexString(targetPeerHash));
if (Arrays.equals(destinationHash, targetPeerHash)) {
log.info("closing link: {}", peerLink.getDestination().getHexHash());
getOrInitPeerLink();
}
}
}
/*
* Callback from buffer when buffer has data available
*
* :param readyBytes: The number of bytes ready to read
*/
public void peerBufferReady(Integer readyBytes) {
// get the message data
byte[] data = this.peerBuffer.read(readyBytes);
ByteBuffer bb = ByteBuffer.wrap(data);
//log.info("data length: {}, MAGIC: {}, data: {}, ByteBuffer: {}", data.length, this.messageMagic, data, bb);
//log.info("data length: {}, MAGIC: {}, ByteBuffer: {}", data.length, this.messageMagic, bb);
//log.trace("peerBufferReady - data bytes: {}", data.length);
this.lastAccessTimestamp = Instant.now();
//if (ByteBuffer.wrap(data, 0, emptyBuffer.length).equals(ByteBuffer.wrap(emptyBuffer, 0, emptyBuffer.length))) {
// log.info("peerBufferReady - empty buffer detected (length: {})", data.length);
//}
//else {
//if (Arrays.equals(SEQ_REQUEST_CONFIRM_ID, Arrays.copyOfRange(data, 0, SEQ_REQUEST_CONFIRM_ID.length))) {
// // a non-initiator peer requested to confirm sending of a packet
// var messageId = subarray(data, SEQ_REQUEST_CONFIRM_ID.length + 1, data.length);
// log.info("received request to confirm message id, id: {}", messageId);
// var confirmData = concatArrays(SEQ_RESPONSE_CONFIRM_ID, "::",data.getBytes(UTF_8), messageId.getBytes(UTF_8));
// this.peerBuffer.write(confirmData);
// this.peerBuffer.flush();
//} else if (Arrays.equals(SEQ_RESPONSE_CONFIRM_ID, Arrays.copyOfRange(data, 0, SEQ_RESPONSE_CONFIRM_ID.lenth))) {
// // an initiator peer receiving the confirmation
// var messageId = subarray(data, SEQ_RESPONSE_CONFIRM_ID.length + 1, data.length);
// this.replyQueues.remove(messageId);
//} else {
try {
//log.info("***> creating message from {} bytes", data.length);
Message message = Message.fromByteBuffer(bb);
//log.info("*=> type {} message received ({} bytes): {}", message.getType(), data.length, message);
log.info("*=> type {} message received ({} bytes, id: {})", message.getType(), data.length, message.getId());
// Handle message based on type
switch (message.getType()) {
// Do we need this ? (seems like a TCP scenario only thing)
// Does any RNSPeer ever require an other RNSPeer's peer list?
//case GET_PEERS:
// //onGetPeersMessage(peer, message);
// onGetRNSPeersMessage(peer, message);
// break;
case PING:
this.lastPingResponseReceived = Instant.now();
if (isFalse(this.isInitiator)) {
onPingMessage(this, message);
}
break;
case PONG:
log.trace("PONG received");
addToQueue(message); // as response in blocking queue for ping getResponse
break;
// Do we need this ? (no need to relay peer list...)
//case PEERS_V2:
// onPeersV2Message(peer, message);
// break;
case BLOCK_SUMMARIES:
// from Synchronizer
addToQueue(message);
case BLOCK_SUMMARIES_V2:
// from Synchronizer
addToQueue(message);
case SIGNATURES:
// from Synchronizer
addToQueue(message);
case BLOCK:
// from Synchronizer
addToQueue(message);
case BLOCK_V2:
// from Synchronizer
addToQueue(message);
default:
log.info("default - type {} message received ({} bytes)", message.getType(), data.length);
// Bump up to controller for possible action
addToQueue(message);
Controller.getInstance().onRNSNetworkMessage(this, message);
break;
}
} catch (MessageException e) {
//log.error("{} from peer {}", e.getMessage(), this);
log.error("{} from peer {}, closing link", e, this);
//log.info("{} from peer {}", e, this);
// don't take any chances:
// can happen if link is closed by peer in which case we close this side of the link
this.peerData.setLastMisbehaved(NTP.getTime());
shutdown();
}
//}
}
/**
* we need to queue all incoming messages that follow request/response
* with explicit handling of the response message.
*/
public void addToQueue(Message message) {
if (message.getType() == MessageType.UNSUPPORTED) {
log.trace("discarding/skipping UNSUPPORTED message");
return;
}
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
if (queue != null) {
// Adding message to queue will unblock thread waiting for response
this.replyQueues.get(message.getId()).add(message);
// Consumed elsewhere (getResponseWithTimeout)
log.info("addToQueue - queue size: {}, message type: {} (id: {})", queue.size(), message.getType(), message.getId());
}
else if (!this.pendingMessages.offer(message)) {
log.info("[{}] Busy, no room to queue message from peer {} - discarding",
this.peerLink, this);
}
}
/**
* Set a packet to remote with the message format "close::<our_destination_hash>"
* This method is only useful for non-initiator links to close the remote initiator.
*
* @param link
*/
public void sendCloseToRemote(Link link) {
var baseDestination = RNSNetwork.getInstance().getBaseDestination();
if (nonNull(link) & (isFalse(link.isInitiator()))) {
// Note: if part of link we need to get the baseDesitination hash
//var data = concatArrays("close::".getBytes(UTF_8),link.getDestination().getHash());
var data = concatArrays("close::".getBytes(UTF_8), baseDestination.getHash());
Packet closePacket = new Packet(link, data);
var packetReceipt = closePacket.send();
packetReceipt.setDeliveryCallback(this::closePacketDelivered);
packetReceipt.setTimeout(1000L);
packetReceipt.setTimeoutCallback(this::packetTimedOut);
} else {
log.debug("can't send to null link");
}
}
/** PacketReceipt callbacks */
public void closePacketDelivered(PacketReceipt receipt) {
var rttString = new String("");
if (receipt.getStatus() == PacketReceiptStatus.DELIVERED) {
var rtt = receipt.getRtt(); // rtt (Java) is in milliseconds
this.lastPacketRtt = rtt;
if (rtt >= 1000) {
rtt = Math.round(rtt / 1000);
rttString = String.format("%d seconds", rtt);
} else {
rttString = String.format("%d miliseconds", rtt);
}
log.info("Shutdown packet confirmation received from {}, round-trip time is {}",
encodeHexString(receipt.getDestination().getHash()), rttString);
}
}
public void packetDelivered(PacketReceipt receipt) {
var rttString = "";
//log.info("packet delivered callback, receipt: {}", receipt);
if (receipt.getStatus() == PacketReceiptStatus.DELIVERED) {
var rtt = receipt.getRtt(); // rtt (Java) is in milliseconds
this.lastPacketRtt = rtt;
//log.info("qqp - packetDelivered - rtt: {}", rtt);
if (rtt >= 1000) {
rtt = Math.round((float) rtt / 1000);
rttString = String.format("%d seconds", rtt);
} else {
rttString = String.format("%d milliseconds", rtt);
}
if (getIsInitiator()) {
// reporting round trip time in one direction is enough
log.info("Valid reply received from {}, round-trip time is {}",
encodeHexString(receipt.getDestination().getHash()), rttString);
}
this.lastAccessTimestamp = Instant.now();
}
}
public void packetTimedOut(PacketReceipt receipt) {
//log.info("packet timed out, receipt status: {}", receipt.getStatus());
if (receipt.getStatus() == PacketReceiptStatus.FAILED) {
log.info("packet timed out, receipt status: {}", PacketReceiptStatus.FAILED);
this.peerTimedOut = true;
this.peerLink.teardown();
}
//this.peerTimedOut = true;
//this.peerLink.teardown();
}
/** Link Request callbacks */
public void linkRequestResponseReceived(RequestReceipt rr) {
log.info("Response received");
}
public void linkRequestResponseProgress(RequestReceipt rr) {
this.requestResponseProgress = rr.getProgress();
log.debug("Response progress set");
}
public void linkRequestFailed(RequestReceipt rr) {
log.error("Request failed");
}
/** Link Resource callbacks */
// Resource: allow arbitrary amounts of data to be passed over a link with
// sequencing, compression, coordination and checksumming handled automatically
//public Boolean linkResourceAdvertised(Resource resource) {
// log.debug("Resource advertised");
//}
public void linkResourceTransferStarted(Resource resource) {
log.debug("Resource transfer started");
}
public void linkResourceTransferConcluded(Resource resource) {
log.debug("Resource transfer complete");
}
/** Utility methods */
public void pingRemote() {
var link = this.peerLink;
if (nonNull(link)) {
if (peerLink.getStatus() == ACTIVE) {
log.info("pinging remote (direct, 1 packet): {}", encodeHexString(link.getLinkId()));
var data = "ping".getBytes(UTF_8);
link.setPacketCallback(this::linkPacketReceived);
Packet pingPacket = new Packet(link, data);
PacketReceipt packetReceipt = pingPacket.send();
packetReceipt.setDeliveryCallback(this::packetDelivered);
// Note: don't setTimeout, we want it to timeout with FAIL if not deliverable
//packetReceipt.setTimeout(5000L);
packetReceipt.setTimeoutCallback(this::packetTimedOut);
} else {
log.info("can't send ping to a peer {} with (link) status: {}",
encodeHexString(peerLink.getDestination().getHash()), peerLink.getStatus());
}
}
}
//public void shutdownLink(Link link) {
// var data = "shutdown".getBytes(UTF_8);
// Packet shutdownPacket = new Packet(link, data);
// PacketReceipt packetReceipt = shutdownPacket.send();
// packetReceipt.setTimeout(2000L);
// packetReceipt.setTimeoutCallback(this::packetTimedOut);
// packetReceipt.setDeliveryCallback(this::shutdownPacketDelivered);
//}
/** qortal networking specific (Tasks) */
private void onPingMessage(RNSPeer peer, Message message) {
PingMessage pingMessage = (PingMessage) message;
try {
PongMessage pongMessage = new PongMessage();
pongMessage.setId(message.getId()); // use the ping message id (for ping getResponse)
this.peerBuffer.write(pongMessage.toBytes());
this.peerBuffer.flush();
this.lastAccessTimestamp = Instant.now();
} catch (MessageException e) {
//log.error("{} from peer {}", e.getMessage(), this);
log.error("{} from peer {}", e, this);
}
}
/**
* Send message to peer and await response, using default RESPONSE_TIMEOUT.
* <p>
* Message is assigned a random ID and sent.
* Responses are handled by registered callbacks.
* <p>
* Note: The method is called "get..." to match the original method name
*
* @param message message to send
* @return <code>Message</code> if valid response received; <code>null</code> if not or error/exception occurs
* @throws InterruptedException if interrupted while waiting
*/
public Message getResponse(Message message) throws InterruptedException {
//log.info("RNSPingTask action - pinging peer {}", encodeHexString(getDestinationHash()));
return getResponseWithTimeout(message, RESPONSE_TIMEOUT);
}
/**
* Send message to peer and await response.
* <p>
* Message is assigned a random ID and sent.
* If a response with matching ID is received then it is returned to caller.
* <p>
* If no response with matching ID within timeout, or some other error/exception occurs,
* then return <code>null</code>.<br>
* (Assume peer will be rapidly disconnected after this).
*
* @param message message to send
* @return <code>Message</code> if valid response received; <code>null</code> if not or error/exception occurs
* @throws InterruptedException if interrupted while waiting
*/
public Message getResponseWithTimeout(Message message, int timeout) throws InterruptedException {
BlockingQueue<Message> blockingQueue = new ArrayBlockingQueue<>(1);
// Assign random ID to this message
Random random = new Random();
int id;
do {
id = random.nextInt(Integer.MAX_VALUE - 1) + 1;
// Put queue into map (keyed by message ID) so we can poll for a response
// If putIfAbsent() doesn't return null, then this ID is already taken
} while (this.replyQueues.putIfAbsent(id, blockingQueue) != null);
message.setId(id);
//log.info("getResponse - before send {} message, random id is {}", message.getType(), id);
// Try to send message
if (!this.sendMessageWithTimeout(message, timeout)) {
this.replyQueues.remove(id);
return null;
}
//log.info("getResponse - after send");
try {
return blockingQueue.poll(timeout, TimeUnit.MILLISECONDS);
} finally {
this.replyQueues.remove(id);
//log.info("getResponse - regular - id removed from replyQueues");
}
}
/**
* Attempt to send Message to peer using the buffer and a custom timeout.
*
* @param message message to be sent
* @return <code>true</code> if message successfully sent; <code>false</code> otherwise
*/
public boolean sendMessageWithTimeout(Message message, int timeout) {
try {
// send the message
log.trace("Sending {} message with ID {} to peer {}", message.getType().name(), message.getId(), this);
var peerBuffer = getOrInitPeerBuffer();
this.peerBuffer.write(message.toBytes());
this.peerBuffer.flush();
//// send a message to confirm receipt over the buffer
//var messageId = message.getId();
//var confirmData = concatArrays(SEQ_REQUEST_CONFIRM_ID,"::".getBytes(UTF_8), messageId.getBytes(UTF_8));
//this.peerBuffer.write(confirmData);
//this.peerBuffer.flush();
return true;
//} catch (InterruptedException e) {
// // Send failure
// return false;
} catch (IllegalStateException e) {
//log.warn("Can't write to buffer (remote buffer down?)");
this.peerLink.teardown();
this.peerBuffer = null;
log.error("IllegalStateException - can't write to buffer: {}", e);
return false;
} catch (MessageException e) {
log.error(e.getMessage(), e);
return false;
}
}
protected Task getMessageTask() {
/*
* If our peerLink is not in ACTIVE node and there is a message yet to be
* processed then don't produce another message task.
* This allows us to process remaining messages sequentially.
*/
if (this.peerLink.getStatus() != ACTIVE) {
return null;
}
final Message nextMessage = this.pendingMessages.poll();
if (nextMessage == null) {
return null;
}
// Return a task to process message in queue
return new RNSMessageTask(this, nextMessage);
}
/**
* Send a Qortal message using a Reticulum Buffer
*
* @param message message to be sent
* @return <code>true</code> if message successfully sent; <code>false</code> otherwise
*/
//@Synchronized
public boolean sendMessage(Message message) {
try {
log.trace("Sending {} message with ID {} to peer {}", message.getType().name(), message.getId(), this.toString());
//log.info("Sending {} message with ID {} to peer {}", message.getType().name(), message.getId(), this.toString());
var peerBuffer = getOrInitPeerBuffer();
peerBuffer.write(message.toBytes());
peerBuffer.flush();
return true;
} catch (IllegalStateException e) {
this.peerLink.teardown();
this.peerBuffer = null;
log.error("IllegalStateException - can't write to buffer: {}", e);
return false;
} catch (MessageException e) {
log.error(e.getMessage(), e);
return false;
}
}
protected void startPings() {
log.trace("[{}] Enabling pings for peer {}",
peerLink.getDestination().getHexHash(), this.toString());
this.lastPingSent = NTP.getTime();
}
protected Task getPingTask(Long now) {
// Pings not enabled yet?
if (now == null || this.lastPingSent == null) {
return null;
}
// ping only possible over ACTIVE Link
if (nonNull(this.peerLink)) {
if (this.peerLink.getStatus() != ACTIVE) {
return null;
}
} else {
return null;
}
// Time to send another ping?
if (now < this.lastPingSent + PING_INTERVAL) {
return null; // Not yet
}
// Not strictly true, but prevents this peer from being immediately chosen again
this.lastPingSent = now;
return new RNSPingTask(this, now);
}
// low-level Link (packet) ping
protected Link getPingLinks(Long now) {
if (now == null || this.lastPingSent == null) {
return null;
}
// ping only possible over ACTIVE link
if (nonNull(this.peerLink)) {
if (this.peerLink.getStatus() != ACTIVE) {
return null;
}
} else {
return null;
}
if (now < this.lastPingSent + LINK_PING_INTERVAL) {
return null;
}
this.lastPingSent = now;
return this.peerLink;
}
// Peer methods reticulum implementations
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(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() {
return this.commonBlockData;
}
public void setCommonBlockData(CommonBlockData commonBlockData) {
this.commonBlockData = commonBlockData;
}
// Common block data
public boolean canUseCachedCommonBlockData() {
BlockSummaryData peerChainTipData = this.getChainTipData();
if (peerChainTipData == null || peerChainTipData.getSignature() == null)
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;
}
// Pending signature requests
public void addPendingSignatureRequest(byte[] signature) {
// Check if we already have this signature in the list
for (byte[] existingSignature : this.pendingSignatureRequests) {
if (Arrays.equals(existingSignature, signature )) {
return;
}
}
this.pendingSignatureRequests.add(signature);
}
public void removePendingSignatureRequest(byte[] signature) {
Iterator iterator = this.pendingSignatureRequests.iterator();
while (iterator.hasNext()) {
byte[] existingSignature = (byte[]) iterator.next();
if (Arrays.equals(existingSignature, signature)) {
iterator.remove();
}
}
}
public List<byte[]> getPendingSignatureRequests() {
return this.pendingSignatureRequests;
}
// Details used by API
public long getConnectionEstablishedTime() {
return linkEstablishedTime;
}
public long getConnectionAge() {
if (linkEstablishedTime > 0L) {
return System.currentTimeMillis() - linkEstablishedTime;
}
return linkEstablishedTime;
}
}

View File

@@ -0,0 +1,27 @@
package org.qortal.network.task;
import org.qortal.controller.Controller;
//import org.qortal.network.RNSNetwork;
//import org.qortal.repository.DataException;
import org.qortal.utils.ExecuteProduceConsume.Task;
public class RNSPrunePeersTask implements Task {
public RNSPrunePeersTask() {
}
@Override
public String getName() {
return "PrunePeersTask";
}
@Override
public void perform() throws InterruptedException {
Controller.getInstance().doRNSPrunePeers();
//try {
// log.debug("Pruning peers...");
// RNSNetwork.getInstance().prunePeers();
//} catch (DataException e) {
// log.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
//}
}
}

View File

@@ -9,6 +9,8 @@ import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Network message for sending over network, or unpacked data received from network.
@@ -33,6 +35,7 @@ import java.util.Arrays;
* </p>
*/
public abstract class Message {
private static final Logger LOGGER = LogManager.getLogger(Message.class);
// MAGIC(4) + TYPE(4) + HAS-ID(1) + ID?(4) + DATA-SIZE(4) + CHECKSUM?(4) + DATA?(*)
private static final int MAGIC_LENGTH = 4;
@@ -95,9 +98,11 @@ public abstract class Message {
byte[] messageMagic = new byte[MAGIC_LENGTH];
readOnlyBuffer.get(messageMagic);
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic())) {
LOGGER.info("xyz - mM: {}, Network getMessageMagic: {}", messageMagic, Network.getInstance().getMessageMagic());
// Didn't receive correct Message "magic"
throw new MessageException("Received incorrect message 'magic'");
}
// Find supporting object
int typeValue = readOnlyBuffer.getInt();

View File

@@ -0,0 +1,19 @@
package org.qortal.network.task;
import org.qortal.controller.Controller;
import org.qortal.utils.ExecuteProduceConsume.Task;
public class RNSBroadcastTask implements Task {
public RNSBroadcastTask() {
}
@Override
public String getName() {
return "BroadcastTask";
}
@Override
public void perform() throws InterruptedException {
Controller.getInstance().doRNSNetworkBroadcast();
}
}

View File

@@ -0,0 +1,30 @@
package org.qortal.network.task;
import org.qortal.network.RNSNetwork;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.Message;
import org.qortal.utils.ExecuteProduceConsume.Task;
public class RNSMessageTask implements Task {
private final RNSPeer peer;
private final Message nextMessage;
private final String name;
public RNSMessageTask(RNSPeer peer, Message nextMessage) {
this.peer = peer;
this.nextMessage = nextMessage;
this.name = "MessageTask::" + peer + "::" + nextMessage.getType();
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
//RNSNetwork.getInstance().onMessage(peer, nextMessage);
// TODO: what do we do in the Reticulum case?
// Note: this is automatically handled (asynchronously) by the RNSPeer peerBufferReady callback
}
}

View File

@@ -0,0 +1,44 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.RNSPeer;
import org.qortal.network.message.Message;
import org.qortal.network.message.MessageType;
import org.qortal.network.message.PingMessage;
import org.qortal.network.message.MessageException;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
public class RNSPingTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(PingTask.class);
private final RNSPeer peer;
private final Long now;
private final String name;
public RNSPingTask(RNSPeer peer, Long now) {
this.peer = peer;
this.now = now;
this.name = "PingTask::" + peer;
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
PingMessage pingMessage = new PingMessage();
// Note: Even though getResponse would work, we can use
// peer.sendMessage(pingMessage) using Reticulum buffer instead.
// More efficient and saves room for other request/response tasks.
peer.getResponse(pingMessage);
//peer.sendMessage(pingMessage);
//// task is not over here (Reticulum is asynchronous)
//peer.setLastPing(NTP.getTime() - now);
}
}

View File

@@ -614,6 +614,17 @@ public class Settings {
}
}
// Related to mesh networking
/** Preferred network: "tcpip" or "reticulum" */
private String preferredNetwork = "reticulum";
/** Maximum number of Reticulum peers allowed. */
private int reticulumMaxPeers = 55;
/** Minimum number of Reticulum peers desired. */
private int reticulumMinDesiredPeers = 8;
/** Maximum number of task executor network threads */
private int reticulumMaxNetworkThreadPoolSize = 89;
// Constructors
private Settings() {
@@ -1371,6 +1382,22 @@ public class Settings {
return connectionPoolMonitorEnabled;
}
public String getPreferredNetwork () {
return this.preferredNetwork.toLowerCase(Locale.getDefault());
}
public int getReticulumMaxPeers() {
return this.reticulumMaxPeers;
}
public int getReticulumMinDesiredPeers() {
return this.reticulumMinDesiredPeers;
}
public int getReticulumMaxNetworkThreadPoolSize() {
return this.reticulumMaxNetworkThreadPoolSize;
}
public int getBuildArbitraryResourcesBatchSize() {
return buildArbitraryResourcesBatchSize;
}

View File

@@ -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: 4242
# network_name: qortal

View File

@@ -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: qortaltest
# 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: 4242
# network_name: qortaltest

View File

@@ -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();
}
}
}
}

View File

@@ -48,6 +48,9 @@ JVM_MEMORY_ARGS="-XX:MaxRAMPercentage=50 -XX:+UseG1GC -Xss1024k"
nohup nice -n 20 java \
-Djava.net.preferIPv4Stack=false \
${JVM_MEMORY_ARGS} \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--illegal-access=warn \
-jar qortal.jar \
1>run.log 2>&1 &