forked from Qortal/qortal
Merge branch 'master' into qdn-metadata
# Conflicts: # src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java
This commit is contained in:
commit
4821139501
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM maven:3-openjdk-11 as builder
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
COPY ./ /work/
|
||||||
|
RUN mvn clean package
|
||||||
|
|
||||||
|
###
|
||||||
|
FROM openjdk:11
|
||||||
|
|
||||||
|
RUN useradd -r -u 1000 -g users qortal && \
|
||||||
|
mkdir /usr/local/qortal /qortal && \
|
||||||
|
chown 1000:100 /qortal
|
||||||
|
|
||||||
|
COPY --from=builder /work/log4j2.properties /usr/local/qortal/
|
||||||
|
COPY --from=builder /work/target/qortal*.jar /usr/local/qortal/qortal.jar
|
||||||
|
|
||||||
|
USER 1000:100
|
||||||
|
|
||||||
|
EXPOSE 12391 12392
|
||||||
|
HEALTHCHECK --start-period=5m CMD curl -sf http://127.0.0.1:12391/admin/info || exit 1
|
||||||
|
|
||||||
|
WORKDIR /qortal
|
||||||
|
VOLUME /qortal
|
||||||
|
|
||||||
|
ENTRYPOINT ["java"]
|
||||||
|
CMD ["-Djava.net.preferIPv4Stack=false", "-jar", "/usr/local/qortal/qortal.jar"]
|
@ -1,10 +1,4 @@
|
|||||||
# Qortal Data Node
|
# Qortal Project - Official Repo
|
||||||
|
|
||||||
## Important
|
|
||||||
|
|
||||||
This code is unfinished, and we haven't had the official genesis block for the data chain yet.
|
|
||||||
Therefore it is only possible to use this code if you first create your own test chain. I would
|
|
||||||
highly recommend waiting until the code is in a more complete state before trying to run this.
|
|
||||||
|
|
||||||
## Build / run
|
## Build / run
|
||||||
|
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||||
<ROW Property="NTP_GOOD" Value="false"/>
|
<ROW Property="NTP_GOOD" Value="false"/>
|
||||||
<ROW Property="ProductCode" Value="1033:{1FB9DC61-308D-4726-B993-82B2D51AD453} 1049:{0EAB0862-4F57-4D79-B8B4-C3E1D84484A2} 2052:{28F67872-0D8B-48D1-9DE1-7F11D5919CB8} 2057:{725A8624-CB8A-426D-8162-68980ED45BCB} " Type="16"/>
|
<ROW Property="ProductCode" Value="1033:{9BDE0BDF-72A2-44DA-8B55-E7C129DBE603} 1049:{F4FCC1D9-D286-4B3D-A50F-82034010A30F} 2052:{DBE9D682-F666-49BA-8B63-28C0AE06CBCA} 2057:{949F4DFE-E55C-4493-AAB6-5DB13E68C754} " Type="16"/>
|
||||||
<ROW Property="ProductLanguage" Value="2057"/>
|
<ROW Property="ProductLanguage" Value="2057"/>
|
||||||
<ROW Property="ProductName" Value="Qortal"/>
|
<ROW Property="ProductName" Value="Qortal"/>
|
||||||
<ROW Property="ProductVersion" Value="3.0.1" Type="32"/>
|
<ROW Property="ProductVersion" Value="3.0.4" Type="32"/>
|
||||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||||
<ROW Component="AI_CustomARPName" ComponentId="{163A0AFB-3694-4E1B-ABB8-7C5F28F61305}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
<ROW Component="AI_CustomARPName" ComponentId="{9B71A82D-8C25-40FD-806D-44BAD0B45AA2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||||
@ -1173,7 +1173,7 @@
|
|||||||
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
|
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
|
||||||
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
|
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
|
||||||
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
||||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||||
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
||||||
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
||||||
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
||||||
|
2
pom.xml
2
pom.xml
@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>3.0.1</version>
|
<version>3.0.4</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
|
@ -7,14 +7,13 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||||
|
import org.qortal.api.ApiKey;
|
||||||
import org.qortal.api.ApiRequest;
|
import org.qortal.api.ApiRequest;
|
||||||
import org.qortal.controller.AutoUpdate;
|
import org.qortal.controller.AutoUpdate;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
@ -70,14 +69,40 @@ public class ApplyUpdate {
|
|||||||
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||||
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
|
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
|
||||||
|
|
||||||
|
// The /admin/stop endpoint requires an API key, which may or may not be already generated
|
||||||
|
boolean apiKeyNewlyGenerated = false;
|
||||||
|
ApiKey apiKey = null;
|
||||||
|
try {
|
||||||
|
apiKey = new ApiKey();
|
||||||
|
if (!apiKey.generated()) {
|
||||||
|
apiKey.generate();
|
||||||
|
apiKeyNewlyGenerated = true;
|
||||||
|
LOGGER.info("Generated API key");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Error loading API key: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GET params
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
if (apiKey != null) {
|
||||||
|
params.put("apiKey", apiKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to stop the node
|
||||||
int attempt;
|
int attempt;
|
||||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||||
final int attemptForLogging = attempt;
|
final int attemptForLogging = attempt;
|
||||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||||
String response = ApiRequest.perform(baseUri + "admin/stop", null);
|
String response = ApiRequest.perform(baseUri + "admin/stop", params);
|
||||||
if (response == null)
|
if (response == null) {
|
||||||
// No response - consider node shut down
|
// No response - consider node shut down
|
||||||
|
if (apiKeyNewlyGenerated) {
|
||||||
|
// API key was newly generated for this auto update, so we need to remove it
|
||||||
|
ApplyUpdate.removeGeneratedApiKey();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.info(() -> String.format("Response from API: %s", response));
|
LOGGER.info(() -> String.format("Response from API: %s", response));
|
||||||
|
|
||||||
@ -89,6 +114,11 @@ public class ApplyUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKeyNewlyGenerated) {
|
||||||
|
// API key was newly generated for this auto update, so we need to remove it
|
||||||
|
ApplyUpdate.removeGeneratedApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt == MAX_ATTEMPTS) {
|
if (attempt == MAX_ATTEMPTS) {
|
||||||
LOGGER.error("Failed to shutdown node - giving up");
|
LOGGER.error("Failed to shutdown node - giving up");
|
||||||
return false;
|
return false;
|
||||||
@ -97,6 +127,19 @@ public class ApplyUpdate {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void removeGeneratedApiKey() {
|
||||||
|
try {
|
||||||
|
LOGGER.info("Removing newly generated API key...");
|
||||||
|
|
||||||
|
// Delete the API key since it was only generated for this auto update
|
||||||
|
ApiKey apiKey = new ApiKey();
|
||||||
|
apiKey.delete();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void replaceJar() {
|
private static void replaceJar() {
|
||||||
// Assuming current working directory contains the JAR files
|
// Assuming current working directory contains the JAR files
|
||||||
Path realJar = Paths.get(JAR_FILENAME);
|
Path realJar = Paths.get(JAR_FILENAME);
|
||||||
|
@ -81,6 +81,15 @@ public class ApiKey {
|
|||||||
writer.close();
|
writer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void delete() throws IOException {
|
||||||
|
this.apiKey = null;
|
||||||
|
|
||||||
|
Path filePath = this.getFilePath();
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
Files.delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean generated() {
|
public boolean generated() {
|
||||||
return (this.apiKey != null);
|
return (this.apiKey != null);
|
||||||
|
@ -65,7 +65,7 @@ public class GatewayResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||||
return resource.getStatus();
|
return resource.getStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
|
|||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
|
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@ -22,7 +23,7 @@ public class NodeStatus {
|
|||||||
public NodeStatus() {
|
public NodeStatus() {
|
||||||
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||||
|
|
||||||
this.syncPercent = Controller.getInstance().getSyncPercent();
|
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||||
this.isSynchronizing = this.syncPercent != null;
|
this.isSynchronizing = this.syncPercent != null;
|
||||||
|
|
||||||
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
||||||
|
@ -44,6 +44,7 @@ import org.qortal.api.model.NodeInfo;
|
|||||||
import org.qortal.api.model.NodeStatus;
|
import org.qortal.api.model.NodeStatus;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||||
import org.qortal.data.account.MintingAccountData;
|
import org.qortal.data.account.MintingAccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
@ -525,7 +526,7 @@ public class AdminResource {
|
|||||||
SynchronizationResult syncResult;
|
SynchronizationResult syncResult;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
syncResult = Controller.getInstance().actuallySynchronize(targetPeer, true);
|
syncResult = Synchronizer.getInstance().actuallySynchronize(targetPeer, true);
|
||||||
} while (syncResult == SynchronizationResult.OK);
|
} while (syncResult == SynchronizationResult.OK);
|
||||||
} finally {
|
} finally {
|
||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
|
@ -237,9 +237,10 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public ArbitraryResourceStatus getDefaultResourceStatus(@PathParam("service") Service service,
|
public ArbitraryResourceStatus getDefaultResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
@PathParam("name") String name,
|
@PathParam("service") Service service,
|
||||||
@QueryParam("build") Boolean build) {
|
@PathParam("name") String name,
|
||||||
|
@QueryParam("build") Boolean build) {
|
||||||
|
|
||||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||||
return this.getStatus(service, name, null, build);
|
return this.getStatus(service, name, null, build);
|
||||||
@ -257,10 +258,11 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public ArbitraryResourceStatus getResourceStatus(@PathParam("service") Service service,
|
public ArbitraryResourceStatus getResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
@PathParam("name") String name,
|
@PathParam("service") Service service,
|
||||||
@PathParam("identifier") String identifier,
|
@PathParam("name") String name,
|
||||||
@QueryParam("build") Boolean build) {
|
@PathParam("identifier") String identifier,
|
||||||
|
@QueryParam("build") Boolean build) {
|
||||||
|
|
||||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||||
return this.getStatus(service, name, identifier, build);
|
return this.getStatus(service, name, identifier, build);
|
||||||
@ -601,10 +603,16 @@ public class ArbitraryResource {
|
|||||||
@PathParam("service") Service service,
|
@PathParam("service") Service service,
|
||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@QueryParam("filepath") String filepath,
|
@QueryParam("filepath") String filepath,
|
||||||
@QueryParam("rebuild") boolean rebuild) {
|
@QueryParam("rebuild") boolean rebuild,
|
||||||
Security.checkApiCallAllowed(request);
|
@QueryParam("async") boolean async,
|
||||||
|
@QueryParam("attempts") Integer attempts) {
|
||||||
|
|
||||||
return this.download(service, name, null, filepath, rebuild);
|
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||||
|
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.download(service, name, null, filepath, rebuild, async, attempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -630,10 +638,16 @@ public class ArbitraryResource {
|
|||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@PathParam("identifier") String identifier,
|
@PathParam("identifier") String identifier,
|
||||||
@QueryParam("filepath") String filepath,
|
@QueryParam("filepath") String filepath,
|
||||||
@QueryParam("rebuild") boolean rebuild) {
|
@QueryParam("rebuild") boolean rebuild,
|
||||||
Security.checkApiCallAllowed(request);
|
@QueryParam("async") boolean async,
|
||||||
|
@QueryParam("attempts") Integer attempts) {
|
||||||
|
|
||||||
return this.download(service, name, identifier, filepath, rebuild);
|
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||||
|
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.download(service, name, identifier, filepath, rebuild, async, attempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1123,30 +1137,45 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, boolean rebuild) {
|
private HttpServletResponse download(Service service, String name, String identifier, String filepath, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||||
|
|
||||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
|
if (maxAttempts == null) {
|
||||||
|
maxAttempts = 5;
|
||||||
|
}
|
||||||
|
|
||||||
// Loop until we have data
|
// Loop until we have data
|
||||||
while (!Controller.isStopping()) {
|
if (async) {
|
||||||
attempts++;
|
// Asynchronous
|
||||||
if (!arbitraryDataReader.isBuilding()) {
|
arbitraryDataReader.loadAsynchronously(false, 1);
|
||||||
try {
|
}
|
||||||
arbitraryDataReader.loadSynchronously(rebuild);
|
else {
|
||||||
break;
|
// Synchronous
|
||||||
} catch (MissingDataException e) {
|
while (!Controller.isStopping()) {
|
||||||
if (attempts > 5) {
|
attempts++;
|
||||||
// Give up after 5 attempts
|
if (!arbitraryDataReader.isBuilding()) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
try {
|
||||||
|
arbitraryDataReader.loadSynchronously(rebuild);
|
||||||
|
break;
|
||||||
|
} catch (MissingDataException e) {
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
// Give up after 5 attempts
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Thread.sleep(3000L);
|
||||||
}
|
}
|
||||||
Thread.sleep(3000L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||||
|
if (outputPath == null) {
|
||||||
|
// Assume the resource doesn't exist
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, "File not found");
|
||||||
|
}
|
||||||
|
|
||||||
if (filepath == null || filepath.isEmpty()) {
|
if (filepath == null || filepath.isEmpty()) {
|
||||||
// No file path supplied - so check if this is a single file resource
|
// No file path supplied - so check if this is a single file resource
|
||||||
@ -1155,6 +1184,10 @@ public class ArbitraryResource {
|
|||||||
// This is a single file resource
|
// This is a single file resource
|
||||||
filepath = files[0];
|
filepath = files[0];
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA,
|
||||||
|
"filepath is required for resources containing more than one file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: limit file size that can be read into memory
|
// TODO: limit file size that can be read into memory
|
||||||
@ -1191,7 +1224,7 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||||
return resource.getStatus();
|
return resource.getStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||||
@ -1200,7 +1233,7 @@ public class ArbitraryResource {
|
|||||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||||
resourceInfo.service, resourceInfo.identifier);
|
resourceInfo.service, resourceInfo.identifier);
|
||||||
ArbitraryResourceStatus status = resource.getStatus();
|
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
resourceInfo.status = status;
|
resourceInfo.status = status;
|
||||||
}
|
}
|
||||||
|
@ -67,11 +67,16 @@ public class CrossChainBitcoinResource {
|
|||||||
if (!bitcoin.isValidDeterministicKey(key58))
|
if (!bitcoin.isValidDeterministicKey(key58))
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
Long balance = bitcoin.getWalletBalance(key58);
|
try {
|
||||||
if (balance == null)
|
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
return balance.toString();
|
return balance.toString();
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
|
@ -65,11 +65,16 @@ public class CrossChainDogecoinResource {
|
|||||||
if (!dogecoin.isValidDeterministicKey(key58))
|
if (!dogecoin.isValidDeterministicKey(key58))
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
Long balance = dogecoin.getWalletBalance(key58);
|
try {
|
||||||
if (balance == null)
|
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
return balance.toString();
|
return balance.toString();
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
|
@ -67,11 +67,16 @@ public class CrossChainLitecoinResource {
|
|||||||
if (!litecoin.isValidDeterministicKey(key58))
|
if (!litecoin.isValidDeterministicKey(key58))
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
Long balance = litecoin.getWalletBalance(key58);
|
try {
|
||||||
if (balance == null)
|
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
return balance.toString();
|
return balance.toString();
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
|
@ -9,6 +9,8 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -44,6 +46,7 @@ import org.qortal.transform.transaction.TransactionTransformer;
|
|||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@Path("/transactions")
|
@Path("/transactions")
|
||||||
@Tag(name = "Transactions")
|
@Tag(name = "Transactions")
|
||||||
@ -363,6 +366,83 @@ public class TransactionsResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/unitfee")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get transaction unit fee",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "number"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||||
|
})
|
||||||
|
public long getTransactionUnitFee(@QueryParam("txType") TransactionType txType,
|
||||||
|
@QueryParam("timestamp") Long timestamp,
|
||||||
|
@QueryParam("level") Integer accountLevel) {
|
||||||
|
try {
|
||||||
|
if (timestamp == null) {
|
||||||
|
timestamp = NTP.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
Constructor<?> constructor = txType.constructor;
|
||||||
|
Transaction transaction = (Transaction) constructor.newInstance(null, null);
|
||||||
|
// FUTURE: add accountLevel parameter to transaction.getUnitFee() if needed
|
||||||
|
return transaction.getUnitFee(timestamp);
|
||||||
|
|
||||||
|
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/fee")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get recommended fee for supplied transaction data",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||||
|
})
|
||||||
|
public long getRecommendedTransactionFee(String rawInputBytes58) {
|
||||||
|
byte[] rawInputBytes = Base58.decode(rawInputBytes58);
|
||||||
|
if (rawInputBytes.length == 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Append null signature on the end before transformation
|
||||||
|
byte[] rawBytes = Bytes.concat(rawInputBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||||
|
|
||||||
|
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||||
|
if (transactionData == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
return transaction.calcRecommendedFee();
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/creator/{publickey}")
|
@Path("/creator/{publickey}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -13,8 +13,11 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
|||||||
private final Long creationTimestamp;
|
private final Long creationTimestamp;
|
||||||
private Long buildStartTimestamp = null;
|
private Long buildStartTimestamp = null;
|
||||||
private Long buildEndTimestamp = null;
|
private Long buildEndTimestamp = null;
|
||||||
|
private Integer priority = 0;
|
||||||
private boolean failed = false;
|
private boolean failed = false;
|
||||||
|
|
||||||
|
private static int HIGH_PRIORITY_THRESHOLD = 5;
|
||||||
|
|
||||||
/* The maximum amount of time to spend on a single build */
|
/* The maximum amount of time to spend on a single build */
|
||||||
// TODO: interrupt an in-progress build
|
// TODO: interrupt an in-progress build
|
||||||
public static long BUILD_TIMEOUT = 60*1000L; // 60 seconds
|
public static long BUILD_TIMEOUT = 60*1000L; // 60 seconds
|
||||||
@ -27,13 +30,20 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
|||||||
this.creationTimestamp = NTP.getTime();
|
this.creationTimestamp = NTP.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void prepareForBuild() {
|
||||||
|
this.buildStartTimestamp = NTP.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
public void build() throws IOException, DataException, MissingDataException {
|
public void build() throws IOException, DataException, MissingDataException {
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
if (now == null) {
|
if (now == null) {
|
||||||
|
this.buildStartTimestamp = null;
|
||||||
throw new DataException("NTP time hasn't synced yet");
|
throw new DataException("NTP time hasn't synced yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buildStartTimestamp = now;
|
if (this.buildStartTimestamp == null) {
|
||||||
|
this.buildStartTimestamp = now;
|
||||||
|
}
|
||||||
ArbitraryDataReader arbitraryDataReader =
|
ArbitraryDataReader arbitraryDataReader =
|
||||||
new ArbitraryDataReader(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
new ArbitraryDataReader(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||||
|
|
||||||
@ -70,6 +80,21 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
|||||||
return this.buildStartTimestamp;
|
return this.buildStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getPriority() {
|
||||||
|
if (this.priority != null) {
|
||||||
|
return this.priority;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriority(Integer priority) {
|
||||||
|
this.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHighPriority() {
|
||||||
|
return this.priority >= HIGH_PRIORITY_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
public void setFailed(boolean failed) {
|
public void setFailed(boolean failed) {
|
||||||
this.failed = failed;
|
this.failed = failed;
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,9 @@ public class ArbitraryDataCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No need to invalidate the cache
|
// No need to invalidate the cache
|
||||||
|
// Remember that it's up to date, so that we won't check again for a while
|
||||||
|
ArbitraryDataManager.getInstance().addResourceToCache(this.getArbitraryDataResource());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,14 +87,7 @@ public class ArbitraryDataCache {
|
|||||||
|
|
||||||
// If the state's sig doesn't match the latest transaction's sig, we need to invalidate
|
// If the state's sig doesn't match the latest transaction's sig, we need to invalidate
|
||||||
// This means that an updated layer is available
|
// This means that an updated layer is available
|
||||||
if (this.shouldInvalidateDueToSignatureMismatch()) {
|
return this.shouldInvalidateDueToSignatureMismatch();
|
||||||
|
|
||||||
// Add to the in-memory cache first, so that we won't check again for a while
|
|
||||||
ArbitraryDataManager.getInstance().addResourceToCache(this.getArbitraryDataResource());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -439,6 +439,11 @@ public class ArbitraryDataFile {
|
|||||||
return chunk.exists();
|
return chunk.exists();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Arrays.equals(hash, this.metadataHash)) {
|
||||||
|
if (this.metadataFile != null) {
|
||||||
|
return this.metadataFile.exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Arrays.equals(this.getHash(), hash)) {
|
if (Arrays.equals(this.getHash(), hash)) {
|
||||||
return this.exists();
|
return this.exists();
|
||||||
}
|
}
|
||||||
@ -455,9 +460,6 @@ public class ArbitraryDataFile {
|
|||||||
|
|
||||||
if (this.metadataFile == null) {
|
if (this.metadataFile == null) {
|
||||||
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||||
if (!metadataFile.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the metadata file doesn't exist, we can't check if we have the chunks
|
// If the metadata file doesn't exist, we can't check if we have the chunks
|
||||||
@ -496,9 +498,6 @@ public class ArbitraryDataFile {
|
|||||||
|
|
||||||
if (this.metadataFile == null) {
|
if (this.metadataFile == null) {
|
||||||
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||||
if (!metadataFile.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the metadata file doesn't exist, we can't check if we have any chunks
|
// If the metadata file doesn't exist, we can't check if we have any chunks
|
||||||
@ -540,6 +539,50 @@ public class ArbitraryDataFile {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a list of file hashes for this transaction that we do not hold locally
|
||||||
|
*
|
||||||
|
* @return a List of chunk hashes, or null if we are unable to determine what is missing
|
||||||
|
*/
|
||||||
|
public List<byte[]> missingHashes() {
|
||||||
|
List<byte[]> missingHashes = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
if (this.metadataHash == null) {
|
||||||
|
// We don't have any metadata so can't check if we have the chunks
|
||||||
|
// Even if this transaction has no chunks, we don't have the file either (already checked above)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.metadataFile == null) {
|
||||||
|
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the metadata file doesn't exist, we can't check if we have the chunks
|
||||||
|
if (!metadataFile.getFilePath().toFile().exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.metadata == null) {
|
||||||
|
this.setMetadata(new ArbitraryDataTransactionMetadata(this.metadataFile.getFilePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the metadata
|
||||||
|
List<byte[]> chunks = metadata.getChunks();
|
||||||
|
for (byte[] chunkHash : chunks) {
|
||||||
|
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(chunkHash, this.signature);
|
||||||
|
if (!chunk.exists()) {
|
||||||
|
missingHashes.add(chunkHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return missingHashes;
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
// Something went wrong, so we can't make a sensible decision
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsChunk(byte[] hash) {
|
public boolean containsChunk(byte[] hash) {
|
||||||
for (ArbitraryDataFileChunk chunk : this.chunks) {
|
for (ArbitraryDataFileChunk chunk : this.chunks) {
|
||||||
if (Arrays.equals(hash, chunk.getHash())) {
|
if (Arrays.equals(hash, chunk.getHash())) {
|
||||||
|
@ -122,10 +122,22 @@ public class ArbitraryDataReader {
|
|||||||
* This adds the build task to a queue, and the result will be cached when complete
|
* This adds the build task to a queue, and the result will be cached when complete
|
||||||
* To check the status of the build, periodically call isCachedDataAvailable()
|
* To check the status of the build, periodically call isCachedDataAvailable()
|
||||||
* Once it returns true, you can then use getFilePath() to access the data itself.
|
* Once it returns true, you can then use getFilePath() to access the data itself.
|
||||||
|
*
|
||||||
|
* @param overwrite - set to true to force rebuild an existing cache
|
||||||
* @return true if added or already present in queue; false if not
|
* @return true if added or already present in queue; false if not
|
||||||
*/
|
*/
|
||||||
public boolean loadAsynchronously() {
|
public boolean loadAsynchronously(boolean overwrite, int priority) {
|
||||||
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(this.createQueueItem());
|
ArbitraryDataCache cache = new ArbitraryDataCache(this.uncompressedPath, overwrite,
|
||||||
|
this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||||
|
if (cache.isCachedDataAvailable()) {
|
||||||
|
// Use cached data
|
||||||
|
this.filePath = this.uncompressedPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArbitraryDataBuildQueueItem item = this.createQueueItem();
|
||||||
|
item.setPriority(priority);
|
||||||
|
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -363,7 +375,7 @@ public class ArbitraryDataReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Throw a missing data exception, which allows subsequent layers to fetch data
|
// Throw a missing data exception, which allows subsequent layers to fetch data
|
||||||
LOGGER.debug(message);
|
LOGGER.trace(message);
|
||||||
throw new MissingDataException(message);
|
throw new MissingDataException(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -458,12 +470,18 @@ public class ArbitraryDataReader {
|
|||||||
throw new DataException(String.format("Unable to unzip file: %s", e.getMessage()));
|
throw new DataException(String.format("Unable to unzip file: %s", e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace filePath pointer with the uncompressed file path
|
if (!this.uncompressedPath.toFile().exists()) {
|
||||||
|
throw new DataException(String.format("Unable to unzip file: %s", this.filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete original compressed file
|
||||||
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
||||||
if (Files.exists(this.filePath)) {
|
if (Files.exists(this.filePath)) {
|
||||||
Files.delete(this.filePath);
|
Files.delete(this.filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace filePath pointer with the uncompressed file path
|
||||||
this.filePath = this.uncompressedPath;
|
this.filePath = this.uncompressedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ public class ArbitraryDataRenderer {
|
|||||||
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
||||||
// If async is requested, show a loading screen whilst build is in progress
|
// If async is requested, show a loading screen whilst build is in progress
|
||||||
if (async) {
|
if (async) {
|
||||||
arbitraryDataReader.loadAsynchronously();
|
arbitraryDataReader.loadAsynchronously(false, 10);
|
||||||
return this.getLoadingResponse(service, resourceId);
|
return this.getLoadingResponse(service, resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
|||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
|
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.list.ResourceListManager;
|
import org.qortal.list.ResourceListManager;
|
||||||
@ -37,6 +38,8 @@ public class ArbitraryDataResource {
|
|||||||
private List<ArbitraryTransactionData> transactions;
|
private List<ArbitraryTransactionData> transactions;
|
||||||
private ArbitraryTransactionData latestPutTransaction;
|
private ArbitraryTransactionData latestPutTransaction;
|
||||||
private int layerCount;
|
private int layerCount;
|
||||||
|
private Integer localChunkCount = null;
|
||||||
|
private Integer totalChunkCount = null;
|
||||||
|
|
||||||
public ArbitraryDataResource(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
public ArbitraryDataResource(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||||
this.resourceId = resourceId.toLowerCase();
|
this.resourceId = resourceId.toLowerCase();
|
||||||
@ -50,50 +53,56 @@ public class ArbitraryDataResource {
|
|||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArbitraryResourceStatus getStatus() {
|
public ArbitraryResourceStatus getStatus(boolean quick) {
|
||||||
|
// Calculate the chunk counts
|
||||||
|
// Avoid this for "quick" statuses, to speed things up
|
||||||
|
if (!quick) {
|
||||||
|
this.calculateChunkCounts();
|
||||||
|
}
|
||||||
|
|
||||||
if (resourceIdType != ResourceIdType.NAME) {
|
if (resourceIdType != ResourceIdType.NAME) {
|
||||||
// We only support statuses for resources with a name
|
// We only support statuses for resources with a name
|
||||||
return new ArbitraryResourceStatus(Status.UNSUPPORTED);
|
return new ArbitraryResourceStatus(Status.UNSUPPORTED, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the name is blocked
|
// Check if the name is blocked
|
||||||
if (ResourceListManager.getInstance()
|
if (ResourceListManager.getInstance()
|
||||||
.listContains("blockedNames", this.resourceId, false)) {
|
.listContains("blockedNames", this.resourceId, false)) {
|
||||||
return new ArbitraryResourceStatus(Status.BLOCKED);
|
return new ArbitraryResourceStatus(Status.BLOCKED, this.localChunkCount, this.totalChunkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a build has failed
|
||||||
|
ArbitraryDataBuildQueueItem queueItem =
|
||||||
|
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
||||||
|
if (ArbitraryDataBuildManager.getInstance().isInFailedBuildsList(queueItem)) {
|
||||||
|
return new ArbitraryResourceStatus(Status.BUILD_FAILED, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firstly check the cache to see if it's already built
|
// Firstly check the cache to see if it's already built
|
||||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(
|
||||||
resourceId, resourceIdType, service, identifier);
|
resourceId, resourceIdType, service, identifier);
|
||||||
if (arbitraryDataReader.isCachedDataAvailable()) {
|
if (arbitraryDataReader.isCachedDataAvailable()) {
|
||||||
return new ArbitraryResourceStatus(Status.READY);
|
return new ArbitraryResourceStatus(Status.READY, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
|
||||||
|
|
||||||
// Next check if there's a build in progress
|
|
||||||
ArbitraryDataBuildQueueItem queueItem =
|
|
||||||
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
|
||||||
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
|
||||||
return new ArbitraryResourceStatus(Status.BUILDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a build has failed
|
|
||||||
if (ArbitraryDataBuildManager.getInstance().isInFailedBuildsList(queueItem)) {
|
|
||||||
return new ArbitraryResourceStatus(Status.BUILD_FAILED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have all data locally for this resource
|
// Check if we have all data locally for this resource
|
||||||
if (!this.allFilesDownloaded()) {
|
if (!this.allFilesDownloaded()) {
|
||||||
if (this.isDownloading()) {
|
if (this.isDownloading()) {
|
||||||
return new ArbitraryResourceStatus(Status.DOWNLOADING);
|
return new ArbitraryResourceStatus(Status.DOWNLOADING, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
else if (this.isDataPotentiallyAvailable()) {
|
else if (this.isDataPotentiallyAvailable()) {
|
||||||
return new ArbitraryResourceStatus(Status.PUBLISHED);
|
return new ArbitraryResourceStatus(Status.PUBLISHED, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
return new ArbitraryResourceStatus(Status.MISSING_DATA);
|
return new ArbitraryResourceStatus(Status.MISSING_DATA, this.localChunkCount, this.totalChunkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a build in progress
|
||||||
|
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
||||||
|
return new ArbitraryResourceStatus(Status.BUILDING, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have all data locally
|
// We have all data locally
|
||||||
return new ArbitraryResourceStatus(Status.DOWNLOADED);
|
return new ArbitraryResourceStatus(Status.DOWNLOADED, this.localChunkCount, this.totalChunkCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean delete() {
|
public boolean delete() {
|
||||||
@ -116,6 +125,9 @@ public class ArbitraryDataResource {
|
|||||||
// Also delete cached data for the entire resource
|
// Also delete cached data for the entire resource
|
||||||
this.deleteCache();
|
this.deleteCache();
|
||||||
|
|
||||||
|
// Invalidate the hosted transactions cache as we have removed an item
|
||||||
|
ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (DataException | IOException e) {
|
} catch (DataException | IOException e) {
|
||||||
@ -124,6 +136,13 @@ public class ArbitraryDataResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteCache() throws IOException {
|
public void deleteCache() throws IOException {
|
||||||
|
// Don't delete anything if there's a build in progress
|
||||||
|
ArbitraryDataBuildQueueItem queueItem =
|
||||||
|
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
||||||
|
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String baseDir = Settings.getInstance().getTempDataPath();
|
String baseDir = Settings.getInstance().getTempDataPath();
|
||||||
String identifier = this.identifier != null ? this.identifier : "default";
|
String identifier = this.identifier != null ? this.identifier : "default";
|
||||||
Path cachePath = Paths.get(baseDir, "reader", this.resourceIdType.toString(), this.resourceId, this.service.toString(), identifier);
|
Path cachePath = Paths.get(baseDir, "reader", this.resourceIdType.toString(), this.resourceId, this.service.toString(), identifier);
|
||||||
@ -136,6 +155,12 @@ public class ArbitraryDataResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean allFilesDownloaded() {
|
private boolean allFilesDownloaded() {
|
||||||
|
// Use chunk counts to speed things up if we can
|
||||||
|
if (this.localChunkCount != null && this.totalChunkCount != null &&
|
||||||
|
this.localChunkCount >= this.totalChunkCount) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fetchTransactions();
|
this.fetchTransactions();
|
||||||
|
|
||||||
@ -154,6 +179,25 @@ public class ArbitraryDataResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void calculateChunkCounts() {
|
||||||
|
try {
|
||||||
|
this.fetchTransactions();
|
||||||
|
|
||||||
|
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||||
|
int localChunkCount = 0;
|
||||||
|
int totalChunkCount = 0;
|
||||||
|
|
||||||
|
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||||
|
localChunkCount += ArbitraryTransactionUtils.ourChunkCount(transactionData);
|
||||||
|
totalChunkCount += ArbitraryTransactionUtils.totalChunkCount(transactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localChunkCount = localChunkCount;
|
||||||
|
this.totalChunkCount = totalChunkCount;
|
||||||
|
|
||||||
|
} catch (DataException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isRateLimited() {
|
private boolean isRateLimited() {
|
||||||
try {
|
try {
|
||||||
this.fetchTransactions();
|
this.fetchTransactions();
|
||||||
|
@ -68,9 +68,15 @@ public class BlockChain {
|
|||||||
atFindNextTransactionFix,
|
atFindNextTransactionFix,
|
||||||
newBlockSigHeight,
|
newBlockSigHeight,
|
||||||
shareBinFix,
|
shareBinFix,
|
||||||
calcChainWeightTimestamp;
|
calcChainWeightTimestamp,
|
||||||
|
transactionV5Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom transaction fees
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long nameRegistrationUnitFee;
|
||||||
|
private long nameRegistrationUnitFeeTimestamp;
|
||||||
|
|
||||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
/** Map of which blockchain features are enabled when (height/timestamp) */
|
||||||
@XmlJavaTypeAdapter(StringLongMapXmlAdapter.class)
|
@XmlJavaTypeAdapter(StringLongMapXmlAdapter.class)
|
||||||
private Map<String, Long> featureTriggers;
|
private Map<String, Long> featureTriggers;
|
||||||
@ -141,7 +147,8 @@ public class BlockChain {
|
|||||||
}
|
}
|
||||||
private List<BlockTimingByHeight> blockTimingsByHeight;
|
private List<BlockTimingByHeight> blockTimingsByHeight;
|
||||||
|
|
||||||
private int minAccountLevelToMint = 1;
|
private int minAccountLevelToMint;
|
||||||
|
private int minAccountLevelForBlockSubmissions;
|
||||||
private int minAccountLevelToRewardShare;
|
private int minAccountLevelToRewardShare;
|
||||||
private int maxRewardSharesPerMintingAccount;
|
private int maxRewardSharesPerMintingAccount;
|
||||||
private int founderEffectiveMintingLevel;
|
private int founderEffectiveMintingLevel;
|
||||||
@ -299,6 +306,16 @@ public class BlockChain {
|
|||||||
return this.maxBlockSize;
|
return this.maxBlockSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom transaction fees
|
||||||
|
public long getNameRegistrationUnitFee() {
|
||||||
|
return this.nameRegistrationUnitFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getNameRegistrationUnitFeeTimestamp() {
|
||||||
|
// FUTURE: we could use a separate structure to indicate fee adjustments for different transaction types
|
||||||
|
return this.nameRegistrationUnitFeeTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||||
public boolean getRequireGroupForApproval() {
|
public boolean getRequireGroupForApproval() {
|
||||||
return this.requireGroupForApproval;
|
return this.requireGroupForApproval;
|
||||||
@ -344,6 +361,10 @@ public class BlockChain {
|
|||||||
return this.minAccountLevelToMint;
|
return this.minAccountLevelToMint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMinAccountLevelForBlockSubmissions() {
|
||||||
|
return this.minAccountLevelForBlockSubmissions;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMinAccountLevelToRewardShare() {
|
public int getMinAccountLevelToRewardShare() {
|
||||||
return this.minAccountLevelToRewardShare;
|
return this.minAccountLevelToRewardShare;
|
||||||
}
|
}
|
||||||
@ -386,6 +407,10 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getTransactionV5Timestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
// More complex getters for aspects that change by height or timestamp
|
// More complex getters for aspects that change by height or timestamp
|
||||||
|
|
||||||
public long getRewardAtHeight(int ourHeight) {
|
public long getRewardAtHeight(int ourHeight) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.qortal.controller;
|
package org.qortal.controller;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@ -16,11 +18,11 @@ import org.qortal.account.PrivateKeyAccount;
|
|||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
import org.qortal.block.Block.ValidationResult;
|
import org.qortal.block.Block.ValidationResult;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.data.account.AccountData;
|
|
||||||
import org.qortal.data.account.MintingAccountData;
|
import org.qortal.data.account.MintingAccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
|
import org.qortal.data.block.CommonBlockData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
import org.qortal.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
@ -48,11 +50,6 @@ public class BlockMinter extends Thread {
|
|||||||
// Recovery
|
// Recovery
|
||||||
public static final long INVALID_BLOCK_RECOVERY_TIMEOUT = 10 * 60 * 1000L; // ms
|
public static final long INVALID_BLOCK_RECOVERY_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||||
|
|
||||||
// Min account level to submit blocks
|
|
||||||
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
|
||||||
// and exists only to reduce block candidates by default.
|
|
||||||
private static int MIN_LEVEL_FOR_BLOCK_SUBMISSION = 3;
|
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
public BlockMinter() {
|
public BlockMinter() {
|
||||||
@ -81,6 +78,10 @@ public class BlockMinter extends Thread {
|
|||||||
BlockRepository blockRepository = repository.getBlockRepository();
|
BlockRepository blockRepository = repository.getBlockRepository();
|
||||||
BlockData previousBlockData = null;
|
BlockData previousBlockData = null;
|
||||||
|
|
||||||
|
// Vars to keep track of blocks that were skipped due to chain weight
|
||||||
|
byte[] parentSignatureForLastLowWeightBlock = null;
|
||||||
|
Long timeOfLastLowWeightBlock = null;
|
||||||
|
|
||||||
List<Block> newBlocks = new ArrayList<>();
|
List<Block> newBlocks = new ArrayList<>();
|
||||||
|
|
||||||
// Flags for tracking change in whether minting is possible,
|
// Flags for tracking change in whether minting is possible,
|
||||||
@ -137,14 +138,13 @@ public class BlockMinter extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional (non-validated) prevention of block submissions below a defined level
|
// Optional (non-validated) prevention of block submissions below a defined level.
|
||||||
AccountData accountData = repository.getAccountRepository().getAccount(mintingAccount.getAddress());
|
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
||||||
if (accountData != null) {
|
// and exists only to reduce block candidates by default.
|
||||||
Integer level = accountData.getLevel();
|
int level = mintingAccount.getEffectiveMintingLevel();
|
||||||
if (level != null && level < MIN_LEVEL_FOR_BLOCK_SUBMISSION) {
|
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
||||||
madi.remove();
|
madi.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ public class BlockMinter extends Thread {
|
|||||||
|
|
||||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||||
if (Controller.getInstance().getRecoveryMode() == false)
|
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
||||||
peers.removeIf(Controller.hasNoRecentBlock);
|
peers.removeIf(Controller.hasNoRecentBlock);
|
||||||
|
|
||||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||||
@ -181,7 +181,7 @@ public class BlockMinter extends Thread {
|
|||||||
|
|
||||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||||
if (Controller.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// There are enough peers with a recent block and our latest block is recent
|
// There are enough peers with a recent block and our latest block is recent
|
||||||
@ -195,6 +195,9 @@ public class BlockMinter extends Thread {
|
|||||||
|
|
||||||
// Reduce log timeout
|
// Reduce log timeout
|
||||||
logTimeout = 10 * 1000L;
|
logTimeout = 10 * 1000L;
|
||||||
|
|
||||||
|
// Last low weight block is no longer valid
|
||||||
|
parentSignatureForLastLowWeightBlock = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard accounts we have already built blocks with
|
// Discard accounts we have already built blocks with
|
||||||
@ -211,6 +214,14 @@ public class BlockMinter extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parentSignatureForLastLowWeightBlock != null) {
|
||||||
|
// The last iteration found a higher weight block in the network, so sleep for a while
|
||||||
|
// to allow is to sync the higher weight chain. We are sleeping here rather than when
|
||||||
|
// detected as we don't want to hold the blockchain lock open.
|
||||||
|
LOGGER.info("Sleeping for 10 seconds...");
|
||||||
|
Thread.sleep(10 * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||||
// First block does the AT heavy-lifting
|
// First block does the AT heavy-lifting
|
||||||
if (newBlocks.isEmpty()) {
|
if (newBlocks.isEmpty()) {
|
||||||
@ -302,6 +313,44 @@ public class BlockMinter extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.higherWeightChainExists(repository, bestWeight)) {
|
||||||
|
|
||||||
|
// Check if the base block has updated since the last time we were here
|
||||||
|
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
|
||||||
|
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
|
||||||
|
// We've switched to a different chain, so reset the timer
|
||||||
|
timeOfLastLowWeightBlock = NTP.getTime();
|
||||||
|
}
|
||||||
|
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
|
||||||
|
|
||||||
|
// If less than 30 seconds has passed since first detection the higher weight chain,
|
||||||
|
// we should skip our block submission to give us the opportunity to sync to the better chain
|
||||||
|
if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
|
||||||
|
LOGGER.info("Higher weight chain found in peers, so not signing a block this round");
|
||||||
|
LOGGER.info("Time since detected: {}", NTP.getTime() - timeOfLastLowWeightBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// More than 30 seconds have passed, so we should submit our block candidate anyway.
|
||||||
|
LOGGER.info("More than 30 seconds passed, so proceeding to submit block candidate...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug("No higher weight chain found in peers");
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard any uncommitted changes as a result of the higher weight chain detection
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
|
// Clear variables that track low weight blocks
|
||||||
|
parentSignatureForLastLowWeightBlock = null;
|
||||||
|
timeOfLastLowWeightBlock = null;
|
||||||
|
|
||||||
|
|
||||||
// Add unconfirmed transactions
|
// Add unconfirmed transactions
|
||||||
addUnconfirmedTransactions(repository, newBlock);
|
addUnconfirmedTransactions(repository, newBlock);
|
||||||
|
|
||||||
@ -469,6 +518,61 @@ public class BlockMinter extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigInteger getOurChainWeightSinceBlock(Repository repository, BlockSummaryData commonBlock, List<BlockSummaryData> peerBlockSummaries) throws DataException {
|
||||||
|
final int commonBlockHeight = commonBlock.getHeight();
|
||||||
|
final byte[] commonBlockSig = commonBlock.getSignature();
|
||||||
|
int mutualHeight = commonBlockHeight;
|
||||||
|
|
||||||
|
// Fetch our corresponding block summaries
|
||||||
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
|
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository()
|
||||||
|
.getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||||
|
if (!ourBlockSummaries.isEmpty()) {
|
||||||
|
Synchronizer.getInstance().populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ourBlockSummaries != null && peerBlockSummaries != null) {
|
||||||
|
mutualHeight += Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
|
||||||
|
}
|
||||||
|
return Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean higherWeightChainExists(Repository repository, BigInteger blockCandidateWeight) throws DataException {
|
||||||
|
if (blockCandidateWeight == null) {
|
||||||
|
// Can't make decisions without knowing the block candidate weight
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||||
|
|
||||||
|
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||||
|
// Loop through handshaked peers and check for any new block candidates
|
||||||
|
for (Peer peer : peers) {
|
||||||
|
if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) {
|
||||||
|
// This peer has common block data
|
||||||
|
CommonBlockData commonBlockData = peer.getCommonBlockData();
|
||||||
|
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
|
||||||
|
if (commonBlockData.getChainWeight() != null) {
|
||||||
|
// The synchronizer has calculated this peer's chain weight
|
||||||
|
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
|
||||||
|
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
|
||||||
|
BigInteger peerChainWeight = commonBlockData.getChainWeight();
|
||||||
|
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
|
||||||
|
// This peer has a higher weight chain than ours
|
||||||
|
LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.debug("Peer {} has no chain weight", peer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.debug("Peer {} has no common block data", peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
private static void moderatedLog(Runnable logFunction) {
|
private static void moderatedLog(Runnable logFunction) {
|
||||||
// We only log if logging at TRACE or previous log timeout has expired
|
// We only log if logging at TRACE or previous log timeout has expired
|
||||||
if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis())
|
if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis())
|
||||||
|
@ -7,25 +7,14 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayDeque;
|
import java.util.*;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@ -51,7 +40,6 @@ import org.qortal.block.Block;
|
|||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
import org.qortal.controller.arbitrary.*;
|
import org.qortal.controller.arbitrary.*;
|
||||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
|
||||||
import org.qortal.controller.repository.PruneManager;
|
import org.qortal.controller.repository.PruneManager;
|
||||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
@ -93,14 +81,22 @@ public class Controller extends Thread {
|
|||||||
public static final String VERSION_PREFIX = "qortal-";
|
public static final String VERSION_PREFIX = "qortal-";
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
|
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
|
||||||
private static final long MISBEHAVIOUR_COOLOFF = 10 * 60 * 1000L; // ms
|
public static final long MISBEHAVIOUR_COOLOFF = 10 * 60 * 1000L; // ms
|
||||||
private static final int MAX_BLOCKCHAIN_TIP_AGE = 5; // blocks
|
private static final int MAX_BLOCKCHAIN_TIP_AGE = 5; // blocks
|
||||||
private static final Object shutdownLock = new Object();
|
private static final Object shutdownLock = new Object();
|
||||||
private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s" + File.separator + "blockchain;create=true;hsqldb.full_log_replay=true";
|
private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s" + File.separator + "blockchain;create=true;hsqldb.full_log_replay=true";
|
||||||
private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
|
private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
|
||||||
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000L; // ms
|
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000L; // ms
|
||||||
private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms
|
private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms
|
||||||
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
|
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
|
||||||
|
|
||||||
// To do with online accounts list
|
// To do with online accounts list
|
||||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||||
@ -112,7 +108,6 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
private static volatile boolean isStopping = false;
|
private static volatile boolean isStopping = false;
|
||||||
private static BlockMinter blockMinter = null;
|
private static BlockMinter blockMinter = null;
|
||||||
private static volatile boolean requestSync = false;
|
|
||||||
private static volatile boolean requestSysTrayUpdate = true;
|
private static volatile boolean requestSysTrayUpdate = true;
|
||||||
private static Controller instance;
|
private static Controller instance;
|
||||||
|
|
||||||
@ -146,20 +141,11 @@ public class Controller extends Thread {
|
|||||||
/** Whether we can mint new blocks, as reported by BlockMinter. */
|
/** Whether we can mint new blocks, as reported by BlockMinter. */
|
||||||
private volatile boolean isMintingPossible = false;
|
private volatile boolean isMintingPossible = false;
|
||||||
|
|
||||||
/** Synchronization object for sync variables below */
|
/** List of incoming transaction that are in the import queue */
|
||||||
private final Object syncLock = new Object();
|
private List<TransactionData> incomingTransactions = Collections.synchronizedList(new ArrayList<>());
|
||||||
/** Whether we are attempting to synchronize. */
|
|
||||||
private volatile boolean isSynchronizing = false;
|
|
||||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
|
||||||
private volatile int syncPercent = 0;
|
|
||||||
|
|
||||||
/** Latest block signatures from other peers that we know are on inferior chains. */
|
/** List of recent invalid unconfirmed transactions */
|
||||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
private Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
/** Recovery mode, which is used to bring back a stalled network */
|
|
||||||
private boolean recoveryMode = false;
|
|
||||||
private boolean peersAvailable = true; // peersAvailable must default to true
|
|
||||||
private long timePeersLastAvailable = 0;
|
|
||||||
|
|
||||||
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */
|
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */
|
||||||
private final ReentrantLock blockchainLock = new ReentrantLock();
|
private final ReentrantLock blockchainLock = new ReentrantLock();
|
||||||
@ -358,20 +344,6 @@ public class Controller extends Thread {
|
|||||||
return this.isMintingPossible;
|
return this.isMintingPossible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSynchronizing() {
|
|
||||||
return this.isSynchronizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getSyncPercent() {
|
|
||||||
synchronized (this.syncLock) {
|
|
||||||
return this.isSynchronizing ? this.syncPercent : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getRecoveryMode() {
|
|
||||||
return this.recoveryMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@ -476,6 +448,9 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
LOGGER.info("Starting synchronizer");
|
||||||
|
Synchronizer.getInstance().start();
|
||||||
|
|
||||||
LOGGER.info("Starting block minter");
|
LOGGER.info("Starting block minter");
|
||||||
blockMinter = new BlockMinter();
|
blockMinter = new BlockMinter();
|
||||||
blockMinter.start();
|
blockMinter.start();
|
||||||
@ -486,6 +461,7 @@ public class Controller extends Thread {
|
|||||||
// Arbitrary data controllers
|
// Arbitrary data controllers
|
||||||
LOGGER.info("Starting arbitrary-transaction controllers");
|
LOGGER.info("Starting arbitrary-transaction controllers");
|
||||||
ArbitraryDataManager.getInstance().start();
|
ArbitraryDataManager.getInstance().start();
|
||||||
|
ArbitraryDataFileManager.getInstance().start();
|
||||||
ArbitraryDataBuildManager.getInstance().start();
|
ArbitraryDataBuildManager.getInstance().start();
|
||||||
ArbitraryDataCleanupManager.getInstance().start();
|
ArbitraryDataCleanupManager.getInstance().start();
|
||||||
ArbitraryDataStorageManager.getInstance().start();
|
ArbitraryDataStorageManager.getInstance().start();
|
||||||
@ -548,7 +524,7 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Controller");
|
Thread.currentThread().setName("Qortal");
|
||||||
|
|
||||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||||
@ -588,10 +564,10 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestSync) {
|
// Process incoming transactions queue
|
||||||
requestSync = false;
|
processIncomingTransactionsQueue();
|
||||||
potentiallySynchronize();
|
// Clean up invalid incoming transactions list
|
||||||
}
|
cleanupInvalidTransactionsList(now);
|
||||||
|
|
||||||
// Clean up arbitrary data request cache
|
// Clean up arbitrary data request cache
|
||||||
ArbitraryDataManager.getInstance().cleanupRequestCache(now);
|
ArbitraryDataManager.getInstance().cleanupRequestCache(now);
|
||||||
@ -717,27 +693,6 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getRandomRepositoryMaintenanceInterval() {
|
|
||||||
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
|
||||||
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
|
||||||
if (maxInterval == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return (new Random().nextLong() % (maxInterval - minInterval)) + minInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export current trade bot states and minting accounts.
|
|
||||||
*/
|
|
||||||
public void exportRepositoryData() {
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
repository.exportNodeLocalData();
|
|
||||||
|
|
||||||
} catch (DataException e) {
|
|
||||||
// Fail silently as this is an optional step
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Predicate<Peer> hasMisbehaved = peer -> {
|
public static final Predicate<Peer> hasMisbehaved = peer -> {
|
||||||
final Long lastMisbehaved = peer.getPeerData().getLastMisbehaved();
|
final Long lastMisbehaved = peer.getPeerData().getLastMisbehaved();
|
||||||
return lastMisbehaved != null && lastMisbehaved > NTP.getTime() - MISBEHAVIOUR_COOLOFF;
|
return lastMisbehaved != null && lastMisbehaved > NTP.getTime() - MISBEHAVIOUR_COOLOFF;
|
||||||
@ -762,7 +717,7 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||||
final List<ByteArray> inferiorChainTips = getInstance().inferiorChainSignatures;
|
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
|
||||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(new ByteArray(peerChainTipData.getLastBlockSignature()));
|
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(new ByteArray(peerChainTipData.getLastBlockSignature()));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -771,218 +726,34 @@ public class Controller extends Thread {
|
|||||||
return peer.isAtLeastVersion(minPeerVersion) == false;
|
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||||
};
|
};
|
||||||
|
|
||||||
private void potentiallySynchronize() throws InterruptedException {
|
private long getRandomRepositoryMaintenanceInterval() {
|
||||||
// Already synchronizing via another thread?
|
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
||||||
if (this.isSynchronizing)
|
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
||||||
return;
|
if (maxInterval == 0) {
|
||||||
|
return 0;
|
||||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
|
||||||
|
|
||||||
// Disregard peers that have "misbehaved" recently
|
|
||||||
peers.removeIf(hasMisbehaved);
|
|
||||||
|
|
||||||
// Disregard peers that only have genesis block
|
|
||||||
peers.removeIf(hasOnlyGenesisBlock);
|
|
||||||
|
|
||||||
// Disregard peers that don't have a recent block
|
|
||||||
peers.removeIf(hasNoRecentBlock);
|
|
||||||
|
|
||||||
// Disregard peers that are on an old version
|
|
||||||
peers.removeIf(hasOldVersion);
|
|
||||||
|
|
||||||
checkRecoveryModeForPeers(peers);
|
|
||||||
if (recoveryMode) {
|
|
||||||
peers = Network.getInstance().getHandshakedPeers();
|
|
||||||
peers.removeIf(hasOnlyGenesisBlock);
|
|
||||||
peers.removeIf(hasMisbehaved);
|
|
||||||
peers.removeIf(hasOldVersion);
|
|
||||||
}
|
}
|
||||||
|
return (new Random().nextLong() % (maxInterval - minInterval)) + minInterval;
|
||||||
// Check we have enough peers to potentially synchronize
|
|
||||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Disregard peers that have no block signature or the same block signature as us
|
|
||||||
peers.removeIf(hasNoOrSameBlock);
|
|
||||||
|
|
||||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
|
||||||
peers.removeIf(hasInferiorChainTip);
|
|
||||||
|
|
||||||
final int peersBeforeComparison = peers.size();
|
|
||||||
|
|
||||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
|
||||||
Synchronizer.getInstance().findCommonBlocksWithPeers(peers);
|
|
||||||
|
|
||||||
// Compare the peers against each other, and against our chain, which will return an updated list excluding those without common blocks
|
|
||||||
peers = Synchronizer.getInstance().comparePeers(peers);
|
|
||||||
|
|
||||||
// We may have added more inferior chain tips when comparing peers, so remove any peers that are currently on those chains
|
|
||||||
peers.removeIf(hasInferiorChainTip);
|
|
||||||
|
|
||||||
final int peersRemoved = peersBeforeComparison - peers.size();
|
|
||||||
if (peersRemoved > 0 && peers.size() > 0)
|
|
||||||
LOGGER.debug(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
|
||||||
|
|
||||||
if (peers.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (peers.size() > 1) {
|
|
||||||
StringBuilder finalPeersString = new StringBuilder();
|
|
||||||
for (Peer peer : peers)
|
|
||||||
finalPeersString = finalPeersString.length() > 0 ? finalPeersString.append(", ").append(peer) : finalPeersString.append(peer);
|
|
||||||
LOGGER.debug(String.format("Choosing random peer from: [%s]", finalPeersString.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick random peer to sync with
|
|
||||||
int index = new SecureRandom().nextInt(peers.size());
|
|
||||||
Peer peer = peers.get(index);
|
|
||||||
|
|
||||||
actuallySynchronize(peer, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
/**
|
||||||
boolean hasStatusChanged = false;
|
* Export current trade bot states and minting accounts.
|
||||||
BlockData priorChainTip = this.getChainTip();
|
*/
|
||||||
|
public void exportRepositoryData() {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
repository.exportNodeLocalData();
|
||||||
|
|
||||||
synchronized (this.syncLock) {
|
} catch (DataException e) {
|
||||||
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
// Fail silently as this is an optional step
|
||||||
|
|
||||||
// Only update SysTray if we're potentially changing height
|
|
||||||
if (this.syncPercent < 100) {
|
|
||||||
this.isSynchronizing = true;
|
|
||||||
hasStatusChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
peer.setSyncInProgress(true);
|
|
||||||
|
|
||||||
if (hasStatusChanged)
|
|
||||||
updateSysTray();
|
|
||||||
|
|
||||||
try {
|
|
||||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
|
||||||
switch (syncResult) {
|
|
||||||
case GENESIS_ONLY:
|
|
||||||
case NO_COMMON_BLOCK:
|
|
||||||
case TOO_DIVERGENT:
|
|
||||||
case INVALID_DATA: {
|
|
||||||
// These are more serious results that warrant a cool-off
|
|
||||||
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
|
||||||
|
|
||||||
// Don't use this peer again for a while
|
|
||||||
Network.getInstance().peerMisbehaved(peer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case INFERIOR_CHAIN: {
|
|
||||||
// Update our list of inferior chain tips
|
|
||||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
|
||||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
|
||||||
inferiorChainSignatures.add(inferiorChainSignature);
|
|
||||||
|
|
||||||
// These are minor failure results so fine to try again
|
|
||||||
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
|
||||||
|
|
||||||
// Notify peer of our superior chain
|
|
||||||
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
|
|
||||||
peer.disconnect("failed to notify peer of our superior chain");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case NO_REPLY:
|
|
||||||
case NO_BLOCKCHAIN_LOCK:
|
|
||||||
case REPOSITORY_ISSUE:
|
|
||||||
// These are minor failure results so fine to try again
|
|
||||||
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SHUTTING_DOWN:
|
|
||||||
// Just quietly exit
|
|
||||||
break;
|
|
||||||
|
|
||||||
case OK:
|
|
||||||
// fall-through...
|
|
||||||
case NOTHING_TO_DO: {
|
|
||||||
// Update our list of inferior chain tips
|
|
||||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
|
||||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
|
||||||
inferiorChainSignatures.add(inferiorChainSignature);
|
|
||||||
|
|
||||||
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has our chain tip changed?
|
|
||||||
BlockData newChainTip;
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
newChainTip = repository.getBlockRepository().getLastBlock();
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.warn(String.format("Repository issue when trying to fetch post-synchronization chain tip: %s", e.getMessage()));
|
|
||||||
return syncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Arrays.equals(newChainTip.getSignature(), priorChainTip.getSignature())) {
|
|
||||||
// Reset our cache of inferior chains
|
|
||||||
inferiorChainSignatures.clear();
|
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
|
||||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncResult;
|
|
||||||
} finally {
|
|
||||||
isSynchronizing = false;
|
|
||||||
peer.setSyncInProgress(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkRecoveryModeForPeers(List<Peer> qualifiedPeers) {
|
|
||||||
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
|
||||||
|
|
||||||
if (handshakedPeers.size() > 0) {
|
|
||||||
// There is at least one handshaked peer
|
|
||||||
if (qualifiedPeers.isEmpty()) {
|
|
||||||
// There are no 'qualified' peers - i.e. peers that have a recent block we can sync to
|
|
||||||
boolean werePeersAvailable = peersAvailable;
|
|
||||||
peersAvailable = false;
|
|
||||||
|
|
||||||
// If peers only just became unavailable, update our record of the time they were last available
|
|
||||||
if (werePeersAvailable)
|
|
||||||
timePeersLastAvailable = NTP.getTime();
|
|
||||||
|
|
||||||
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
|
||||||
if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
|
|
||||||
if (recoveryMode == false) {
|
|
||||||
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
|
|
||||||
recoveryMode = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We now have at least one peer with a recent block, so we can exit recovery mode and sync normally
|
|
||||||
peersAvailable = true;
|
|
||||||
if (recoveryMode) {
|
|
||||||
LOGGER.info("Peers have become available again. Exiting recovery mode...");
|
|
||||||
recoveryMode = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return recoveryMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addInferiorChainSignature(byte[] inferiorSignature) {
|
|
||||||
// Update our list of inferior chain tips
|
|
||||||
ByteArray inferiorChainSignature = new ByteArray(inferiorSignature);
|
|
||||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
|
||||||
inferiorChainSignatures.add(inferiorChainSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StatusChangeEvent implements Event {
|
public static class StatusChangeEvent implements Event {
|
||||||
public StatusChangeEvent() {
|
public StatusChangeEvent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSysTray() {
|
public void updateSysTray() {
|
||||||
if (NTP.getTime() == null) {
|
if (NTP.getTime() == null) {
|
||||||
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
|
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
|
||||||
SysTray.getInstance().setTrayIcon(1);
|
SysTray.getInstance().setTrayIcon(1);
|
||||||
@ -998,13 +769,13 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
String actionText;
|
String actionText;
|
||||||
|
|
||||||
synchronized (this.syncLock) {
|
synchronized (Synchronizer.getInstance().syncLock) {
|
||||||
if (this.isMintingPossible) {
|
if (this.isMintingPossible) {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||||
SysTray.getInstance().setTrayIcon(2);
|
SysTray.getInstance().setTrayIcon(2);
|
||||||
}
|
}
|
||||||
else if (this.isSynchronizing) {
|
else if (Synchronizer.getInstance().isSynchronizing()) {
|
||||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), this.syncPercent);
|
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
|
||||||
SysTray.getInstance().setTrayIcon(3);
|
SysTray.getInstance().setTrayIcon(3);
|
||||||
}
|
}
|
||||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
|
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
|
||||||
@ -1060,6 +831,121 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incoming transactions queue
|
||||||
|
|
||||||
|
private boolean incomingTransactionQueueContains(byte[] signature) {
|
||||||
|
synchronized (incomingTransactions) {
|
||||||
|
return incomingTransactions.stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeIncomingTransaction(byte[] signature) {
|
||||||
|
incomingTransactions.removeIf(t -> Arrays.equals(t.getSignature(), signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processIncomingTransactionsQueue() {
|
||||||
|
if (this.incomingTransactions.size() == 0) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
|
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.trace(() -> String.format("Too busy to process incoming transactions queue"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOGGER.info("Interrupted when trying to acquire blockchain lock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
LOGGER.debug("Processing incoming transactions queue (size {})...", this.incomingTransactions.size());
|
||||||
|
|
||||||
|
// Take a copy of incomingTransactions so we can release the lock
|
||||||
|
List<TransactionData>incomingTransactionsCopy = new ArrayList<>(this.incomingTransactions);
|
||||||
|
|
||||||
|
// Iterate through incoming transactions list
|
||||||
|
Iterator iterator = incomingTransactionsCopy.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Synchronizer.getInstance().isSyncRequestPending()) {
|
||||||
|
LOGGER.debug("Breaking out of transaction processing loop with {} remaining, because a sync request is pending", incomingTransactionsCopy.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionData transactionData = (TransactionData) iterator.next();
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
|
||||||
|
// Check signature
|
||||||
|
if (!transaction.isSignatureValid()) {
|
||||||
|
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||||
|
removeIncomingTransaction(transactionData.getSignature());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) {
|
||||||
|
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature())));
|
||||||
|
removeIncomingTransaction(transactionData.getSignature());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) {
|
||||||
|
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction", Base58.encode(transactionData.getSignature())));
|
||||||
|
removeIncomingTransaction(transactionData.getSignature());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationResult != ValidationResult.OK) {
|
||||||
|
final String signature58 = Base58.encode(transactionData.getSignature());
|
||||||
|
LOGGER.trace(() -> 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 == ValidationResult.TIMESTAMP_TOO_OLD) {
|
||||||
|
// Use shorter recheck interval for expired transactions
|
||||||
|
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL;
|
||||||
|
}
|
||||||
|
Long expiry = now + expiryLength;
|
||||||
|
LOGGER.debug("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);
|
||||||
|
}
|
||||||
|
removeIncomingTransaction(transactionData.getSignature());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||||
|
removeIncomingTransaction(transactionData.getSignature());
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while processing incoming transactions", e));
|
||||||
|
} finally {
|
||||||
|
LOGGER.debug("Finished processing incoming transactions queue");
|
||||||
|
blockchainLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Shutdown
|
// Shutdown
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
@ -1067,6 +953,9 @@ public class Controller extends Thread {
|
|||||||
if (!isStopping) {
|
if (!isStopping) {
|
||||||
isStopping = true;
|
isStopping = true;
|
||||||
|
|
||||||
|
LOGGER.info("Shutting down synchronizer");
|
||||||
|
Synchronizer.getInstance().shutdown();
|
||||||
|
|
||||||
LOGGER.info("Shutting down API");
|
LOGGER.info("Shutting down API");
|
||||||
ApiService.getInstance().stop();
|
ApiService.getInstance().stop();
|
||||||
|
|
||||||
@ -1078,6 +967,7 @@ public class Controller extends Thread {
|
|||||||
// Arbitrary data controllers
|
// Arbitrary data controllers
|
||||||
LOGGER.info("Shutting down arbitrary-transaction controllers");
|
LOGGER.info("Shutting down arbitrary-transaction controllers");
|
||||||
ArbitraryDataManager.getInstance().shutdown();
|
ArbitraryDataManager.getInstance().shutdown();
|
||||||
|
ArbitraryDataFileManager.getInstance().shutdown();
|
||||||
ArbitraryDataBuildManager.getInstance().shutdown();
|
ArbitraryDataBuildManager.getInstance().shutdown();
|
||||||
ArbitraryDataCleanupManager.getInstance().shutdown();
|
ArbitraryDataCleanupManager.getInstance().shutdown();
|
||||||
ArbitraryDataStorageManager.getInstance().shutdown();
|
ArbitraryDataStorageManager.getInstance().shutdown();
|
||||||
@ -1108,6 +998,17 @@ public class Controller extends Thread {
|
|||||||
// We were interrupted while waiting for thread to join
|
// We were interrupted while waiting for thread to join
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure we're the only thread modifying the blockchain when shutting down the repository
|
||||||
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
|
try {
|
||||||
|
if (!blockchainLock.tryLock(5, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 5 seconds");
|
||||||
|
// Proceed anyway, as we have to shut down
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOGGER.info("Interrupted when waiting for blockchain lock");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.info("Shutting down repository");
|
LOGGER.info("Shutting down repository");
|
||||||
RepositoryManager.closeRepositoryFactory();
|
RepositoryManager.closeRepositoryFactory();
|
||||||
@ -1115,6 +1016,11 @@ public class Controller extends Thread {
|
|||||||
LOGGER.error("Error occurred while shutting down repository", e);
|
LOGGER.error("Error occurred while shutting down repository", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release the lock if we acquired it
|
||||||
|
if (blockchainLock.isHeldByCurrentThread()) {
|
||||||
|
blockchainLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.info("Shutting down NTP");
|
LOGGER.info("Shutting down NTP");
|
||||||
NTP.shutdownNow();
|
NTP.shutdownNow();
|
||||||
|
|
||||||
@ -1514,50 +1420,10 @@ public class Controller extends Thread {
|
|||||||
private void onNetworkTransactionMessage(Peer peer, Message message) {
|
private void onNetworkTransactionMessage(Peer peer, Message message) {
|
||||||
TransactionMessage transactionMessage = (TransactionMessage) message;
|
TransactionMessage transactionMessage = (TransactionMessage) message;
|
||||||
TransactionData transactionData = transactionMessage.getTransactionData();
|
TransactionData transactionData = transactionMessage.getTransactionData();
|
||||||
|
if (this.incomingTransactions.size() < MAX_INCOMING_TRANSACTIONS) {
|
||||||
/*
|
if (!this.incomingTransactions.contains(transactionData)) {
|
||||||
* If we can't obtain blockchain lock immediately,
|
this.incomingTransactions.add(transactionData);
|
||||||
* e.g. Synchronizer is active, or another transaction is taking a while to validate,
|
|
||||||
* then we're using up a network thread for ages and clogging things up
|
|
||||||
* so bail out early
|
|
||||||
*/
|
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
|
||||||
if (!blockchainLock.tryLock()) {
|
|
||||||
LOGGER.trace(() -> String.format("Too busy to import %s transaction %s from peer %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
|
||||||
|
|
||||||
// Check signature
|
|
||||||
if (!transaction.isSignatureValid()) {
|
|
||||||
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature from peer %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidationResult validationResult = transaction.importAsUnconfirmed();
|
|
||||||
|
|
||||||
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) {
|
|
||||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) {
|
|
||||||
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationResult != ValidationResult.OK) {
|
|
||||||
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s from peer %s", validationResult.name(), transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.debug(() -> String.format("Imported %s transaction %s from peer %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.error(String.format("Repository issue while processing transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer), e);
|
|
||||||
} finally {
|
|
||||||
blockchainLock.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1709,7 +1575,7 @@ public class Controller extends Thread {
|
|||||||
peer.setChainTipData(newChainTipData);
|
peer.setChainTipData(newChainTipData);
|
||||||
|
|
||||||
// Potentially synchronize
|
// Potentially synchronize
|
||||||
requestSync = true;
|
Synchronizer.getInstance().requestSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
private void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
||||||
@ -1756,6 +1622,19 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
for (byte[] signature : signatures) {
|
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)
|
// Do we have it already? (Before requesting transaction data itself)
|
||||||
if (repository.getTransactionRepository().exists(signature)) {
|
if (repository.getTransactionRepository().exists(signature)) {
|
||||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
|
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
|
||||||
@ -1949,88 +1828,94 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
private void sendOurOnlineAccountsInfo() {
|
private void sendOurOnlineAccountsInfo() {
|
||||||
final Long now = NTP.getTime();
|
final Long now = NTP.getTime();
|
||||||
if (now == null)
|
if (now != null) {
|
||||||
return;
|
|
||||||
|
|
||||||
List<MintingAccountData> mintingAccounts;
|
List<MintingAccountData> mintingAccounts;
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||||
|
|
||||||
// We have no accounts, but don't reset timestamp
|
// We have no accounts, but don't reset timestamp
|
||||||
if (mintingAccounts.isEmpty())
|
if (mintingAccounts.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Only reward-share accounts allowed
|
// Only reward-share accounts allowed
|
||||||
Iterator<MintingAccountData> iterator = mintingAccounts.iterator();
|
Iterator<MintingAccountData> iterator = mintingAccounts.iterator();
|
||||||
while (iterator.hasNext()) {
|
int i = 0;
|
||||||
MintingAccountData mintingAccountData = iterator.next();
|
|
||||||
|
|
||||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
|
||||||
if (rewardShareData == null) {
|
|
||||||
// Reward-share doesn't even exist - probably not a good sign
|
|
||||||
iterator.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
|
||||||
if (!mintingAccount.canMint()) {
|
|
||||||
// Minting-account component of reward-share can no longer mint - disregard
|
|
||||||
iterator.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'current' timestamp
|
|
||||||
final long onlineAccountsTimestamp = Controller.toOnlineAccountTimestamp(now);
|
|
||||||
boolean hasInfoChanged = false;
|
|
||||||
|
|
||||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
|
||||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
|
||||||
|
|
||||||
MINTING_ACCOUNTS:
|
|
||||||
for (MintingAccountData mintingAccountData : mintingAccounts) {
|
|
||||||
PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey());
|
|
||||||
|
|
||||||
byte[] signature = mintingAccount.sign(timestampBytes);
|
|
||||||
byte[] publicKey = mintingAccount.getPublicKey();
|
|
||||||
|
|
||||||
// Our account is online
|
|
||||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
|
||||||
synchronized (this.onlineAccounts) {
|
|
||||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
OnlineAccountData existingOnlineAccountData = iterator.next();
|
MintingAccountData mintingAccountData = iterator.next();
|
||||||
|
|
||||||
if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) {
|
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||||
// If our online account is already present, with same timestamp, then move on to next mintingAccount
|
if (rewardShareData == null) {
|
||||||
if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp)
|
// Reward-share doesn't even exist - probably not a good sign
|
||||||
continue MINTING_ACCOUNTS;
|
|
||||||
|
|
||||||
// If our online account is already present, but with older timestamp, then remove it
|
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
break;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||||
|
if (!mintingAccount.canMint()) {
|
||||||
|
// Minting-account component of reward-share can no longer mint - disregard
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++i > 2) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
this.onlineAccounts.add(ourOnlineAccountData);
|
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp));
|
// 'current' timestamp
|
||||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
final long onlineAccountsTimestamp = Controller.toOnlineAccountTimestamp(now);
|
||||||
hasInfoChanged = true;
|
boolean hasInfoChanged = false;
|
||||||
|
|
||||||
|
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||||
|
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||||
|
|
||||||
|
MINTING_ACCOUNTS:
|
||||||
|
for (MintingAccountData mintingAccountData : mintingAccounts) {
|
||||||
|
PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey());
|
||||||
|
|
||||||
|
byte[] signature = mintingAccount.sign(timestampBytes);
|
||||||
|
byte[] publicKey = mintingAccount.getPublicKey();
|
||||||
|
|
||||||
|
// Our account is online
|
||||||
|
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||||
|
synchronized (this.onlineAccounts) {
|
||||||
|
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
OnlineAccountData existingOnlineAccountData = iterator.next();
|
||||||
|
|
||||||
|
if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) {
|
||||||
|
// If our online account is already present, with same timestamp, then move on to next mintingAccount
|
||||||
|
if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp)
|
||||||
|
continue MINTING_ACCOUNTS;
|
||||||
|
|
||||||
|
// If our online account is already present, but with older timestamp, then remove it
|
||||||
|
iterator.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onlineAccounts.add(ourOnlineAccountData);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp));
|
||||||
|
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||||
|
hasInfoChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasInfoChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Message message = new OnlineAccountsMessage(ourOnlineAccounts);
|
||||||
|
Network.getInstance().broadcast(peer -> message);
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInfoChanged)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Message message = new OnlineAccountsMessage(ourOnlineAccounts);
|
|
||||||
Network.getInstance().broadcast(peer -> message);
|
|
||||||
|
|
||||||
LOGGER.trace(()-> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long toOnlineAccountTimestamp(long timestamp) {
|
public static long toOnlineAccountTimestamp(long timestamp) {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package org.qortal.controller;
|
package org.qortal.controller;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -20,6 +22,7 @@ import org.qortal.data.block.CommonBlockData;
|
|||||||
import org.qortal.data.network.PeerChainTipData;
|
import org.qortal.data.network.PeerChainTipData;
|
||||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.network.Network;
|
||||||
import org.qortal.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
import org.qortal.network.message.BlockMessage;
|
import org.qortal.network.message.BlockMessage;
|
||||||
import org.qortal.network.message.BlockSummariesMessage;
|
import org.qortal.network.message.BlockSummariesMessage;
|
||||||
@ -35,11 +38,10 @@ import org.qortal.repository.RepositoryManager;
|
|||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.ByteArray;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import static org.qortal.network.Peer.FETCH_BLOCKS_TIMEOUT;
|
public class Synchronizer extends Thread {
|
||||||
|
|
||||||
public class Synchronizer {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class);
|
private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class);
|
||||||
|
|
||||||
@ -57,12 +59,32 @@ public class Synchronizer {
|
|||||||
/** Maximum number of block signatures we ask from peer in one go */
|
/** Maximum number of block signatures we ask from peer in one go */
|
||||||
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
||||||
|
|
||||||
|
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||||
|
|
||||||
|
|
||||||
|
private boolean running;
|
||||||
|
|
||||||
|
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||||
|
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
||||||
|
|
||||||
|
/** Recovery mode, which is used to bring back a stalled network */
|
||||||
|
private boolean recoveryMode = false;
|
||||||
|
private boolean peersAvailable = true; // peersAvailable must default to true
|
||||||
|
private long timePeersLastAvailable = 0;
|
||||||
|
|
||||||
// Keep track of the size of the last re-org, so it can be logged
|
// Keep track of the size of the last re-org, so it can be logged
|
||||||
private int lastReorgSize;
|
private int lastReorgSize;
|
||||||
|
|
||||||
|
/** Synchronization object for sync variables below */
|
||||||
|
public final Object syncLock = new Object();
|
||||||
|
/** Whether we are attempting to synchronize. */
|
||||||
|
private volatile boolean isSynchronizing = false;
|
||||||
|
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||||
|
private volatile int syncPercent = 0;
|
||||||
|
|
||||||
|
private static volatile boolean requestSync = false;
|
||||||
|
private boolean syncRequestPending = false;
|
||||||
|
|
||||||
// Keep track of invalid blocks so that we don't keep trying to sync them
|
// Keep track of invalid blocks so that we don't keep trying to sync them
|
||||||
private Map<String, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
|
private Map<String, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
|
||||||
public Long timeValidBlockLastReceived = null;
|
public Long timeValidBlockLastReceived = null;
|
||||||
@ -77,6 +99,7 @@ public class Synchronizer {
|
|||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
private Synchronizer() {
|
private Synchronizer() {
|
||||||
|
this.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Synchronizer getInstance() {
|
public static Synchronizer getInstance() {
|
||||||
@ -87,6 +110,284 @@ public class Synchronizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Synchronizer");
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (running && !Controller.isStopping()) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
if (requestSync) {
|
||||||
|
requestSync = false;
|
||||||
|
boolean success = Synchronizer.getInstance().potentiallySynchronize();
|
||||||
|
if (!success) {
|
||||||
|
// Something went wrong, so try again next time
|
||||||
|
requestSync = true;
|
||||||
|
}
|
||||||
|
// Remember that we have a pending sync request if this attempt failed
|
||||||
|
syncRequestPending = !success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Clear interrupted flag so we can shutdown trim threads
|
||||||
|
Thread.interrupted();
|
||||||
|
// Fall-through to exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
this.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isSynchronizing() {
|
||||||
|
return this.isSynchronizing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSyncRequestPending() {
|
||||||
|
return this.syncRequestPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSyncPercent() {
|
||||||
|
synchronized (this.syncLock) {
|
||||||
|
return this.isSynchronizing ? this.syncPercent : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestSync() {
|
||||||
|
requestSync = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSyncRequested() {
|
||||||
|
return requestSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getRecoveryMode() {
|
||||||
|
return this.recoveryMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean potentiallySynchronize() throws InterruptedException {
|
||||||
|
// Already synchronizing via another thread?
|
||||||
|
if (this.isSynchronizing)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||||
|
|
||||||
|
// Disregard peers that have "misbehaved" recently
|
||||||
|
peers.removeIf(Controller.hasMisbehaved);
|
||||||
|
|
||||||
|
// Disregard peers that only have genesis block
|
||||||
|
peers.removeIf(Controller.hasOnlyGenesisBlock);
|
||||||
|
|
||||||
|
// Disregard peers that don't have a recent block
|
||||||
|
peers.removeIf(Controller.hasNoRecentBlock);
|
||||||
|
|
||||||
|
// Disregard peers that are on an old version
|
||||||
|
peers.removeIf(Controller.hasOldVersion);
|
||||||
|
|
||||||
|
checkRecoveryModeForPeers(peers);
|
||||||
|
if (recoveryMode) {
|
||||||
|
peers = Network.getInstance().getHandshakedPeers();
|
||||||
|
peers.removeIf(Controller.hasOnlyGenesisBlock);
|
||||||
|
peers.removeIf(Controller.hasMisbehaved);
|
||||||
|
peers.removeIf(Controller.hasOldVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we have enough peers to potentially synchronize
|
||||||
|
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Disregard peers that have no block signature or the same block signature as us
|
||||||
|
peers.removeIf(Controller.hasNoOrSameBlock);
|
||||||
|
|
||||||
|
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||||
|
peers.removeIf(Controller.hasInferiorChainTip);
|
||||||
|
|
||||||
|
final int peersBeforeComparison = peers.size();
|
||||||
|
|
||||||
|
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||||
|
Synchronizer.getInstance().findCommonBlocksWithPeers(peers);
|
||||||
|
|
||||||
|
// Compare the peers against each other, and against our chain, which will return an updated list excluding those without common blocks
|
||||||
|
peers = Synchronizer.getInstance().comparePeers(peers);
|
||||||
|
|
||||||
|
// We may have added more inferior chain tips when comparing peers, so remove any peers that are currently on those chains
|
||||||
|
peers.removeIf(Controller.hasInferiorChainTip);
|
||||||
|
|
||||||
|
final int peersRemoved = peersBeforeComparison - peers.size();
|
||||||
|
if (peersRemoved > 0 && peers.size() > 0)
|
||||||
|
LOGGER.debug(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
||||||
|
|
||||||
|
if (peers.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (peers.size() > 1) {
|
||||||
|
StringBuilder finalPeersString = new StringBuilder();
|
||||||
|
for (Peer peer : peers)
|
||||||
|
finalPeersString = finalPeersString.length() > 0 ? finalPeersString.append(", ").append(peer) : finalPeersString.append(peer);
|
||||||
|
LOGGER.debug(String.format("Choosing random peer from: [%s]", finalPeersString.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick random peer to sync with
|
||||||
|
int index = new SecureRandom().nextInt(peers.size());
|
||||||
|
Peer peer = peers.get(index);
|
||||||
|
|
||||||
|
SynchronizationResult syncResult = actuallySynchronize(peer, false);
|
||||||
|
if (syncResult == SynchronizationResult.NO_BLOCKCHAIN_LOCK) {
|
||||||
|
// No blockchain lock - force a retry by returning false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||||
|
boolean hasStatusChanged = false;
|
||||||
|
BlockData priorChainTip = Controller.getInstance().getChainTip();
|
||||||
|
|
||||||
|
synchronized (this.syncLock) {
|
||||||
|
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||||
|
|
||||||
|
// Only update SysTray if we're potentially changing height
|
||||||
|
if (this.syncPercent < 100) {
|
||||||
|
this.isSynchronizing = true;
|
||||||
|
hasStatusChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peer.setSyncInProgress(true);
|
||||||
|
|
||||||
|
if (hasStatusChanged)
|
||||||
|
Controller.getInstance().updateSysTray();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||||
|
switch (syncResult) {
|
||||||
|
case GENESIS_ONLY:
|
||||||
|
case NO_COMMON_BLOCK:
|
||||||
|
case TOO_DIVERGENT:
|
||||||
|
case INVALID_DATA: {
|
||||||
|
// These are more serious results that warrant a cool-off
|
||||||
|
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
||||||
|
|
||||||
|
// Don't use this peer again for a while
|
||||||
|
Network.getInstance().peerMisbehaved(peer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case INFERIOR_CHAIN: {
|
||||||
|
// Update our list of inferior chain tips
|
||||||
|
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||||
|
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||||
|
inferiorChainSignatures.add(inferiorChainSignature);
|
||||||
|
|
||||||
|
// These are minor failure results so fine to try again
|
||||||
|
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||||
|
|
||||||
|
// Notify peer of our superior chain
|
||||||
|
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
|
||||||
|
peer.disconnect("failed to notify peer of our superior chain");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case NO_REPLY:
|
||||||
|
case NO_BLOCKCHAIN_LOCK:
|
||||||
|
case REPOSITORY_ISSUE:
|
||||||
|
// These are minor failure results so fine to try again
|
||||||
|
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SHUTTING_DOWN:
|
||||||
|
// Just quietly exit
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OK:
|
||||||
|
// fall-through...
|
||||||
|
case NOTHING_TO_DO: {
|
||||||
|
// Update our list of inferior chain tips
|
||||||
|
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||||
|
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||||
|
inferiorChainSignatures.add(inferiorChainSignature);
|
||||||
|
|
||||||
|
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!running) {
|
||||||
|
// We've stopped
|
||||||
|
return SynchronizationResult.SHUTTING_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has our chain tip changed?
|
||||||
|
BlockData newChainTip;
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
newChainTip = repository.getBlockRepository().getLastBlock();
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.warn(String.format("Repository issue when trying to fetch post-synchronization chain tip: %s", e.getMessage()));
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(newChainTip.getSignature(), priorChainTip.getSignature())) {
|
||||||
|
// Reset our cache of inferior chains
|
||||||
|
inferiorChainSignatures.clear();
|
||||||
|
|
||||||
|
Network network = Network.getInstance();
|
||||||
|
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
} finally {
|
||||||
|
this.isSynchronizing = false;
|
||||||
|
peer.setSyncInProgress(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkRecoveryModeForPeers(List<Peer> qualifiedPeers) {
|
||||||
|
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
||||||
|
|
||||||
|
if (handshakedPeers.size() > 0) {
|
||||||
|
// There is at least one handshaked peer
|
||||||
|
if (qualifiedPeers.isEmpty()) {
|
||||||
|
// There are no 'qualified' peers - i.e. peers that have a recent block we can sync to
|
||||||
|
boolean werePeersAvailable = peersAvailable;
|
||||||
|
peersAvailable = false;
|
||||||
|
|
||||||
|
// If peers only just became unavailable, update our record of the time they were last available
|
||||||
|
if (werePeersAvailable)
|
||||||
|
timePeersLastAvailable = NTP.getTime();
|
||||||
|
|
||||||
|
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
||||||
|
if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
|
||||||
|
if (recoveryMode == false) {
|
||||||
|
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
|
||||||
|
recoveryMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We now have at least one peer with a recent block, so we can exit recovery mode and sync normally
|
||||||
|
peersAvailable = true;
|
||||||
|
if (recoveryMode) {
|
||||||
|
LOGGER.info("Peers have become available again. Exiting recovery mode...");
|
||||||
|
recoveryMode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recoveryMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInferiorChainSignature(byte[] inferiorSignature) {
|
||||||
|
// Update our list of inferior chain tips
|
||||||
|
ByteArray inferiorChainSignature = new ByteArray(inferiorSignature);
|
||||||
|
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||||
|
inferiorChainSignatures.add(inferiorChainSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterate through a list of supplied peers, and attempt to find our common block with each.
|
* Iterate through a list of supplied peers, and attempt to find our common block with each.
|
||||||
* If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later.
|
* If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later.
|
||||||
@ -259,6 +560,8 @@ public class Synchronizer {
|
|||||||
// Create a placeholder to track of common blocks that we can discard due to being inferior chains
|
// Create a placeholder to track of common blocks that we can discard due to being inferior chains
|
||||||
int dropPeersAfterCommonBlockHeight = 0;
|
int dropPeersAfterCommonBlockHeight = 0;
|
||||||
|
|
||||||
|
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||||
|
|
||||||
// Remove peers with no common block data
|
// Remove peers with no common block data
|
||||||
Iterator iterator = peers.iterator();
|
Iterator iterator = peers.iterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
@ -279,7 +582,7 @@ public class Synchronizer {
|
|||||||
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
|
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
|
||||||
for (Peer peer : peersSharingCommonBlock) {
|
for (Peer peer : peersSharingCommonBlock) {
|
||||||
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
|
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
|
||||||
Controller.getInstance().addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -381,9 +684,7 @@ public class Synchronizer {
|
|||||||
if (ourBlockSummaries.size() > 0)
|
if (ourBlockSummaries.size() > 0)
|
||||||
ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, maxHeightForChainWeightComparisons);
|
ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, maxHeightForChainWeightComparisons);
|
||||||
|
|
||||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), accurateFormatter.format(ourChainWeight)));
|
||||||
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
|
||||||
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), formatter.format(ourChainWeight)));
|
|
||||||
|
|
||||||
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
||||||
for (Peer peer : peersSharingCommonBlock) {
|
for (Peer peer : peersSharingCommonBlock) {
|
||||||
@ -405,7 +706,7 @@ public class Synchronizer {
|
|||||||
LOGGER.debug(String.format("About to calculate chain weight based on %d blocks for peer %s with common block %.8s (peer has %d blocks after common block)", (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peer, Base58.encode(commonBlockSummary.getSignature()), peerAdditionalBlocksAfterCommonBlock));
|
LOGGER.debug(String.format("About to calculate chain weight based on %d blocks for peer %s with common block %.8s (peer has %d blocks after common block)", (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peer, Base58.encode(commonBlockSummary.getSignature()), peerAdditionalBlocksAfterCommonBlock));
|
||||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), peerBlockSummariesAfterCommonBlock, maxHeightForChainWeightComparisons);
|
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), peerBlockSummariesAfterCommonBlock, maxHeightForChainWeightComparisons);
|
||||||
peer.getCommonBlockData().setChainWeight(peerChainWeight);
|
peer.getCommonBlockData().setChainWeight(peerChainWeight);
|
||||||
LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), formatter.format(peerChainWeight)));
|
LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), accurateFormatter.format(peerChainWeight)));
|
||||||
|
|
||||||
// Compare against our chain - if our blockchain has greater weight then don't synchronize with peer (or any others in this group)
|
// Compare against our chain - if our blockchain has greater weight then don't synchronize with peer (or any others in this group)
|
||||||
if (ourChainWeight.compareTo(peerChainWeight) > 0) {
|
if (ourChainWeight.compareTo(peerChainWeight) > 0) {
|
||||||
@ -571,9 +872,11 @@ public class Synchronizer {
|
|||||||
// Make sure we're the only thread modifying the blockchain
|
// Make sure we're the only thread modifying the blockchain
|
||||||
// If we're already synchronizing with another peer then this will also return fast
|
// If we're already synchronizing with another peer then this will also return fast
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
if (!blockchainLock.tryLock())
|
if (!blockchainLock.tryLock(3, TimeUnit.SECONDS)) {
|
||||||
// Wasn't peer's fault we couldn't sync
|
// Wasn't peer's fault we couldn't sync
|
||||||
|
LOGGER.info("Synchronizer couldn't acquire blockchain lock");
|
||||||
return SynchronizationResult.NO_BLOCKCHAIN_LOCK;
|
return SynchronizationResult.NO_BLOCKCHAIN_LOCK;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -793,7 +1096,7 @@ public class Synchronizer {
|
|||||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||||
|
|
||||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||||
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
|
LOGGER.info(String.format("Ditching our chain after height %d", commonBlockHeight));
|
||||||
} else {
|
} else {
|
||||||
// Compare chain weights
|
// Compare chain weights
|
||||||
|
|
||||||
@ -853,8 +1156,9 @@ public class Synchronizer {
|
|||||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
|
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
|
||||||
|
|
||||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
LOGGER.debug(String.format("commonBlockHeight: %d, commonBlockSig: %.8s, ourBlockSummaries.size(): %d, peerBlockSummaries.size(): %d", commonBlockHeight, Base58.encode(commonBlockSig), ourBlockSummaries.size(), peerBlockSummaries.size()));
|
||||||
|
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", accurateFormatter.format(ourChainWeight), accurateFormatter.format(peerChainWeight)));
|
||||||
|
|
||||||
// If our blockchain has greater weight then don't synchronize with peer
|
// If our blockchain has greater weight then don't synchronize with peer
|
||||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||||
@ -1222,7 +1526,7 @@ public class Synchronizer {
|
|||||||
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
|
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
public void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
||||||
final int firstBlockHeight = blockSummaries.get(0).getHeight();
|
final int firstBlockHeight = blockSummaries.get(0).getHeight();
|
||||||
|
|
||||||
for (int i = 0; i < blockSummaries.size(); ++i) {
|
for (int i = 0; i < blockSummaries.size(); ++i) {
|
||||||
|
@ -37,11 +37,16 @@ public class ArbitraryDataBuildManager extends Thread {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Arbitrary Data Build Manager");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use a fixed thread pool to execute the arbitrary data build actions (currently just a single thread)
|
// Use a fixed thread pool to execute the arbitrary data build actions (currently just a single thread)
|
||||||
// This can be expanded to have multiple threads processing the build queue when needed
|
// This can be expanded to have multiple threads processing the build queue when needed
|
||||||
ExecutorService arbitraryDataBuildExecutor = Executors.newFixedThreadPool(1);
|
int threadCount = 5;
|
||||||
arbitraryDataBuildExecutor.execute(new ArbitraryDataBuilderThread());
|
ExecutorService arbitraryDataBuildExecutor = Executors.newFixedThreadPool(threadCount);
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
arbitraryDataBuildExecutor.execute(new ArbitraryDataBuilderThread());
|
||||||
|
}
|
||||||
|
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
// Nothing to do yet
|
// Nothing to do yet
|
||||||
@ -101,7 +106,7 @@ public class ArbitraryDataBuildManager extends Thread {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Added {} to build queue", queueItem);
|
log(queueItem, String.format("Added %s to build queue", queueItem));
|
||||||
|
|
||||||
// Added to queue
|
// Added to queue
|
||||||
return true;
|
return true;
|
||||||
@ -149,7 +154,7 @@ public class ArbitraryDataBuildManager extends Thread {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Added {} to failed builds list", queueItem);
|
log(queueItem, String.format("Added %s to failed builds list", queueItem));
|
||||||
|
|
||||||
// Added to queue
|
// Added to queue
|
||||||
return true;
|
return true;
|
||||||
@ -182,4 +187,17 @@ public class ArbitraryDataBuildManager extends Thread {
|
|||||||
public boolean getBuildInProgress() {
|
public boolean getBuildInProgress() {
|
||||||
return this.buildInProgress;
|
return this.buildInProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void log(ArbitraryDataBuildQueueItem queueItem, String message) {
|
||||||
|
if (queueItem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueItem.isHighPriority()) {
|
||||||
|
LOGGER.info(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import org.qortal.repository.DataException;
|
|||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
@ -20,13 +21,14 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Arbitrary Data Build Manager");
|
Thread.currentThread().setName("Arbitrary Data Builder Thread");
|
||||||
ArbitraryDataBuildManager buildManager = ArbitraryDataBuildManager.getInstance();
|
ArbitraryDataBuildManager buildManager = ArbitraryDataBuildManager.getInstance();
|
||||||
|
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(100);
|
||||||
|
|
||||||
if (buildManager.arbitraryDataBuildQueue == null) {
|
if (buildManager.arbitraryDataBuildQueue == null) {
|
||||||
continue;
|
continue;
|
||||||
@ -35,48 +37,57 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find resources that are queued for building
|
|
||||||
Map.Entry<String, ArbitraryDataBuildQueueItem> next = buildManager.arbitraryDataBuildQueue
|
|
||||||
.entrySet().stream()
|
|
||||||
.filter(e -> e.getValue().isQueued())
|
|
||||||
.findFirst().get();
|
|
||||||
|
|
||||||
if (next == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
if (now == null) {
|
if (now == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryDataBuildQueueItem queueItem = next.getValue();
|
ArbitraryDataBuildQueueItem queueItem = null;
|
||||||
|
|
||||||
if (queueItem == null) {
|
// Find resources that are queued for building (sorted by highest priority first)
|
||||||
this.removeFromQueue(queueItem);
|
synchronized (buildManager.arbitraryDataBuildQueue) {
|
||||||
|
Map.Entry<String, ArbitraryDataBuildQueueItem> next = buildManager.arbitraryDataBuildQueue
|
||||||
|
.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue().isQueued())
|
||||||
|
.sorted(Comparator.comparing(item -> item.getValue().getPriority()))
|
||||||
|
.reduce((first, second) -> second).orElse(null);
|
||||||
|
|
||||||
|
if (next == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueItem = next.getValue();
|
||||||
|
|
||||||
|
if (queueItem == null) {
|
||||||
|
this.removeFromQueue(queueItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore builds that have failed recently
|
||||||
|
if (buildManager.isInFailedBuildsList(queueItem)) {
|
||||||
|
this.removeFromQueue(queueItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the start timestamp, to prevent other threads from building it at the same time
|
||||||
|
queueItem.prepareForBuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore builds that have failed recently
|
|
||||||
if (buildManager.isInFailedBuildsList(queueItem)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Perform the build
|
// Perform the build
|
||||||
LOGGER.info("Building {}...", queueItem);
|
log(queueItem, String.format("Building %s... priority: %d", queueItem, queueItem.getPriority()));
|
||||||
queueItem.build();
|
queueItem.build();
|
||||||
this.removeFromQueue(queueItem);
|
this.removeFromQueue(queueItem);
|
||||||
LOGGER.info("Finished building {}", queueItem);
|
log(queueItem, String.format("Finished building %s", queueItem));
|
||||||
|
|
||||||
} catch (MissingDataException e) {
|
} catch (MissingDataException e) {
|
||||||
LOGGER.info("Missing data for {}: {}", queueItem, e.getMessage());
|
log(queueItem, String.format("Missing data for %s: %s", queueItem, e.getMessage()));
|
||||||
queueItem.setFailed(true);
|
queueItem.setFailed(true);
|
||||||
this.removeFromQueue(queueItem);
|
this.removeFromQueue(queueItem);
|
||||||
// Don't add to the failed builds list, as we may want to retry sooner
|
// Don't add to the failed builds list, as we may want to retry sooner
|
||||||
|
|
||||||
} catch (IOException | DataException | RuntimeException e) {
|
} catch (IOException | DataException | RuntimeException e) {
|
||||||
LOGGER.info("Error building {}: {}", queueItem, e.getMessage());
|
log(queueItem, String.format("Error building %s: %s", queueItem, e.getMessage()));
|
||||||
// Something went wrong - so remove it from the queue, and add to failed builds list
|
// Something went wrong - so remove it from the queue, and add to failed builds list
|
||||||
queueItem.setFailed(true);
|
queueItem.setFailed(true);
|
||||||
buildManager.addToFailedBuildsList(queueItem);
|
buildManager.addToFailedBuildsList(queueItem);
|
||||||
@ -95,4 +106,17 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
|||||||
}
|
}
|
||||||
ArbitraryDataBuildManager.getInstance().arbitraryDataBuildQueue.remove(queueItem.getUniqueKey());
|
ArbitraryDataBuildManager.getInstance().arbitraryDataBuildQueue.remove(queueItem.getUniqueKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void log(ArbitraryDataBuildQueueItem queueItem, String message) {
|
||||||
|
if (queueItem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueItem.isHighPriority()) {
|
||||||
|
LOGGER.info(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,10 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||||
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (signatures == null || signatures.isEmpty()) {
|
if (signatures == null || signatures.isEmpty()) {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
continue;
|
continue;
|
||||||
@ -117,6 +121,10 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
|
|
||||||
// Loop through the signatures in this batch
|
// Loop through the signatures in this batch
|
||||||
for (int i=0; i<signatures.size(); i++) {
|
for (int i=0; i<signatures.size(); i++) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] signature = signatures.get(i);
|
byte[] signature = signatures.get(i);
|
||||||
if (signature == null) {
|
if (signature == null) {
|
||||||
continue;
|
continue;
|
||||||
@ -231,6 +239,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
// Delete random data associated with name if we're over our storage limit for this name
|
// Delete random data associated with name if we're over our storage limit for this name
|
||||||
// Use the DELETION_THRESHOLD, for the same reasons as above
|
// Use the DELETION_THRESHOLD, for the same reasons as above
|
||||||
for (String followedName : storageManager.followedNames()) {
|
for (String followedName : storageManager.followedNames()) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!storageManager.isStorageSpaceAvailableForName(repository, followedName, DELETION_THRESHOLD)) {
|
if (!storageManager.isStorageSpaceAvailableForName(repository, followedName, DELETION_THRESHOLD)) {
|
||||||
this.storageLimitReachedForName(repository, followedName);
|
this.storageLimitReachedForName(repository, followedName);
|
||||||
}
|
}
|
||||||
@ -253,6 +264,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
|
|
||||||
// Loop through each path and find those without matching signatures
|
// Loop through each path and find those without matching signatures
|
||||||
for (Path path : allPaths) {
|
for (Path path : allPaths) {
|
||||||
|
if (isStopping) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
String[] contents = path.toFile().list();
|
String[] contents = path.toFile().list();
|
||||||
if (contents == null || contents.length == 0) {
|
if (contents == null || contents.length == 0) {
|
||||||
@ -279,6 +293,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
private void checkForExpiredTransactions(Repository repository) {
|
private void checkForExpiredTransactions(Repository repository) {
|
||||||
List<Path> expiredPaths = this.findPathsWithNoAssociatedTransaction(repository);
|
List<Path> expiredPaths = this.findPathsWithNoAssociatedTransaction(repository);
|
||||||
for (Path expiredPath : expiredPaths) {
|
for (Path expiredPath : expiredPaths) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOGGER.info("Found path with no associated transaction: {}", expiredPath.toString());
|
LOGGER.info("Found path with no associated transaction: {}", expiredPath.toString());
|
||||||
this.safeDeleteDirectory(expiredPath.toFile(), "no matching transaction");
|
this.safeDeleteDirectory(expiredPath.toFile(), "no matching transaction");
|
||||||
}
|
}
|
||||||
@ -300,6 +317,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
// when they reach their storage limit
|
// when they reach their storage limit
|
||||||
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
||||||
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.deleteRandomFile(repository, dataPath.toFile(), null);
|
this.deleteRandomFile(repository, dataPath.toFile(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,6 +338,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
// when they reach their storage limit
|
// when they reach their storage limit
|
||||||
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
||||||
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.deleteRandomFile(repository, dataPath.toFile(), name);
|
this.deleteRandomFile(repository, dataPath.toFile(), name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,6 +452,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
final File[] directories = tempDir.toFile().listFiles();
|
final File[] directories = tempDir.toFile().listFiles();
|
||||||
if (directories != null) {
|
if (directories != null) {
|
||||||
for (final File directory : directories) {
|
for (final File directory : directories) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
contentsCount++;
|
contentsCount++;
|
||||||
|
|
||||||
// We're expecting the contents of each subfolder to be a directory
|
// We're expecting the contents of each subfolder to be a directory
|
||||||
@ -464,6 +490,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
final File[] directories = readerCacheNamesPath.toFile().listFiles();
|
final File[] directories = readerCacheNamesPath.toFile().listFiles();
|
||||||
if (directories != null) {
|
if (directories != null) {
|
||||||
for (final File directory : directories) {
|
for (final File directory : directories) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete data relating to blocked names
|
// Delete data relating to blocked names
|
||||||
String name = directory.getName();
|
String name = directory.getName();
|
||||||
@ -489,6 +518,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
|||||||
final File[] directories = readerNameCachePath.toFile().listFiles();
|
final File[] directories = readerNameCachePath.toFile().listFiles();
|
||||||
if (directories != null) {
|
if (directories != null) {
|
||||||
for (final File directory : directories) {
|
for (final File directory : directories) {
|
||||||
|
if (isStopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Each directory is a "service" type
|
// Each directory is a "service" type
|
||||||
String service = directory.getName();
|
String service = directory.getName();
|
||||||
this.cleanupReaderCacheForNameAndService(name, service, now);
|
this.cleanupReaderCacheForNameAndService(name, service, now);
|
||||||
|
@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
@ -59,7 +60,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
/** Maximum number of seconds that a file list relay request is able to exist on the network */
|
/** 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;
|
public static long RELAY_REQUEST_MAX_DURATION = 5000L;
|
||||||
/** Maximum number of hops that a file list relay request is allowed to make */
|
/** Maximum number of hops that a file list relay request is allowed to make */
|
||||||
public static int RELAY_REQUEST_MAX_HOPS = 3;
|
public static int RELAY_REQUEST_MAX_HOPS = 4;
|
||||||
|
|
||||||
|
|
||||||
private ArbitraryDataFileListManager() {
|
private ArbitraryDataFileListManager() {
|
||||||
@ -236,9 +237,11 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Lookup file lists by signature
|
// Lookup file lists by signature (and optionally hashes)
|
||||||
|
|
||||||
public boolean fetchArbitraryDataFileList(ArbitraryTransactionData arbitraryTransactionData) {
|
public boolean fetchArbitraryDataFileList(ArbitraryTransactionData arbitraryTransactionData) {
|
||||||
|
byte[] digest = arbitraryTransactionData.getData();
|
||||||
|
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||||
byte[] signature = arbitraryTransactionData.getSignature();
|
byte[] signature = arbitraryTransactionData.getSignature();
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
|
|
||||||
@ -261,10 +264,24 @@ public class ArbitraryDataFileListManager {
|
|||||||
this.addToSignatureRequests(signature58, true, false);
|
this.addToSignatureRequests(signature58, true, false);
|
||||||
|
|
||||||
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
||||||
LOGGER.debug(String.format("Sending data file list request for signature %s to %d peers...", signature58, handshakedPeers.size()));
|
List<byte[]> missingHashes = null;
|
||||||
|
|
||||||
|
// // TODO: uncomment after GetArbitraryDataFileListMessage updates are deployed
|
||||||
|
// // Find hashes that we are missing
|
||||||
|
// try {
|
||||||
|
// ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||||
|
// arbitraryDataFile.setMetadataHash(metadataHash);
|
||||||
|
// missingHashes = arbitraryDataFile.missingHashes();
|
||||||
|
// } catch (DataException e) {
|
||||||
|
// // Leave missingHashes as null, so that all hashes are requested
|
||||||
|
// }
|
||||||
|
// int hashCount = missingHashes != null ? missingHashes.size() : 0;
|
||||||
|
|
||||||
|
int hashCount = 0;
|
||||||
|
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
|
||||||
|
|
||||||
// Build request
|
// Build request
|
||||||
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, now, 0);
|
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, missingHashes, now, 0);
|
||||||
|
|
||||||
// Save our request into requests map
|
// Save our request into requests map
|
||||||
Triple<String, Peer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
|
Triple<String, Peer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
|
||||||
@ -304,6 +321,64 @@ public class ArbitraryDataFileListManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean fetchArbitraryDataFileList(Peer 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);
|
||||||
|
|
||||||
|
// Save our request into requests map
|
||||||
|
Triple<String, Peer, 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) {
|
public void deleteFileListRequestsForSignature(byte[] signature) {
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
||||||
@ -377,6 +452,14 @@ public class ArbitraryDataFileListManager {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
||||||
|
// Keep track of the hashes this peer reports to have access to
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
for (byte[] hash : hashes) {
|
||||||
|
String hash58 = Base58.encode(hash);
|
||||||
|
String sig58 = Base58.encode(signature);
|
||||||
|
ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.put(hash58, new Triple<>(peer, sig58, now));
|
||||||
|
}
|
||||||
|
|
||||||
// Go and fetch the actual data, since this isn't a relay request
|
// Go and fetch the actual data, since this isn't a relay request
|
||||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, hashes);
|
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, hashes);
|
||||||
}
|
}
|
||||||
@ -395,10 +478,8 @@ public class ArbitraryDataFileListManager {
|
|||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
for (byte[] hash : hashes) {
|
for (byte[] hash : hashes) {
|
||||||
String hash58 = Base58.encode(hash);
|
String hash58 = Base58.encode(hash);
|
||||||
Triple<String, Peer, Long> value = new Triple<>(signature58, peer, now);
|
ArbitraryRelayInfo relayMap = new ArbitraryRelayInfo(hash58, signature58, peer, now);
|
||||||
if (arbitraryDataFileManager.arbitraryRelayMap.putIfAbsent(hash58, value) == null) {
|
ArbitraryDataFileManager.getInstance().addToRelayMap(relayMap);
|
||||||
LOGGER.debug("Added {} to relay map: {}, {}, {}", hash58, signature58, peer, now);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward to requesting peer
|
// Forward to requesting peer
|
||||||
@ -422,6 +503,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||||
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
|
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
||||||
|
|
||||||
@ -451,36 +533,37 @@ public class ArbitraryDataFileListManager {
|
|||||||
|
|
||||||
// Load file(s) and add any that exist to the list of hashes
|
// Load file(s) and add any that exist to the list of hashes
|
||||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||||
if (metadataHash != null) {
|
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
|
||||||
|
|
||||||
// Assume all chunks exists, unless one can't be found below
|
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
|
||||||
allChunksExist = true;
|
if (requestedHashes == null || requestedHashes.isEmpty()) {
|
||||||
|
requestedHashes = new ArrayList<>();
|
||||||
|
|
||||||
// If we have the metadata file, add its hash
|
// Add the metadata file
|
||||||
if (arbitraryDataFile.getMetadataFile().exists()) {
|
if (arbitraryDataFile.getMetadataHash() != null) {
|
||||||
hashes.add(arbitraryDataFile.getMetadataHash());
|
requestedHashes.add(arbitraryDataFile.getMetadataHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the chunk hashes
|
||||||
|
if (arbitraryDataFile.getChunkHashes().size() > 0) {
|
||||||
|
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
|
||||||
|
}
|
||||||
|
// Add complete file if there are no hashes
|
||||||
else {
|
else {
|
||||||
allChunksExist = false;
|
requestedHashes.add(arbitraryDataFile.getHash());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (ArbitraryDataFileChunk chunk : arbitraryDataFile.getChunks()) {
|
// Assume all chunks exists, unless one can't be found below
|
||||||
if (chunk.exists()) {
|
allChunksExist = true;
|
||||||
hashes.add(chunk.getHash());
|
|
||||||
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
for (byte[] requestedHash : requestedHashes) {
|
||||||
} else {
|
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(requestedHash, signature);
|
||||||
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
if (chunk.exists()) {
|
||||||
allChunksExist = false;
|
hashes.add(chunk.getHash());
|
||||||
}
|
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
||||||
}
|
} else {
|
||||||
} else {
|
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
||||||
// This transaction has no chunks, so include the complete file if we have it
|
|
||||||
if (arbitraryDataFile.exists()) {
|
|
||||||
hashes.add(arbitraryDataFile.getHash());
|
|
||||||
allChunksExist = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
allChunksExist = false;
|
allChunksExist = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
|
||||||
import org.qortal.data.network.ArbitraryPeerData;
|
import org.qortal.data.network.ArbitraryPeerData;
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
import org.qortal.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
import org.qortal.network.message.*;
|
import org.qortal.network.message.*;
|
||||||
@ -22,13 +22,16 @@ import org.qortal.utils.Triple;
|
|||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ArbitraryDataFileManager {
|
public class ArbitraryDataFileManager extends Thread {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileManager.class);
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileManager.class);
|
||||||
|
|
||||||
private static ArbitraryDataFileManager instance;
|
private static ArbitraryDataFileManager instance;
|
||||||
|
private volatile boolean isStopping = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,10 +40,16 @@ public class ArbitraryDataFileManager {
|
|||||||
private Map<String, Long> arbitraryDataFileRequests = Collections.synchronizedMap(new HashMap<>());
|
private Map<String, Long> arbitraryDataFileRequests = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map to keep track of hashes that we might need to relay, keyed by the hash of the file (base58 encoded).
|
* Map to keep track of hashes that we might need to relay
|
||||||
* Value is comprised of the base58-encoded signature, the peer that is hosting it, and the timestamp that it was added
|
|
||||||
*/
|
*/
|
||||||
public Map<String, Triple<String, Peer, Long>> arbitraryRelayMap = Collections.synchronizedMap(new HashMap<>());
|
public List<ArbitraryRelayInfo> arbitraryRelayMap = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map to keep track of any arbitrary data file hash responses
|
||||||
|
* Key: string - the hash encoded in base58
|
||||||
|
* Value: Triple<respondingPeer, signature58, timeResponded>
|
||||||
|
*/
|
||||||
|
public Map<String, Triple<Peer, String, Long>> arbitraryDataFileHashResponses = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
|
|
||||||
private ArbitraryDataFileManager() {
|
private ArbitraryDataFileManager() {
|
||||||
@ -53,6 +62,32 @@ public class ArbitraryDataFileManager {
|
|||||||
return instance;
|
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 = 10;
|
||||||
|
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) {
|
public void cleanupRequestCache(Long now) {
|
||||||
if (now == null) {
|
if (now == null) {
|
||||||
@ -62,29 +97,14 @@ public class ArbitraryDataFileManager {
|
|||||||
arbitraryDataFileRequests.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < requestMinimumTimestamp);
|
arbitraryDataFileRequests.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < requestMinimumTimestamp);
|
||||||
|
|
||||||
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
||||||
arbitraryRelayMap.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < relayMinimumTimestamp);
|
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
|
||||||
|
arbitraryDataFileHashResponses.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < relayMinimumTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Fetch data files by hash
|
// Fetch data files by hash
|
||||||
|
|
||||||
public boolean fetchAllArbitraryDataFiles(Repository repository, Peer peer, byte[] signature) {
|
|
||||||
try {
|
|
||||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
|
||||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
|
||||||
|
|
||||||
// We use null to represent all hashes associated with this transaction
|
|
||||||
return this.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, null);
|
|
||||||
|
|
||||||
} catch (DataException e) {}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean fetchArbitraryDataFiles(Repository repository,
|
public boolean fetchArbitraryDataFiles(Repository repository,
|
||||||
Peer peer,
|
Peer peer,
|
||||||
byte[] signature,
|
byte[] signature,
|
||||||
@ -95,43 +115,46 @@ public class ArbitraryDataFileManager {
|
|||||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(arbitraryTransactionData.getData(), signature);
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(arbitraryTransactionData.getData(), signature);
|
||||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||||
|
|
||||||
// If hashes are null, we will treat this to mean all data hashes associated with this file
|
|
||||||
if (hashes == null) {
|
|
||||||
if (metadataHash == null) {
|
|
||||||
// This transaction has no metadata/chunks, so use the main file hash
|
|
||||||
hashes = Arrays.asList(arbitraryDataFile.getHash());
|
|
||||||
}
|
|
||||||
else if (!arbitraryDataFile.getMetadataFile().exists()) {
|
|
||||||
// We don't have the metadata file yet, so request it
|
|
||||||
hashes = Arrays.asList(arbitraryDataFile.getMetadataFile().getHash());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Add the chunk hashes
|
|
||||||
hashes = arbitraryDataFile.getChunkHashes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean receivedAtLeastOneFile = false;
|
boolean receivedAtLeastOneFile = false;
|
||||||
|
|
||||||
// Now fetch actual data from this peer
|
// Now fetch actual data from this peer
|
||||||
for (byte[] hash : hashes) {
|
for (byte[] hash : hashes) {
|
||||||
|
if (isStopping) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String hash58 = Base58.encode(hash);
|
||||||
if (!arbitraryDataFile.chunkExists(hash)) {
|
if (!arbitraryDataFile.chunkExists(hash)) {
|
||||||
// Only request the file if we aren't already requesting it from someone else
|
// Only request the file if we aren't already requesting it from someone else
|
||||||
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
|
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
|
||||||
|
LOGGER.debug("Requesting data file {} from peer {}", hash58, peer);
|
||||||
|
Long startTime = NTP.getTime();
|
||||||
ArbitraryDataFileMessage receivedArbitraryDataFileMessage = fetchArbitraryDataFile(peer, null, signature, hash, null);
|
ArbitraryDataFileMessage receivedArbitraryDataFileMessage = fetchArbitraryDataFile(peer, null, signature, hash, null);
|
||||||
|
Long endTime = NTP.getTime();
|
||||||
if (receivedArbitraryDataFileMessage != null) {
|
if (receivedArbitraryDataFileMessage != null) {
|
||||||
LOGGER.debug("Received data file {} from peer {}", receivedArbitraryDataFileMessage.getArbitraryDataFile().getHash58(), peer);
|
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFileMessage.getArbitraryDataFile().getHash58(), peer, (endTime-startTime));
|
||||||
receivedAtLeastOneFile = true;
|
receivedAtLeastOneFile = true;
|
||||||
|
|
||||||
|
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
|
||||||
|
arbitraryDataFileHashResponses.remove(hash58);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}", peer, Base58.encode(hash), Base58.encode(signature));
|
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 {
|
else {
|
||||||
LOGGER.trace("Already requesting data file {} for signature {}", arbitraryDataFile, Base58.encode(signature));
|
LOGGER.trace("Already requesting data file {} for signature {}", arbitraryDataFile, Base58.encode(signature));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Remove this hash from arbitraryDataFileHashResponses because we have a local copy
|
||||||
|
arbitraryDataFileHashResponses.remove(hash58);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receivedAtLeastOneFile) {
|
if (receivedAtLeastOneFile) {
|
||||||
@ -147,22 +170,23 @@ public class ArbitraryDataFileManager {
|
|||||||
|
|
||||||
// Invalidate the hosted transactions cache as we are now hosting something new
|
// Invalidate the hosted transactions cache as we are now hosting something new
|
||||||
ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache();
|
ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache();
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have all the files we need for this transaction
|
// Check if we have all the files we need for this transaction
|
||||||
if (arbitraryDataFile.allFilesExist()) {
|
if (arbitraryDataFile.allFilesExist()) {
|
||||||
|
|
||||||
// We have all the chunks for this transaction, so we should invalidate the transaction's name's
|
// 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
|
// data cache so that it is rebuilt the next time we serve it
|
||||||
ArbitraryDataManager.getInstance().invalidateCache(arbitraryTransactionData);
|
ArbitraryDataManager.getInstance().invalidateCache(arbitraryTransactionData);
|
||||||
|
|
||||||
// We may also need to broadcast to the network that we are now hosting files for this transaction,
|
// We may also need to broadcast to the network that we are now hosting files for this transaction,
|
||||||
// but only if these files are in accordance with our storage policy
|
// but only if these files are in accordance with our storage policy
|
||||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(arbitraryTransactionData)) {
|
if (ArbitraryDataStorageManager.getInstance().canStoreData(arbitraryTransactionData)) {
|
||||||
// Use a null peer address to indicate our own
|
// Use a null peer address to indicate our own
|
||||||
Message newArbitrarySignatureMessage = new ArbitrarySignaturesMessage(null, 0, Arrays.asList(signature));
|
Message newArbitrarySignatureMessage = new ArbitrarySignaturesMessage(null, 0, Arrays.asList(signature));
|
||||||
Network.getInstance().broadcast(broadcastPeer -> newArbitrarySignatureMessage);
|
Network.getInstance().broadcast(broadcastPeer -> newArbitrarySignatureMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return receivedAtLeastOneFile;
|
return receivedAtLeastOneFile;
|
||||||
@ -171,11 +195,11 @@ public class ArbitraryDataFileManager {
|
|||||||
private ArbitraryDataFileMessage fetchArbitraryDataFile(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
private ArbitraryDataFileMessage fetchArbitraryDataFile(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||||
boolean fileAlreadyExists = existingFile.exists();
|
boolean fileAlreadyExists = existingFile.exists();
|
||||||
|
String hash58 = Base58.encode(hash);
|
||||||
Message message = null;
|
Message message = null;
|
||||||
|
|
||||||
// Fetch the file if it doesn't exist locally
|
// Fetch the file if it doesn't exist locally
|
||||||
if (!fileAlreadyExists) {
|
if (!fileAlreadyExists) {
|
||||||
String hash58 = Base58.encode(hash);
|
|
||||||
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
|
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
|
||||||
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
||||||
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
||||||
@ -191,9 +215,17 @@ public class ArbitraryDataFileManager {
|
|||||||
// We may need to remove the file list request, if we have all the files for this transaction
|
// We may need to remove the file list request, if we have all the files for this transaction
|
||||||
this.handleFileListRequests(signature);
|
this.handleFileListRequests(signature);
|
||||||
|
|
||||||
if (message == null || message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
|
if (message == null) {
|
||||||
|
LOGGER.debug("Received null message from peer {}", peer);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
|
||||||
|
LOGGER.debug("Received message with invalid type: {} from peer {}", message.getType(), peer);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||||
}
|
}
|
||||||
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message;
|
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message;
|
||||||
|
|
||||||
@ -359,6 +391,48 @@ public class ArbitraryDataFileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Relays
|
||||||
|
|
||||||
|
private List<ArbitraryRelayInfo> getRelayInfoListForHash(String hash58) {
|
||||||
|
synchronized (arbitraryRelayMap) {
|
||||||
|
return arbitraryRelayMap.stream()
|
||||||
|
.filter(relayInfo -> Objects.equals(relayInfo.getHash58(), hash58))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArbitraryRelayInfo getRandomRelayInfoEntryForHash(String hash58) {
|
||||||
|
LOGGER.trace("Fetching random relay info for hash: {}", hash58);
|
||||||
|
List<ArbitraryRelayInfo> 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(ArbitraryRelayInfo 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(ArbitraryRelayInfo entry) {
|
||||||
|
arbitraryRelayMap.removeIf(relayInfo -> relayInfo.equals(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Network handlers
|
// Network handlers
|
||||||
|
|
||||||
public void onNetworkGetArbitraryDataFileMessage(Peer peer, Message message) {
|
public void onNetworkGetArbitraryDataFileMessage(Peer peer, Message message) {
|
||||||
@ -377,7 +451,7 @@ public class ArbitraryDataFileManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||||
Triple<String, Peer, Long> relayInfo = this.arbitraryRelayMap.get(hash58);
|
ArbitraryRelayInfo relayInfo = this.getRandomRelayInfoEntryForHash(hash58);
|
||||||
|
|
||||||
if (arbitraryDataFile.exists()) {
|
if (arbitraryDataFile.exists()) {
|
||||||
LOGGER.trace("Hash {} exists", hash58);
|
LOGGER.trace("Hash {} exists", hash58);
|
||||||
@ -394,15 +468,12 @@ public class ArbitraryDataFileManager {
|
|||||||
else if (relayInfo != null) {
|
else if (relayInfo != null) {
|
||||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||||
// We need to ask this peer for the file
|
// We need to ask this peer for the file
|
||||||
Peer peerToAsk = relayInfo.getB();
|
Peer peerToAsk = relayInfo.getPeer();
|
||||||
if (peerToAsk != null) {
|
if (peerToAsk != null) {
|
||||||
|
|
||||||
// Forward the message to this peer
|
// Forward the message to this peer
|
||||||
LOGGER.debug("Asking peer {} for hash {}", peerToAsk, hash58);
|
LOGGER.debug("Asking peer {} for hash {}", peerToAsk, hash58);
|
||||||
this.fetchArbitraryDataFile(peerToAsk, peer, signature, hash, message);
|
this.fetchArbitraryDataFile(peerToAsk, peer, signature, hash, message);
|
||||||
|
|
||||||
// Remove from the map regardless of outcome, as the relay attempt is now considered complete
|
|
||||||
arbitraryRelayMap.remove(hash58);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LOGGER.debug("Peer {} not found in relay info", peer);
|
LOGGER.debug("Peer {} not found in relay info", peer);
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
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.transaction.ArbitraryTransactionData;
|
||||||
|
import org.qortal.network.Peer;
|
||||||
|
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 org.qortal.utils.Triple;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ArbitraryDataFileRequestThread implements Runnable {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
|
||||||
|
|
||||||
|
public ArbitraryDataFileRequestThread() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Arbitrary Data File Request Thread");
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (!Controller.isStopping()) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
this.processFileHashes(now);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Fall-through to exit thread...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processFileHashes(Long now) {
|
||||||
|
if (Controller.isStopping()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||||
|
String signature58 = null;
|
||||||
|
String hash58 = null;
|
||||||
|
Peer peer = null;
|
||||||
|
boolean shouldProcess = false;
|
||||||
|
|
||||||
|
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
||||||
|
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
if (Controller.isStopping()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.Entry entry = (Map.Entry) iterator.next();
|
||||||
|
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash58 = (String) entry.getKey();
|
||||||
|
Triple<Peer, String, Long> value = (Triple<Peer, String, Long>) entry.getValue();
|
||||||
|
if (value == null) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
peer = value.getA();
|
||||||
|
signature58 = value.getB();
|
||||||
|
Long timestamp = value.getC();
|
||||||
|
|
||||||
|
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
|
||||||
|
// Ignore - to be deleted
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to process this file
|
||||||
|
shouldProcess = true;
|
||||||
|
iterator.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldProcess) {
|
||||||
|
// Nothing to do
|
||||||
|
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.debug("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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,7 @@ public class ArbitraryDataManager extends Thread {
|
|||||||
public static final long ARBITRARY_REQUEST_TIMEOUT = 10 * 1000L; // ms
|
public static final long ARBITRARY_REQUEST_TIMEOUT = 10 * 1000L; // ms
|
||||||
|
|
||||||
/** Maximum time to hold information about an in-progress relay */
|
/** Maximum time to hold information about an in-progress relay */
|
||||||
public static final long ARBITRARY_RELAY_TIMEOUT = 30 * 1000L; // ms
|
public static final long ARBITRARY_RELAY_TIMEOUT = 60 * 1000L; // ms
|
||||||
|
|
||||||
/** Maximum number of hops that an arbitrary signatures request is allowed to make */
|
/** Maximum number of hops that an arbitrary signatures request is allowed to make */
|
||||||
private static int ARBITRARY_SIGNATURES_REQUEST_MAX_HOPS = 3;
|
private static int ARBITRARY_SIGNATURES_REQUEST_MAX_HOPS = 3;
|
||||||
|
@ -32,7 +32,7 @@ public class ArbitraryDataRenderManager extends Thread {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Arbitrary Data Manager");
|
Thread.currentThread().setName("Arbitrary Data Render Manager");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -47,7 +48,7 @@ public class AtStatesPruner implements Runnable {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||||
if (Controller.getInstance().isSynchronizing())
|
if (Synchronizer.getInstance().isSynchronizing())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Prune AT states for all blocks up until our latest minus pruneBlockLimit
|
// Prune AT states for all blocks up until our latest minus pruneBlockLimit
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -34,7 +35,7 @@ public class AtStatesTrimmer implements Runnable {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||||
if (Controller.getInstance().isSynchronizing())
|
if (Synchronizer.getInstance().isSynchronizing())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.repository.*;
|
import org.qortal.repository.*;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
@ -51,7 +52,7 @@ public class BlockArchiver implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||||
if (Controller.getInstance().isSynchronizing()) {
|
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -51,7 +52,7 @@ public class BlockPruner implements Runnable {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||||
if (Controller.getInstance().isSynchronizing()) {
|
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,42 +268,6 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
return registerNameTransactions;
|
return registerNameTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UpdateNameTransactionData> fetchUpdateNameTransactions() {
|
|
||||||
List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>();
|
|
||||||
|
|
||||||
for (TransactionData transactionData : this.nameTransactions) {
|
|
||||||
if (transactionData.getType() == TransactionType.UPDATE_NAME) {
|
|
||||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
|
||||||
updateNameTransactions.add(updateNameTransactionData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updateNameTransactions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<SellNameTransactionData> fetchSellNameTransactions() {
|
|
||||||
List<SellNameTransactionData> sellNameTransactions = new ArrayList<>();
|
|
||||||
|
|
||||||
for (TransactionData transactionData : this.nameTransactions) {
|
|
||||||
if (transactionData.getType() == TransactionType.SELL_NAME) {
|
|
||||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
|
||||||
sellNameTransactions.add(sellNameTransactionData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sellNameTransactions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<BuyNameTransactionData> fetchBuyNameTransactions() {
|
|
||||||
List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>();
|
|
||||||
|
|
||||||
for (TransactionData transactionData : this.nameTransactions) {
|
|
||||||
if (transactionData.getType() == TransactionType.BUY_NAME) {
|
|
||||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
|
||||||
buyNameTransactions.add(buyNameTransactionData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buyNameTransactions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchAllNameTransactions(Repository repository) throws DataException {
|
private void fetchAllNameTransactions(Repository repository) throws DataException {
|
||||||
List<TransactionData> nameTransactions = new ArrayList<>();
|
List<TransactionData> nameTransactions = new ArrayList<>();
|
||||||
|
|
||||||
@ -319,41 +283,34 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
this.nameTransactions = nameTransactions;
|
this.nameTransactions = nameTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
|
public List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
|
||||||
List<TransactionData> transactions = new ArrayList<>();
|
List<byte[]> signatures = new ArrayList<>();
|
||||||
String reducedName = Unicode.sanitize(name);
|
String reducedName = Unicode.sanitize(name);
|
||||||
|
|
||||||
// Fetch all the confirmed name-modification transactions
|
List<byte[]> registerNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||||
if (this.nameTransactions.isEmpty()) {
|
TransactionType.REGISTER_NAME, Arrays.asList("(name = ? OR reduced_name = ?)"), Arrays.asList(name, reducedName));
|
||||||
this.fetchAllNameTransactions(repository);
|
signatures.addAll(registerNameTransactions);
|
||||||
}
|
|
||||||
|
|
||||||
for (TransactionData transactionData : this.nameTransactions) {
|
List<byte[]> updateNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||||
|
TransactionType.UPDATE_NAME,
|
||||||
|
Arrays.asList("(name = ? OR new_name = ? OR (reduced_new_name != '' AND reduced_new_name = ?))"),
|
||||||
|
Arrays.asList(name, name, reducedName));
|
||||||
|
signatures.addAll(updateNameTransactions);
|
||||||
|
|
||||||
if ((transactionData instanceof RegisterNameTransactionData)) {
|
List<byte[]> sellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
TransactionType.SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||||
if (Objects.equals(registerNameTransactionData.getReducedName(), reducedName)) {
|
signatures.addAll(sellNameTransactions);
|
||||||
transactions.add(transactionData);
|
|
||||||
}
|
List<byte[]> buyNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||||
}
|
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||||
if ((transactionData instanceof UpdateNameTransactionData)) {
|
signatures.addAll(buyNameTransactions);
|
||||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
|
||||||
if (Objects.equals(updateNameTransactionData.getName(), name) ||
|
List<TransactionData> transactions = new ArrayList<>();
|
||||||
Objects.equals(updateNameTransactionData.getReducedNewName(), reducedName)) {
|
for (byte[] signature : signatures) {
|
||||||
transactions.add(transactionData);
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||||
}
|
// Filter out any unconfirmed transactions
|
||||||
}
|
if (transactionData.getBlockHeight() != null && transactionData.getBlockHeight() > 0) {
|
||||||
if ((transactionData instanceof BuyNameTransactionData)) {
|
transactions.add(transactionData);
|
||||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
|
||||||
if (Objects.equals(buyNameTransactionData.getName(), name)) {
|
|
||||||
transactions.add(transactionData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((transactionData instanceof SellNameTransactionData)) {
|
|
||||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
|
||||||
if (Objects.equals(sellNameTransactionData.getName(), name)) {
|
|
||||||
transactions.add(transactionData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transactions;
|
return transactions;
|
||||||
|
@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -36,7 +37,7 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||||
if (Controller.getInstance().isSynchronizing())
|
if (Synchronizer.getInstance().isSynchronizing())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Trim blockchain by removing 'old' online accounts signatures
|
// Trim blockchain by removing 'old' online accounts signatures
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -39,6 +33,7 @@ import org.qortal.utils.Amounts;
|
|||||||
import org.qortal.utils.BitTwiddling;
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
|
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
|
||||||
public abstract class Bitcoiny implements ForeignBlockchain {
|
public abstract class Bitcoiny implements ForeignBlockchain {
|
||||||
@ -53,6 +48,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
protected final NetworkParameters params;
|
protected final NetworkParameters params;
|
||||||
|
|
||||||
|
/** Cache recent transactions to speed up subsequent lookups */
|
||||||
|
protected List<SimpleTransaction> transactionsCache;
|
||||||
|
protected Long transactionsCacheTimestamp;
|
||||||
|
protected String transactionsCacheXpub;
|
||||||
|
protected static long TRANSACTIONS_CACHE_TIMEOUT = 2 * 60 * 1000L; // 2 minutes
|
||||||
|
|
||||||
/** Keys that have been previously marked as fully spent,<br>
|
/** Keys that have been previously marked as fully spent,<br>
|
||||||
* i.e. keys with transactions but with no unspent outputs. */
|
* i.e. keys with transactions but with no unspent outputs. */
|
||||||
protected final Set<ECKey> spentKeys = Collections.synchronizedSet(new HashSet<>());
|
protected final Set<ECKey> spentKeys = Collections.synchronizedSet(new HashSet<>());
|
||||||
@ -228,6 +229,25 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
return transaction.getOutputs();
|
return transaction.getOutputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns transactions for passed script
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
public List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
int retries = 0;
|
||||||
|
ForeignBlockchainException e2 = null;
|
||||||
|
while (retries <= 3) {
|
||||||
|
try {
|
||||||
|
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
e2 = e;
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw(e2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of transaction hashes pertaining to passed address.
|
* Returns list of transaction hashes pertaining to passed address.
|
||||||
* <p>
|
* <p>
|
||||||
@ -262,7 +282,17 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if error occurs
|
* @throws ForeignBlockchainException if error occurs
|
||||||
*/
|
*/
|
||||||
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
|
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
|
||||||
return this.blockchain.getTransaction(txHash);
|
int retries = 0;
|
||||||
|
ForeignBlockchainException e2 = null;
|
||||||
|
while (retries <= 3) {
|
||||||
|
try {
|
||||||
|
return this.blockchain.getTransaction(txHash);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
e2 = e;
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw(e2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,70 +367,99 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
return balance.value;
|
return balance.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||||
|
long balance = 0;
|
||||||
|
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp);
|
||||||
|
List<SimpleTransaction> transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
|
||||||
|
for (SimpleTransaction transaction : transactions) {
|
||||||
|
balance += transaction.getTotalAmount();
|
||||||
|
}
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
public List<SimpleTransaction> getWalletTransactions(String key58) throws ForeignBlockchainException {
|
public List<SimpleTransaction> getWalletTransactions(String key58) throws ForeignBlockchainException {
|
||||||
Context.propagate(bitcoinjContext);
|
synchronized (this) {
|
||||||
|
// Serve from the cache if it's recent, and matches this xpub
|
||||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
if (Objects.equals(transactionsCacheXpub, key58)) {
|
||||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
if (transactionsCache != null && transactionsCacheTimestamp != null) {
|
||||||
|
Long now = NTP.getTime();
|
||||||
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
boolean isCacheStale = (now != null && now - transactionsCacheTimestamp >= TRANSACTIONS_CACHE_TIMEOUT);
|
||||||
keyChain.maybeLookAhead();
|
if (!isCacheStale) {
|
||||||
|
return transactionsCache;
|
||||||
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
}
|
||||||
|
|
||||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
|
||||||
Set<String> keySet = new HashSet<>();
|
|
||||||
|
|
||||||
// Set the number of consecutive empty batches required before giving up
|
|
||||||
final int numberOfAdditionalBatchesToSearch = 5;
|
|
||||||
|
|
||||||
int unusedCounter = 0;
|
|
||||||
int ki = 0;
|
|
||||||
do {
|
|
||||||
boolean areAllKeysUnused = true;
|
|
||||||
|
|
||||||
for (; ki < keys.size(); ++ki) {
|
|
||||||
DeterministicKey dKey = keys.get(ki);
|
|
||||||
|
|
||||||
// Check for transactions
|
|
||||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
|
||||||
keySet.add(address.toString());
|
|
||||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
|
||||||
|
|
||||||
// Ask for transaction history - if it's empty then key has never been used
|
|
||||||
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
|
|
||||||
|
|
||||||
if (!historicTransactionHashes.isEmpty()) {
|
|
||||||
areAllKeysUnused = false;
|
|
||||||
|
|
||||||
for (TransactionHash transactionHash : historicTransactionHashes)
|
|
||||||
walletTransactions.add(this.getTransaction(transactionHash.txHash));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areAllKeysUnused) {
|
Context.propagate(bitcoinjContext);
|
||||||
// No transactions
|
|
||||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
// ... and we've hit our search limit
|
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||||
break;
|
|
||||||
|
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
|
||||||
|
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||||
|
|
||||||
|
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||||
|
Set<String> keySet = new HashSet<>();
|
||||||
|
|
||||||
|
// Set the number of consecutive empty batches required before giving up
|
||||||
|
final int numberOfAdditionalBatchesToSearch = 5;
|
||||||
|
|
||||||
|
int unusedCounter = 0;
|
||||||
|
int ki = 0;
|
||||||
|
do {
|
||||||
|
boolean areAllKeysUnused = true;
|
||||||
|
|
||||||
|
for (; ki < keys.size(); ++ki) {
|
||||||
|
DeterministicKey dKey = keys.get(ki);
|
||||||
|
|
||||||
|
// Check for transactions
|
||||||
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
|
keySet.add(address.toString());
|
||||||
|
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||||
|
|
||||||
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
|
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||||
|
|
||||||
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
|
areAllKeysUnused = false;
|
||||||
|
|
||||||
|
for (TransactionHash transactionHash : historicTransactionHashes)
|
||||||
|
walletTransactions.add(this.getTransaction(transactionHash.txHash));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
|
||||||
unusedCounter++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Some keys in this batch were used, so reset the counter
|
|
||||||
unusedCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate some more keys
|
if (areAllKeysUnused) {
|
||||||
keys.addAll(generateMoreKeys(keyChain));
|
// No transactions
|
||||||
|
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||||
|
// ... and we've hit our search limit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||||
|
unusedCounter++;
|
||||||
|
} else {
|
||||||
|
// Some keys in this batch were used, so reset the counter
|
||||||
|
unusedCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Process new keys
|
// Generate some more keys
|
||||||
} while (true);
|
keys.addAll(generateMoreKeys(keyChain));
|
||||||
|
|
||||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
// Process new keys
|
||||||
|
} while (true);
|
||||||
|
|
||||||
return walletTransactions.stream().map(t -> convertToSimpleTransaction(t, keySet)).sorted(newestTimestampFirstComparator).collect(Collectors.toList());
|
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
||||||
|
|
||||||
|
// Update cache and return
|
||||||
|
transactionsCacheTimestamp = NTP.getTime();
|
||||||
|
transactionsCacheXpub = key58;
|
||||||
|
transactionsCache = walletTransactions.stream()
|
||||||
|
.map(t -> convertToSimpleTransaction(t, keySet))
|
||||||
|
.sorted(newestTimestampFirstComparator).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return transactionsCache;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||||
@ -417,13 +476,15 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
List<String> senders = t2.outputs.get(input.outputVout).addresses;
|
List<String> senders = t2.outputs.get(input.outputVout).addresses;
|
||||||
long inputAmount = t2.outputs.get(input.outputVout).value;
|
long inputAmount = t2.outputs.get(input.outputVout).value;
|
||||||
totalInputAmount += inputAmount;
|
totalInputAmount += inputAmount;
|
||||||
for (String sender : senders) {
|
if (senders != null) {
|
||||||
boolean addressInWallet = false;
|
for (String sender : senders) {
|
||||||
if (keySet.contains(sender)) {
|
boolean addressInWallet = false;
|
||||||
total += inputAmount;
|
if (keySet.contains(sender)) {
|
||||||
addressInWallet = true;
|
total += inputAmount;
|
||||||
|
addressInWallet = true;
|
||||||
|
}
|
||||||
|
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
|
||||||
}
|
}
|
||||||
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
|
|
||||||
}
|
}
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
|
LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
|
||||||
@ -431,17 +492,19 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
if (t.outputs != null && !t.outputs.isEmpty()) {
|
if (t.outputs != null && !t.outputs.isEmpty()) {
|
||||||
for (BitcoinyTransaction.Output output : t.outputs) {
|
for (BitcoinyTransaction.Output output : t.outputs) {
|
||||||
for (String address : output.addresses) {
|
if (output.addresses != null) {
|
||||||
boolean addressInWallet = false;
|
for (String address : output.addresses) {
|
||||||
if (keySet.contains(address)) {
|
boolean addressInWallet = false;
|
||||||
if (total > 0L) {
|
if (keySet.contains(address)) {
|
||||||
amount -= (total - output.value);
|
if (total > 0L) {
|
||||||
} else {
|
amount -= (total - output.value);
|
||||||
amount += output.value;
|
} else {
|
||||||
|
amount += output.value;
|
||||||
|
}
|
||||||
|
addressInWallet = true;
|
||||||
}
|
}
|
||||||
addressInWallet = true;
|
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
|
||||||
}
|
}
|
||||||
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
|
|
||||||
}
|
}
|
||||||
totalOutputAmount += output.value;
|
totalOutputAmount += output.value;
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,13 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
|
|
||||||
public static final String CURRENCY_CODE = "DOGE";
|
public static final String CURRENCY_CODE = "DOGE";
|
||||||
|
|
||||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(500000000); // 5 DOGE per 1000 bytes
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1000000); // 0.01 DOGE per 1000 bytes
|
||||||
|
|
||||||
private static final long MINIMUM_ORDER_AMOUNT = 300000000L; // 3 DOGE minimum order. The RPC dust threshold is around 2 DOGE
|
private static final long MINIMUM_ORDER_AMOUNT = 100000000L; // 1 DOGE minimum order. See recommendations:
|
||||||
|
// https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md
|
||||||
|
|
||||||
// Temporary values until a dynamic fee system is written.
|
// Temporary values until a dynamic fee system is written.
|
||||||
private static final long MAINNET_FEE = 110000000L;
|
private static final long MAINNET_FEE = 100000L;
|
||||||
private static final long NON_MAINNET_FEE = 10000L; // TODO: calibrate this
|
private static final long NON_MAINNET_FEE = 10000L; // TODO: calibrate this
|
||||||
|
|
||||||
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||||
|
@ -5,19 +5,7 @@ import java.math.BigDecimal;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.Scanner;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -50,6 +38,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
/** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */
|
/** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */
|
||||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||||
|
|
||||||
|
private static final int RESPONSE_TIME_READINGS = 5;
|
||||||
|
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||||
|
|
||||||
public static class Server {
|
public static class Server {
|
||||||
String hostname;
|
String hostname;
|
||||||
|
|
||||||
@ -57,6 +48,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
ConnectionType connectionType;
|
ConnectionType connectionType;
|
||||||
|
|
||||||
int port;
|
int port;
|
||||||
|
private List<Long> responseTimes = new ArrayList<>();
|
||||||
|
|
||||||
public Server(String hostname, ConnectionType connectionType, int port) {
|
public Server(String hostname, ConnectionType connectionType, int port) {
|
||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
@ -64,6 +56,25 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addResponseTime(long responseTime) {
|
||||||
|
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
|
||||||
|
this.responseTimes.remove(0);
|
||||||
|
}
|
||||||
|
this.responseTimes.add(responseTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long averageResponseTime() {
|
||||||
|
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
|
||||||
|
// Not enough readings yet
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
OptionalDouble average = this.responseTimes.stream().mapToDouble(a -> a).average();
|
||||||
|
if (average.isPresent()) {
|
||||||
|
return Double.valueOf(average.getAsDouble()).longValue();
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
if (other == this)
|
if (other == this)
|
||||||
@ -103,7 +114,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private Scanner scanner;
|
private Scanner scanner;
|
||||||
private int nextId = 1;
|
private int nextId = 1;
|
||||||
|
|
||||||
private static final int TX_CACHE_SIZE = 200;
|
private static final int TX_CACHE_SIZE = 1000;
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
|
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
|
||||||
// This method is called just after a new entry has been added
|
// This method is called just after a new entry has been added
|
||||||
@ -539,6 +550,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
|
|
||||||
while (haveConnection()) {
|
while (haveConnection()) {
|
||||||
Object response = connectedRpc(method, params);
|
Object response = connectedRpc(method, params);
|
||||||
|
|
||||||
|
// If we have more servers and this one replied slowly, try another
|
||||||
|
if (!this.remainingServers.isEmpty()) {
|
||||||
|
long averageResponseTime = this.currentServer.averageResponseTime();
|
||||||
|
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
|
||||||
|
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
|
||||||
|
this.closeServer();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (response != null)
|
if (response != null)
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
@ -628,6 +650,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
String request = requestJson.toJSONString() + "\n";
|
String request = requestJson.toJSONString() + "\n";
|
||||||
LOGGER.trace(() -> String.format("Request: %s", request));
|
LOGGER.trace(() -> String.format("Request: %s", request));
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
final String response;
|
final String response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -638,7 +661,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long responseTime = endTime-startTime;
|
||||||
|
|
||||||
LOGGER.trace(() -> String.format("Response: %s", response));
|
LOGGER.trace(() -> String.format("Response: %s", response));
|
||||||
|
LOGGER.trace(() -> String.format("Time taken: %dms", endTime-startTime));
|
||||||
|
|
||||||
if (response.isEmpty())
|
if (response.isEmpty())
|
||||||
// Empty response - try another server?
|
// Empty response - try another server?
|
||||||
@ -649,6 +676,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
// Unexpected response - try another server?
|
// Unexpected response - try another server?
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
// Keep track of response times
|
||||||
|
if (this.currentServer != null) {
|
||||||
|
this.currentServer.addResponseTime(responseTime);
|
||||||
|
}
|
||||||
|
|
||||||
JSONObject responseJson = (JSONObject) responseObj;
|
JSONObject responseJson = (JSONObject) responseObj;
|
||||||
|
|
||||||
Object errorObj = responseJson.get("error");
|
Object errorObj = responseJson.get("error");
|
||||||
|
@ -50,8 +50,12 @@ public class Litecoin extends Bitcoiny {
|
|||||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 50001),
|
||||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
|
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
|
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||||
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
|
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||||
|
new Server("electrum3.cipig.net", ConnectionType.TCP, 10063),
|
||||||
|
new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10063),
|
||||||
|
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||||
|
new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10063),
|
||||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||||
new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
|
new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package org.qortal.data.arbitrary;
|
||||||
|
|
||||||
|
import org.qortal.network.Peer;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ArbitraryRelayInfo {
|
||||||
|
|
||||||
|
private final String hash58;
|
||||||
|
private final String signature58;
|
||||||
|
private final Peer peer;
|
||||||
|
private final Long timestamp;
|
||||||
|
|
||||||
|
public ArbitraryRelayInfo(String hash58, String signature58, Peer peer, Long timestamp) {
|
||||||
|
this.hash58 = hash58;
|
||||||
|
this.signature58 = signature58;
|
||||||
|
this.peer = peer;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Peer getPeer() {
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 ArbitraryRelayInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ArbitraryRelayInfo otherRelayInfo = (ArbitraryRelayInfo) other;
|
||||||
|
|
||||||
|
return this.peer == otherRelayInfo.getPeer()
|
||||||
|
&& Objects.equals(this.hash58, otherRelayInfo.getHash58())
|
||||||
|
&& Objects.equals(this.signature58, otherRelayInfo.getSignature58());
|
||||||
|
}
|
||||||
|
}
|
@ -30,13 +30,21 @@ public class ArbitraryResourceStatus {
|
|||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
private Integer localChunkCount;
|
||||||
|
private Integer totalChunkCount;
|
||||||
|
|
||||||
public ArbitraryResourceStatus() {
|
public ArbitraryResourceStatus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArbitraryResourceStatus(Status status) {
|
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
||||||
this.id = status.toString();
|
this.id = status.toString();
|
||||||
this.title = status.title;
|
this.title = status.title;
|
||||||
this.description = status.description;
|
this.description = status.description;
|
||||||
|
this.localChunkCount = localChunkCount;
|
||||||
|
this.totalChunkCount = totalChunkCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ArbitraryResourceStatus(Status status) {
|
||||||
|
this(status, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ResourceList {
|
public class ResourceList {
|
||||||
@ -20,7 +21,7 @@ public class ResourceList {
|
|||||||
private static final Logger LOGGER = LogManager.getLogger(ResourceList.class);
|
private static final Logger LOGGER = LogManager.getLogger(ResourceList.class);
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private List<String> list = new ArrayList<>();
|
private List<String> list = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResourceList
|
* ResourceList
|
||||||
|
@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ public class ResourceListManager {
|
|||||||
private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class);
|
private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class);
|
||||||
|
|
||||||
private static ResourceListManager instance;
|
private static ResourceListManager instance;
|
||||||
private List<ResourceList> lists = new ArrayList<>();
|
private List<ResourceList> lists = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
|
||||||
public ResourceListManager() {
|
public ResourceListManager() {
|
||||||
|
@ -6,7 +6,7 @@ import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
|||||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataFileManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
@ -307,12 +307,7 @@ public class Network {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
return ArbitraryDataFileListManager.getInstance().fetchArbitraryDataFileList(connectedPeer, signature);
|
||||||
return ArbitraryDataFileManager.getInstance().fetchAllArbitraryDataFiles(repository, connectedPeer, signature);
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.info("Unable to fetch arbitrary data files");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1169,11 +1164,13 @@ public class Network {
|
|||||||
if (consecutiveReadings >= consecutiveReadingsRequired) {
|
if (consecutiveReadings >= consecutiveReadingsRequired) {
|
||||||
// Last 10 readings were the same - i.e. more than one peer agreed on the new IP address...
|
// Last 10 readings were the same - i.e. more than one peer agreed on the new IP address...
|
||||||
String ip = ipAddressHistory.get(size - 1);
|
String ip = ipAddressHistory.get(size - 1);
|
||||||
if (!Objects.equals(ip, this.ourExternalIpAddress)) {
|
if (ip != null && !Objects.equals(ip, "null")) {
|
||||||
// ... and the readings were different to our current recorded value, so
|
if (!Objects.equals(ip, this.ourExternalIpAddress)) {
|
||||||
// update our external IP address value
|
// ... and the readings were different to our current recorded value, so
|
||||||
this.ourExternalIpAddress = ip;
|
// update our external IP address value
|
||||||
this.onExternalIpUpdate(ip);
|
this.ourExternalIpAddress = ip;
|
||||||
|
this.onExternalIpUpdate(ip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,16 +473,18 @@ public class Peer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytesRead > 0) {
|
if (LOGGER.isTraceEnabled()) {
|
||||||
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
|
if (bytesRead > 0) {
|
||||||
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
|
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
|
||||||
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
|
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
|
||||||
|
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
|
||||||
|
|
||||||
LOGGER.trace("[{}] Received {} bytes, starting {}, into byteBuffer[{}] from peer {}",
|
LOGGER.trace("[{}] Received {} bytes, starting {}, into byteBuffer[{}] from peer {}",
|
||||||
this.peerConnectionId, bytesRead, leadingHex, priorPosition, this);
|
this.peerConnectionId, bytesRead, leadingHex, priorPosition, this);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.trace("[{}] Received {} bytes into byteBuffer[{}] from peer {}", this.peerConnectionId,
|
LOGGER.trace("[{}] Received {} bytes into byteBuffer[{}] from peer {}", this.peerConnectionId,
|
||||||
bytesRead, priorPosition, this);
|
bytesRead, priorPosition, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
||||||
|
|
||||||
|
@ -3,11 +3,14 @@ package org.qortal.network.message;
|
|||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import com.google.common.primitives.Longs;
|
import com.google.common.primitives.Longs;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.qortal.transform.Transformer.INT_LENGTH;
|
import static org.qortal.transform.Transformer.INT_LENGTH;
|
||||||
import static org.qortal.transform.Transformer.LONG_LENGTH;
|
import static org.qortal.transform.Transformer.LONG_LENGTH;
|
||||||
@ -15,19 +18,22 @@ import static org.qortal.transform.Transformer.LONG_LENGTH;
|
|||||||
public class GetArbitraryDataFileListMessage extends Message {
|
public class GetArbitraryDataFileListMessage extends Message {
|
||||||
|
|
||||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||||
|
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||||
|
|
||||||
private final byte[] signature;
|
private final byte[] signature;
|
||||||
|
private List<byte[]> hashes;
|
||||||
private final long requestTime;
|
private final long requestTime;
|
||||||
private int requestHops;
|
private int requestHops;
|
||||||
|
|
||||||
public GetArbitraryDataFileListMessage(byte[] signature, long requestTime, int requestHops) {
|
public GetArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, long requestTime, int requestHops) {
|
||||||
this(-1, signature, requestTime, requestHops);
|
this(-1, signature, hashes, requestTime, requestHops);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GetArbitraryDataFileListMessage(int id, byte[] signature, long requestTime, int requestHops) {
|
private GetArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, long requestTime, int requestHops) {
|
||||||
super(id, MessageType.GET_ARBITRARY_DATA_FILE_LIST);
|
super(id, MessageType.GET_ARBITRARY_DATA_FILE_LIST);
|
||||||
|
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
|
this.hashes = hashes;
|
||||||
this.requestTime = requestTime;
|
this.requestTime = requestTime;
|
||||||
this.requestHops = requestHops;
|
this.requestHops = requestHops;
|
||||||
}
|
}
|
||||||
@ -36,10 +42,11 @@ public class GetArbitraryDataFileListMessage extends Message {
|
|||||||
return this.signature;
|
return this.signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
public List<byte[]> getHashes() {
|
||||||
if (bytes.remaining() != SIGNATURE_LENGTH + LONG_LENGTH + INT_LENGTH)
|
return this.hashes;
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||||
|
|
||||||
bytes.get(signature);
|
bytes.get(signature);
|
||||||
@ -48,7 +55,23 @@ public class GetArbitraryDataFileListMessage extends Message {
|
|||||||
|
|
||||||
int requestHops = bytes.getInt();
|
int requestHops = bytes.getInt();
|
||||||
|
|
||||||
return new GetArbitraryDataFileListMessage(id, signature, requestTime, requestHops);
|
List<byte[]> hashes = null;
|
||||||
|
if (bytes.hasRemaining()) {
|
||||||
|
int hashCount = bytes.getInt();
|
||||||
|
|
||||||
|
if (bytes.remaining() != hashCount * HASH_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes = new ArrayList<>();
|
||||||
|
for (int i = 0; i < hashCount; ++i) {
|
||||||
|
byte[] hash = new byte[HASH_LENGTH];
|
||||||
|
bytes.get(hash);
|
||||||
|
hashes.add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -62,6 +85,14 @@ public class GetArbitraryDataFileListMessage extends Message {
|
|||||||
|
|
||||||
bytes.write(Ints.toByteArray(this.requestHops));
|
bytes.write(Ints.toByteArray(this.requestHops));
|
||||||
|
|
||||||
|
if (this.hashes != null) {
|
||||||
|
bytes.write(Ints.toByteArray(this.hashes.size()));
|
||||||
|
|
||||||
|
for (byte[] hash : this.hashes) {
|
||||||
|
bytes.write(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return bytes.toByteArray();
|
return bytes.toByteArray();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -15,7 +15,7 @@ import com.google.common.primitives.Ints;
|
|||||||
import com.google.common.primitives.Longs;
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
public class GetOnlineAccountsMessage extends Message {
|
public class GetOnlineAccountsMessage extends Message {
|
||||||
private static final int MAX_ACCOUNT_COUNT = 1000;
|
private static final int MAX_ACCOUNT_COUNT = 5000;
|
||||||
|
|
||||||
private List<OnlineAccountData> onlineAccounts;
|
private List<OnlineAccountData> onlineAccounts;
|
||||||
|
|
||||||
|
@ -145,20 +145,22 @@ public class BlockArchiveReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getFilenameForHeight(int height) {
|
private String getFilenameForHeight(int height) {
|
||||||
Iterator it = this.fileListCache.entrySet().iterator();
|
synchronized (this.fileListCache) {
|
||||||
while (it.hasNext()) {
|
Iterator it = this.fileListCache.entrySet().iterator();
|
||||||
Map.Entry pair = (Map.Entry)it.next();
|
while (it.hasNext()) {
|
||||||
if (pair == null && pair.getKey() == null && pair.getValue() == null) {
|
Map.Entry pair = (Map.Entry) it.next();
|
||||||
continue;
|
if (pair == null && pair.getKey() == null && pair.getValue() == null) {
|
||||||
}
|
continue;
|
||||||
Triple<Integer, Integer, Integer> heightInfo = (Triple<Integer, Integer, Integer>) pair.getValue();
|
}
|
||||||
Integer startHeight = heightInfo.getA();
|
Triple<Integer, Integer, Integer> heightInfo = (Triple<Integer, Integer, Integer>) pair.getValue();
|
||||||
Integer endHeight = heightInfo.getB();
|
Integer startHeight = heightInfo.getA();
|
||||||
|
Integer endHeight = heightInfo.getB();
|
||||||
|
|
||||||
if (height >= startHeight && height <= endHeight) {
|
if (height >= startHeight && height <= endHeight) {
|
||||||
// Found the correct file
|
// Found the correct file
|
||||||
String filename = (String) pair.getKey();
|
String filename = (String) pair.getKey();
|
||||||
return filename;
|
return filename;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.data.block.BlockArchiveData;
|
import org.qortal.data.block.BlockArchiveData;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
@ -100,7 +101,7 @@ public class BlockArchiveWriter {
|
|||||||
if (Controller.isStopping()) {
|
if (Controller.isStopping()) {
|
||||||
return BlockArchiveWriteResult.STOPPING;
|
return BlockArchiveWriteResult.STOPPING;
|
||||||
}
|
}
|
||||||
if (Controller.getInstance().isSynchronizing()) {
|
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +108,23 @@ public interface TransactionRepository {
|
|||||||
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||||
Integer minBlockHeight, Integer maxBlockHeight) throws DataException;
|
Integer minBlockHeight, Integer maxBlockHeight) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns signatures for transactions that match search criteria.
|
||||||
|
* <p>
|
||||||
|
* Alternate version that allows for custom where clauses and bind params.
|
||||||
|
* Only use for very specific use cases, such as the names integrity check.
|
||||||
|
* Not advised to be used otherwise, given that it could be possible for
|
||||||
|
* unsanitized inputs to be passed in if not careful.
|
||||||
|
*
|
||||||
|
* @param txType
|
||||||
|
* @param whereClauses
|
||||||
|
* @param bindParams
|
||||||
|
* @return
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public List<byte[]> getSignaturesMatchingCustomCriteria(TransactionType txType, List<String> whereClauses,
|
||||||
|
List<Object> bindParams) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns signature for latest auto-update transaction.
|
* Returns signature for latest auto-update transaction.
|
||||||
* <p>
|
* <p>
|
||||||
@ -125,6 +142,17 @@ public interface TransactionRepository {
|
|||||||
*/
|
*/
|
||||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns signatures for all name-registration related transactions relating to supplied name.
|
||||||
|
* Note: this does not currently include ARBITRARY data relating to the name.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param confirmationStatus
|
||||||
|
* @return
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public List<TransactionData> getTransactionsInvolvingName(String name, ConfirmationStatus confirmationStatus) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of transactions relating to specific asset ID.
|
* Returns list of transactions relating to specific asset ID.
|
||||||
*
|
*
|
||||||
|
@ -330,7 +330,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
|||||||
bindParams.add(name);
|
bindParams.add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" GROUP BY name, service, identifier ORDER BY name");
|
sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||||
|
|
||||||
if (reverse != null && reverse) {
|
if (reverse != null && reverse) {
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
@ -401,7 +401,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
|||||||
bindParams.add(queryWildcard);
|
bindParams.add(queryWildcard);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" GROUP BY name, service, identifier ORDER BY name");
|
sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||||
|
|
||||||
if (reverse != null && reverse) {
|
if (reverse != null && reverse) {
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
@ -465,7 +465,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
|||||||
sql.append(" AND (identifier = ? OR (? IS NULL))");
|
sql.append(" AND (identifier = ? OR (? IS NULL))");
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" GROUP BY name ORDER BY name");
|
sql.append(" GROUP BY name ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||||
|
|
||||||
if (reverse != null && reverse) {
|
if (reverse != null && reverse) {
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
|
@ -945,6 +945,20 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("CREATE INDEX ArbitraryPeersHashIndex ON ArbitraryPeers (hash)");
|
stmt.execute("CREATE INDEX ArbitraryPeersHashIndex ON ArbitraryPeers (hash)");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 40:
|
||||||
|
// For looking up name registration transactions based on name or reduced name
|
||||||
|
stmt.execute("CREATE INDEX RegisterNameNameIndex ON RegisterNameTransactions (name)");
|
||||||
|
stmt.execute("CREATE INDEX RegisterNameReducedNameIndex ON RegisterNameTransactions (reduced_name)");
|
||||||
|
// For looking up update name transactions based on name, new name, or new reduced name
|
||||||
|
stmt.execute("CREATE INDEX UpdateNameNameIndex ON UpdateNameTransactions (name)");
|
||||||
|
stmt.execute("CREATE INDEX UpdateNameNewNameIndex ON UpdateNameTransactions (new_name)");
|
||||||
|
stmt.execute("CREATE INDEX UpdateNameReducedNewNameIndex ON UpdateNameTransactions (reduced_new_name)");
|
||||||
|
// For looking up buy name transactions based on name
|
||||||
|
stmt.execute("CREATE INDEX BuyNameNameIndex ON BuyNameTransactions (name)");
|
||||||
|
// For looking up sell name transactions based on name
|
||||||
|
stmt.execute("CREATE INDEX SellNameNameIndex ON SellNameTransactions (name)");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -30,6 +30,7 @@ import org.qortal.repository.hsqldb.HSQLDBSaver;
|
|||||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.Unicode;
|
||||||
|
|
||||||
public class HSQLDBTransactionRepository implements TransactionRepository {
|
public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||||
|
|
||||||
@ -655,6 +656,44 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<byte[]> getSignaturesMatchingCustomCriteria(TransactionType txType, List<String> whereClauses,
|
||||||
|
List<Object> bindParams) throws DataException {
|
||||||
|
List<byte[]> signatures = new ArrayList<>();
|
||||||
|
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
sql.append(String.format("SELECT signature FROM %sTransactions", txType.className));
|
||||||
|
|
||||||
|
if (!whereClauses.isEmpty()) {
|
||||||
|
sql.append(" WHERE ");
|
||||||
|
|
||||||
|
final int whereClausesSize = whereClauses.size();
|
||||||
|
for (int wci = 0; wci < whereClausesSize; ++wci) {
|
||||||
|
if (wci != 0)
|
||||||
|
sql.append(" AND ");
|
||||||
|
|
||||||
|
sql.append(whereClauses.get(wci));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Transaction search SQL: %s", sql));
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return signatures;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] signature = resultSet.getBytes(1);
|
||||||
|
|
||||||
|
signatures.add(signature);
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return signatures;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch matching transaction signatures from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException {
|
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(1024);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
@ -700,6 +739,88 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TransactionData> getTransactionsInvolvingName(String name, ConfirmationStatus confirmationStatus) throws DataException {
|
||||||
|
TransactionType[] transactionTypes = new TransactionType[] {
|
||||||
|
REGISTER_NAME, UPDATE_NAME, BUY_NAME, SELL_NAME
|
||||||
|
}; // TODO: CancelSellNameTransaction?
|
||||||
|
|
||||||
|
String reducedName = Unicode.sanitize(name);
|
||||||
|
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
sql.append("SELECT Transactions.signature FROM Transactions");
|
||||||
|
|
||||||
|
for (int ti = 0; ti < transactionTypes.length; ++ti) {
|
||||||
|
sql.append(" LEFT OUTER JOIN ");
|
||||||
|
sql.append(transactionTypes[ti].className);
|
||||||
|
sql.append("Transactions USING (signature)");
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" WHERE Transactions.type IN (");
|
||||||
|
for (int ti = 0; ti < transactionTypes.length; ++ti) {
|
||||||
|
if (ti != 0)
|
||||||
|
sql.append(", ");
|
||||||
|
|
||||||
|
sql.append(transactionTypes[ti].value);
|
||||||
|
}
|
||||||
|
sql.append(")");
|
||||||
|
|
||||||
|
// Confirmation status
|
||||||
|
switch (confirmationStatus) {
|
||||||
|
case BOTH:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONFIRMED:
|
||||||
|
sql.append(" AND Transactions.block_height IS NOT NULL");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UNCONFIRMED:
|
||||||
|
sql.append(" AND Transactions.block_height IS NULL");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" AND (RegisterNameTransactions.name = ?");
|
||||||
|
bindParams.add(name);
|
||||||
|
sql.append(" OR RegisterNameTransactions.reduced_name = ?");
|
||||||
|
bindParams.add(reducedName);
|
||||||
|
sql.append(" OR UpdateNameTransactions.name = ?");
|
||||||
|
bindParams.add(name);
|
||||||
|
sql.append(" OR (UpdateNameTransactions.reduced_new_name != '' AND UpdateNameTransactions.reduced_new_name = ?)");
|
||||||
|
bindParams.add(reducedName);
|
||||||
|
sql.append(" OR UpdateNameTransactions.new_name = ?");
|
||||||
|
bindParams.add(name);
|
||||||
|
sql.append(" OR SellNameTransactions.name = ?");
|
||||||
|
bindParams.add(name);
|
||||||
|
sql.append(" OR BuyNameTransactions.name = ?");
|
||||||
|
bindParams.add(name);
|
||||||
|
|
||||||
|
sql.append(") GROUP BY Transactions.signature, Transactions.created_when ORDER BY Transactions.created_when");
|
||||||
|
|
||||||
|
List<TransactionData> transactions = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return transactions;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] signature = resultSet.getBytes(1);
|
||||||
|
|
||||||
|
TransactionData transactionData = this.fromSignature(signature);
|
||||||
|
|
||||||
|
if (transactionData == null)
|
||||||
|
// Something inconsistent with the repository
|
||||||
|
throw new DataException("Unable to fetch name-related transaction from repository?");
|
||||||
|
|
||||||
|
transactions.add(transactionData);
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
} catch (SQLException | DataException e) {
|
||||||
|
throw new DataException("Unable to fetch name-related transactions from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||||
throws DataException {
|
throws DataException {
|
||||||
|
@ -202,9 +202,9 @@ public class Settings {
|
|||||||
private boolean allowConnectionsWithOlderPeerVersions = true;
|
private boolean allowConnectionsWithOlderPeerVersions = true;
|
||||||
|
|
||||||
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
|
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||||
private int minPeerConnectionTime = 2 * 60; // seconds
|
private int minPeerConnectionTime = 5 * 60; // seconds
|
||||||
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
|
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||||
private int maxPeerConnectionTime = 20 * 60; // seconds
|
private int maxPeerConnectionTime = 60 * 60; // seconds
|
||||||
|
|
||||||
/** Whether to sync multiple blocks at once in normal operation */
|
/** Whether to sync multiple blocks at once in normal operation */
|
||||||
private boolean fastSyncEnabled = true;
|
private boolean fastSyncEnabled = true;
|
||||||
@ -243,7 +243,8 @@ public class Settings {
|
|||||||
private String[] bootstrapHosts = new String[] {
|
private String[] bootstrapHosts = new String[] {
|
||||||
"http://bootstrap.qortal.org",
|
"http://bootstrap.qortal.org",
|
||||||
"http://bootstrap2.qortal.org",
|
"http://bootstrap2.qortal.org",
|
||||||
"http://cinfu1.crowetic.com"
|
"http://81.169.136.59",
|
||||||
|
"http://62.171.190.193"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-update sources
|
// Auto-update sources
|
||||||
@ -308,6 +309,9 @@ public class Settings {
|
|||||||
/** Maximum total size of hosted data, in bytes. Unlimited if null */
|
/** Maximum total size of hosted data, in bytes. Unlimited if null */
|
||||||
private Long maxStorageCapacity = null;
|
private Long maxStorageCapacity = null;
|
||||||
|
|
||||||
|
/** Whether to serve QDN data without authentication */
|
||||||
|
private boolean qdnAuthBypassEnabled = false;
|
||||||
|
|
||||||
// Domain mapping
|
// Domain mapping
|
||||||
public static class DomainMap {
|
public static class DomainMap {
|
||||||
private String domain;
|
private String domain;
|
||||||
@ -884,4 +888,8 @@ public class Settings {
|
|||||||
public Long getMaxStorageCapacity() {
|
public Long getMaxStorageCapacity() {
|
||||||
return this.maxStorageCapacity;
|
return this.maxStorageCapacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isQDNAuthBypassEnabled() {
|
||||||
|
return this.qdnAuthBypassEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,15 @@ public class RegisterNameTransaction extends Transaction {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUnitFee(Long timestamp) {
|
||||||
|
// Use a higher unit fee after the fee increase timestamp
|
||||||
|
if (timestamp > BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp()) {
|
||||||
|
return BlockChain.getInstance().getNameRegistrationUnitFee();
|
||||||
|
}
|
||||||
|
return BlockChain.getInstance().getUnitFee();
|
||||||
|
}
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
|
||||||
public Account getRegistrant() {
|
public Account getRegistrant() {
|
||||||
|
@ -334,7 +334,7 @@ public abstract class Transaction {
|
|||||||
|
|
||||||
/** Returns whether transaction's fee is at least minimum unit fee as specified in blockchain config. */
|
/** Returns whether transaction's fee is at least minimum unit fee as specified in blockchain config. */
|
||||||
public boolean hasMinimumFee() {
|
public boolean hasMinimumFee() {
|
||||||
return this.transactionData.getFee() >= BlockChain.getInstance().getUnitFee();
|
return this.transactionData.getFee() >= this.getUnitFee(this.transactionData.getTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long feePerByte() {
|
public long feePerByte() {
|
||||||
@ -347,7 +347,7 @@ public abstract class Transaction {
|
|||||||
|
|
||||||
/** Returns whether transaction's fee is at least amount needed to cover byte-length of transaction. */
|
/** Returns whether transaction's fee is at least amount needed to cover byte-length of transaction. */
|
||||||
public boolean hasMinimumFeePerByte() {
|
public boolean hasMinimumFeePerByte() {
|
||||||
long unitFee = BlockChain.getInstance().getUnitFee();
|
long unitFee = this.getUnitFee(this.transactionData.getTimestamp());
|
||||||
int maxBytePerUnitFee = BlockChain.getInstance().getMaxBytesPerUnitFee();
|
int maxBytePerUnitFee = BlockChain.getInstance().getMaxBytesPerUnitFee();
|
||||||
|
|
||||||
// If the unit fee is zero, any fee is enough to cover the byte-length of the transaction
|
// If the unit fee is zero, any fee is enough to cover the byte-length of the transaction
|
||||||
@ -369,7 +369,18 @@ public abstract class Transaction {
|
|||||||
|
|
||||||
int unitFeeCount = ((dataLength - 1) / maxBytePerUnitFee) + 1;
|
int unitFeeCount = ((dataLength - 1) / maxBytePerUnitFee) + 1;
|
||||||
|
|
||||||
return BlockChain.getInstance().getUnitFee() * unitFeeCount;
|
return this.getUnitFee(this.transactionData.getTimestamp()) * unitFeeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate unit fee for a given transaction type
|
||||||
|
*
|
||||||
|
* FUTURE: add "accountLevel" parameter if needed - the level of the transaction creator
|
||||||
|
* @param timestamp - the transaction's timestamp (currently not used)
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public long getUnitFee(Long timestamp) {
|
||||||
|
return BlockChain.getInstance().getUnitFee();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -382,7 +393,7 @@ public abstract class Transaction {
|
|||||||
* @return transaction version number
|
* @return transaction version number
|
||||||
*/
|
*/
|
||||||
public static int getVersionByTimestamp(long timestamp) {
|
public static int getVersionByTimestamp(long timestamp) {
|
||||||
if (timestamp >= 1642176000000L) {
|
if (timestamp >= BlockChain.getInstance().getTransactionV5Timestamp()) {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
return 4;
|
return 4;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.utils;
|
package org.qortal.utils;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||||
@ -147,16 +148,49 @@ public class ArbitraryTransactionUtils {
|
|||||||
byte[] metadataHash = transactionData.getMetadataHash();
|
byte[] metadataHash = transactionData.getMetadataHash();
|
||||||
byte[] signature = transactionData.getSignature();
|
byte[] signature = transactionData.getSignature();
|
||||||
|
|
||||||
if (metadataHash == null) {
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||||
// This file doesn't have any metadata, therefore it has no chunks
|
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||||
|
|
||||||
|
// Find the folder containing the files
|
||||||
|
Path parentPath = arbitraryDataFile.getFilePath().getParent();
|
||||||
|
String[] files = parentPath.toFile().list();
|
||||||
|
if (files == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the original copy indicator file if it exists
|
||||||
|
files = ArrayUtils.removeElement(files, ".original");
|
||||||
|
|
||||||
|
int count = files.length;
|
||||||
|
|
||||||
|
// If the complete file exists (and this transaction has chunks), subtract it from the count
|
||||||
|
if (arbitraryDataFile.chunkCount() > 0 && arbitraryDataFile.exists()) {
|
||||||
|
// We are only measuring the individual chunks, not the joined file
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int totalChunkCount(ArbitraryTransactionData transactionData) throws DataException {
|
||||||
|
if (transactionData == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] digest = transactionData.getData();
|
||||||
|
byte[] metadataHash = transactionData.getMetadataHash();
|
||||||
|
byte[] signature = transactionData.getSignature();
|
||||||
|
|
||||||
|
if (metadataHash == null) {
|
||||||
|
// This file doesn't have any metadata, therefore it has a single (complete) chunk
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Load complete file and chunks
|
// Load complete file and chunks
|
||||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||||
|
|
||||||
return arbitraryDataFile.chunkCount();
|
return arbitraryDataFile.chunkCount() + 1; // +1 for the metadata file
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isFileRecent(Path filePath, long now, long cleanupAfter) {
|
public static boolean isFileRecent(Path filePath, long now, long cleanupAfter) {
|
||||||
|
@ -4,10 +4,14 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.001",
|
"unitFee": "0.001",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
|
"nameRegistrationUnitFeeTimestamp": 1645372800000,
|
||||||
"useBrokenMD160ForAddresses": false,
|
"useBrokenMD160ForAddresses": false,
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"defaultGroupId": 0,
|
"defaultGroupId": 0,
|
||||||
"oneNamePerAccount": true,
|
"oneNamePerAccount": true,
|
||||||
|
"minAccountLevelToMint": 1,
|
||||||
|
"minAccountLevelForBlockSubmissions": 5,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 6,
|
"maxRewardSharesPerMintingAccount": 6,
|
||||||
"founderEffectiveMintingLevel": 10,
|
"founderEffectiveMintingLevel": 10,
|
||||||
@ -51,7 +55,8 @@
|
|||||||
"atFindNextTransactionFix": 275000,
|
"atFindNextTransactionFix": 275000,
|
||||||
"newBlockSigHeight": 320000,
|
"newBlockSigHeight": 320000,
|
||||||
"shareBinFix": 399000,
|
"shareBinFix": 399000,
|
||||||
"calcChainWeightTimestamp": 1620579600000
|
"calcChainWeightTimestamp": 1620579600000,
|
||||||
|
"transactionV5Timestamp": 1642176000000
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
var url = host + '/arbitrary/resource/status/' + service + '/' + name + '?build=true';
|
var url = host + '/arbitrary/resource/status/' + service + '/' + name + '?build=true';
|
||||||
var textStatus = "Loading...";
|
var textStatus = "Loading...";
|
||||||
|
var textProgress = "";
|
||||||
var retryInterval = 2500;
|
var retryInterval = 2500;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@ -82,7 +83,7 @@
|
|||||||
textStatus = status.description;
|
textStatus = status.description;
|
||||||
retryInterval = 1000;
|
retryInterval = 1000;
|
||||||
}
|
}
|
||||||
else if (status.status == "DOWNLOADING") {
|
else if (status.id == "DOWNLOADING") {
|
||||||
textStatus = status.description;
|
textStatus = status.description;
|
||||||
retryInterval = 1000;
|
retryInterval = 1000;
|
||||||
}
|
}
|
||||||
@ -96,7 +97,12 @@
|
|||||||
textStatus = status.description;
|
textStatus = status.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.localChunkCount != null && status.totalChunkCount != null) {
|
||||||
|
textProgress = "Files downloaded: " + status.localChunkCount + " / " + status.totalChunkCount;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("status").innerHTML = textStatus;
|
document.getElementById("status").innerHTML = textStatus;
|
||||||
|
document.getElementById("progress").innerHTML = textProgress;
|
||||||
|
|
||||||
setTimeout(checkStatus, retryInterval);
|
setTimeout(checkStatus, retryInterval);
|
||||||
}
|
}
|
||||||
@ -260,6 +266,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
This page will refresh automatically when the content becomes available.
|
This page will refresh automatically when the content becomes available.
|
||||||
</p>
|
</p>
|
||||||
<p><span id="status">Loading...</span></p>
|
<p><span id="status">Loading...</span></p>
|
||||||
|
<p><span id="progress"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@ package org.qortal.test.api;
|
|||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.qortal.api.resource.AdminResource;
|
import org.qortal.api.resource.AdminResource;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.test.common.ApiCommon;
|
import org.qortal.test.common.ApiCommon;
|
||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
|
|
||||||
@ -29,7 +31,10 @@ public class AdminApiTests extends ApiCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSummary() {
|
public void testSummary() throws IllegalAccessException {
|
||||||
|
// Set localAuthBypassEnabled to true, since we don't need to test authentication here
|
||||||
|
FieldUtils.writeField(Settings.getInstance(), "localAuthBypassEnabled", true, true);
|
||||||
|
|
||||||
assertNotNull(this.adminResource.summary("testApiKey"));
|
assertNotNull(this.adminResource.summary("testApiKey"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
@ -76,7 +77,8 @@ public class BlockApiTests extends ApiCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetBlockByTimestamp() {
|
@Ignore(value = "Doesn't work, to be fixed later")
|
||||||
|
public void testGetBlockByTimestamp() throws DataException {
|
||||||
assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis(), false));
|
assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis(), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ import org.qortal.test.common.ApiCommon;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class NamesApiTests extends ApiCommon {
|
public class NamesApiTests extends ApiCommon {
|
||||||
|
|
||||||
@ -47,6 +49,7 @@ public class NamesApiTests extends ApiCommon {
|
|||||||
String name = "test-name";
|
String name = "test-name";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null));
|
assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null));
|
||||||
@ -62,6 +65,7 @@ public class NamesApiTests extends ApiCommon {
|
|||||||
String name = "test-name";
|
String name = "test-name";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
assertNotNull(this.namesResource.getName(name));
|
assertNotNull(this.namesResource.getName(name));
|
||||||
@ -77,6 +81,7 @@ public class NamesApiTests extends ApiCommon {
|
|||||||
long price = 1_23456789L;
|
long price = 1_23456789L;
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Sell-name
|
// Sell-name
|
||||||
|
@ -8,6 +8,7 @@ import org.qortal.test.common.Common;
|
|||||||
|
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -38,7 +39,9 @@ public class ArbitraryDataDigestTests extends Common {
|
|||||||
// Write a random file to .qortal/cache to ensure it isn't being included in the digest function
|
// Write a random file to .qortal/cache to ensure it isn't being included in the digest function
|
||||||
// We exclude all .qortal files from the digest since they can be different with each build, and
|
// We exclude all .qortal files from the digest since they can be different with each build, and
|
||||||
// we only care about the actual user files
|
// we only care about the actual user files
|
||||||
FileWriter fileWriter = new FileWriter(Paths.get(dataPath.toString(), ".qortal", "cache").toString());
|
Path cachePath = Paths.get(dataPath.toString(), ".qortal", "cache");
|
||||||
|
Files.createDirectories(cachePath.getParent());
|
||||||
|
FileWriter fileWriter = new FileWriter(cachePath.toString());
|
||||||
fileWriter.append(UUID.randomUUID().toString());
|
fileWriter.append(UUID.randomUUID().toString());
|
||||||
fileWriter.close();
|
fileWriter.close();
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import org.qortal.test.common.ArbitraryUtils;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -153,6 +154,7 @@ public class ArbitraryDataStorageCapacityTests extends Common {
|
|||||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
String aliceName = "alice";
|
String aliceName = "alice";
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), aliceName, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), aliceName, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
Path alicePath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
Path alicePath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||||
ArbitraryDataFile aliceArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(alice.getPublicKey()), alicePath, aliceName, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
|
ArbitraryDataFile aliceArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(alice.getPublicKey()), alicePath, aliceName, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
|
||||||
@ -161,6 +163,7 @@ public class ArbitraryDataStorageCapacityTests extends Common {
|
|||||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
String bobName = "bob";
|
String bobName = "bob";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), bobName, "");
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), bobName, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, bob);
|
TransactionUtils.signAndMint(repository, transactionData, bob);
|
||||||
Path bobPath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
Path bobPath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||||
ArbitraryDataFile bobArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(bob.getPublicKey()), bobPath, bobName, identifier, ArbitraryTransactionData.Method.PUT, service, bob, chunkSize);
|
ArbitraryDataFile bobArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(bob.getPublicKey()), bobPath, bobName, identifier, ArbitraryTransactionData.Method.PUT, service, bob, chunkSize);
|
||||||
|
@ -21,7 +21,9 @@ import org.qortal.settings.Settings;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -59,25 +61,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
|||||||
String name = "Test";
|
String name = "Test";
|
||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||||
|
|
||||||
// Add name to followed list
|
// Add name to followed list
|
||||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store and pre-fetch data for this transaction
|
// We should store and pre-fetch data for this transaction
|
||||||
assertEquals(StoragePolicy.FOLLOWED_OR_VIEWED, Settings.getInstance().getStoragePolicy());
|
assertEquals(StoragePolicy.FOLLOWED_OR_VIEWED, Settings.getInstance().getStoragePolicy());
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
|
|
||||||
// Now unfollow the name
|
// Now unfollow the name
|
||||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store but not pre-fetch data for this transaction
|
// We should store but not pre-fetch data for this transaction
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,25 +96,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
|||||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "FOLLOWED", true);
|
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "FOLLOWED", true);
|
||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||||
|
|
||||||
// Add name to followed list
|
// Add name to followed list
|
||||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store and pre-fetch data for this transaction
|
// We should store and pre-fetch data for this transaction
|
||||||
assertEquals(StoragePolicy.FOLLOWED, Settings.getInstance().getStoragePolicy());
|
assertEquals(StoragePolicy.FOLLOWED, Settings.getInstance().getStoragePolicy());
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
|
|
||||||
// Now unfollow the name
|
// Now unfollow the name
|
||||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||||
|
|
||||||
// We shouldn't store or pre-fetch data for this transaction
|
// We shouldn't store or pre-fetch data for this transaction
|
||||||
assertFalse(storageManager.canStoreData(transactionData));
|
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,25 +131,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
|||||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "VIEWED", true);
|
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "VIEWED", true);
|
||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||||
|
|
||||||
// Add name to followed list
|
// Add name to followed list
|
||||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store but not pre-fetch data for this transaction
|
// We should store but not pre-fetch data for this transaction
|
||||||
assertEquals(StoragePolicy.VIEWED, Settings.getInstance().getStoragePolicy());
|
assertEquals(StoragePolicy.VIEWED, Settings.getInstance().getStoragePolicy());
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
|
|
||||||
// Now unfollow the name
|
// Now unfollow the name
|
||||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store but not pre-fetch data for this transaction
|
// We should store but not pre-fetch data for this transaction
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,25 +166,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
|||||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "ALL", true);
|
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "ALL", true);
|
||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||||
|
|
||||||
// Add name to followed list
|
// Add name to followed list
|
||||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store and pre-fetch data for this transaction
|
// We should store and pre-fetch data for this transaction
|
||||||
assertEquals(StoragePolicy.ALL, Settings.getInstance().getStoragePolicy());
|
assertEquals(StoragePolicy.ALL, Settings.getInstance().getStoragePolicy());
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
|
|
||||||
// Now unfollow the name
|
// Now unfollow the name
|
||||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||||
|
|
||||||
// We should store and pre-fetch data for this transaction
|
// We should store and pre-fetch data for this transaction
|
||||||
assertTrue(storageManager.canStoreData(transactionData));
|
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,25 +201,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
|||||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "NONE", true);
|
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "NONE", true);
|
||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||||
|
|
||||||
// Add name to followed list
|
// Add name to followed list
|
||||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||||
|
|
||||||
// We shouldn't store or pre-fetch data for this transaction
|
// We shouldn't store or pre-fetch data for this transaction
|
||||||
assertEquals(StoragePolicy.NONE, Settings.getInstance().getStoragePolicy());
|
assertEquals(StoragePolicy.NONE, Settings.getInstance().getStoragePolicy());
|
||||||
assertFalse(storageManager.canStoreData(transactionData));
|
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
|
|
||||||
// Now unfollow the name
|
// Now unfollow the name
|
||||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||||
|
|
||||||
// We shouldn't store or pre-fetch data for this transaction
|
// We shouldn't store or pre-fetch data for this transaction
|
||||||
assertFalse(storageManager.canStoreData(transactionData));
|
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,9 @@ import org.qortal.test.common.ArbitraryUtils;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -55,6 +57,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -149,6 +152,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -181,6 +185,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -226,6 +231,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -294,6 +300,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -343,6 +350,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -380,6 +388,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -409,6 +418,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
@ -435,6 +445,7 @@ public class ArbitraryDataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
|
@ -21,7 +21,9 @@ import org.qortal.test.common.ArbitraryUtils;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -51,6 +53,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Create PUT transaction
|
// Create PUT transaction
|
||||||
|
@ -19,7 +19,9 @@ import org.qortal.test.common.Common;
|
|||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
import org.qortal.transaction.ArbitraryTransaction;
|
import org.qortal.transaction.ArbitraryTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -46,6 +48,7 @@ public class ArbitraryTransactionTests extends Common {
|
|||||||
|
|
||||||
// Register the name to Alice
|
// Register the name to Alice
|
||||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||||
|
|
||||||
// Set difficulty to 1
|
// Set difficulty to 1
|
||||||
|
@ -20,7 +20,9 @@ import org.qortal.test.common.BlockUtils;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.utils.Amounts;
|
import org.qortal.utils.Amounts;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class BuySellTests extends Common {
|
public class BuySellTests extends Common {
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ public class BuySellTests extends Common {
|
|||||||
public void testRegisterName() throws DataException {
|
public void testRegisterName() throws DataException {
|
||||||
// Register-name
|
// Register-name
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
String name = transactionData.getName();
|
String name = transactionData.getName();
|
||||||
|
@ -11,7 +11,11 @@ import org.qortal.repository.RepositoryManager;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
@ -31,6 +35,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -45,6 +50,96 @@ public class IntegrityTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBlankReducedName() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
// Register-name
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String name = "\uD83E\uDD73"; // Translates to a reducedName of ""
|
||||||
|
String data = "\uD83E\uDD73";
|
||||||
|
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Ensure the name exists and the data is correct
|
||||||
|
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||||
|
|
||||||
|
// Ensure the reducedName is blank
|
||||||
|
assertEquals("", repository.getNameRepository().fromName(name).getReducedName());
|
||||||
|
|
||||||
|
// Run the database integrity check for this name
|
||||||
|
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||||
|
assertEquals(1, integrityCheck.rebuildName(name, repository));
|
||||||
|
|
||||||
|
// Ensure the name still exists and the data is still correct
|
||||||
|
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||||
|
assertEquals("", repository.getNameRepository().fromName(name).getReducedName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateWithBlankNewName() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
// Register-name to Alice
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String name = "initial_name";
|
||||||
|
String data = "initial_data";
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Update the name, but keep the new name blank
|
||||||
|
String newName = "";
|
||||||
|
String newData = "updated_data";
|
||||||
|
UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
|
||||||
|
TransactionUtils.signAndMint(repository, updateTransactionData, alice);
|
||||||
|
|
||||||
|
// Ensure the original name exists and the data is correct
|
||||||
|
assertEquals(name, repository.getNameRepository().fromName(name).getName());
|
||||||
|
assertEquals(newData, repository.getNameRepository().fromName(name).getData());
|
||||||
|
|
||||||
|
// Run the database integrity check for this name
|
||||||
|
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||||
|
assertEquals(2, integrityCheck.rebuildName(name, repository));
|
||||||
|
|
||||||
|
// Ensure the name still exists and the data is still correct
|
||||||
|
assertEquals(name, repository.getNameRepository().fromName(name).getName());
|
||||||
|
assertEquals(newData, repository.getNameRepository().fromName(name).getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateWithBlankNewNameAndBlankEmojiName() throws DataException {
|
||||||
|
// Attempt to simulate a real world problem where an emoji with blank reducedName
|
||||||
|
// confused the integrity check by associating it with previous UPDATE_NAME transactions
|
||||||
|
// due to them also having a blank "newReducedName"
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
// Register-name to Alice
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String name = "initial_name";
|
||||||
|
String data = "initial_data";
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Update the name, but keep the new name blank
|
||||||
|
String newName = "";
|
||||||
|
String newData = "updated_data";
|
||||||
|
UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
|
||||||
|
TransactionUtils.signAndMint(repository, updateTransactionData, alice);
|
||||||
|
|
||||||
|
// Register emoji name
|
||||||
|
String emojiName = "\uD83E\uDD73"; // Translates to a reducedName of ""
|
||||||
|
|
||||||
|
// Ensure that the initial_name isn't associated with the emoji name
|
||||||
|
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||||
|
List<TransactionData> transactions = namesDatabaseIntegrityCheck.fetchAllTransactionsInvolvingName(emojiName, repository);
|
||||||
|
assertEquals(0, transactions.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMissingName() throws DataException {
|
public void testMissingName() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -54,6 +149,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -83,6 +179,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -121,6 +218,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -146,6 +244,7 @@ public class IntegrityTests extends Common {
|
|||||||
|
|
||||||
// Attempt to register the new name
|
// Attempt to register the new name
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, data);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(alice);
|
transaction.sign(alice);
|
||||||
|
|
||||||
@ -165,6 +264,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -179,6 +279,7 @@ public class IntegrityTests extends Common {
|
|||||||
// Attempt to register the name again
|
// Attempt to register the name again
|
||||||
String duplicateName = "TEST-nÁme";
|
String duplicateName = "TEST-nÁme";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(alice);
|
transaction.sign(alice);
|
||||||
|
|
||||||
@ -198,6 +299,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -231,6 +333,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -240,6 +343,7 @@ public class IntegrityTests extends Common {
|
|||||||
String secondName = "new-missing-name";
|
String secondName = "new-missing-name";
|
||||||
String secondNameData = "{\"data2\":true}";
|
String secondNameData = "{\"data2\":true}";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, secondNameData);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, secondNameData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the second name exists and the data is correct
|
// Ensure the second name exists and the data is correct
|
||||||
@ -273,6 +377,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -304,6 +409,7 @@ public class IntegrityTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
|
@ -3,20 +3,26 @@ package org.qortal.test.naming;
|
|||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.controller.BlockMinter;
|
import org.qortal.controller.BlockMinter;
|
||||||
import org.qortal.data.transaction.*;
|
import org.qortal.data.transaction.*;
|
||||||
import org.qortal.naming.Name;
|
import org.qortal.naming.Name;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.test.common.*;
|
import org.qortal.test.common.*;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class MiscTests extends Common {
|
public class MiscTests extends Common {
|
||||||
|
|
||||||
@ -34,6 +40,7 @@ public class MiscTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
List<String> recentNames = repository.getNameRepository().getRecentNames(0L);
|
List<String> recentNames = repository.getNameRepository().getRecentNames(0L);
|
||||||
@ -53,11 +60,13 @@ public class MiscTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// duplicate
|
// duplicate
|
||||||
String duplicateName = "TEST-nÁme";
|
String duplicateName = "TEST-nÁme";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(alice);
|
transaction.sign(alice);
|
||||||
|
|
||||||
@ -76,12 +85,14 @@ public class MiscTests extends Common {
|
|||||||
String data = "{}";
|
String data = "{}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// duplicate (this time registered by Bob)
|
// duplicate (this time registered by Bob)
|
||||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
String duplicateName = "TEST-nÁme";
|
String duplicateName = "TEST-nÁme";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), duplicateName, data);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), duplicateName, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(alice);
|
transaction.sign(alice);
|
||||||
|
|
||||||
@ -100,12 +111,14 @@ public class MiscTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Register another name that we will later attempt to rename to first name (above)
|
// Register another name that we will later attempt to rename to first name (above)
|
||||||
String otherName = "new-name";
|
String otherName = "new-name";
|
||||||
String otherData = "";
|
String otherData = "";
|
||||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData);
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// we shouldn't be able to update name to existing name
|
// we shouldn't be able to update name to existing name
|
||||||
@ -129,6 +142,7 @@ public class MiscTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(alice);
|
transaction.sign(alice);
|
||||||
|
|
||||||
@ -147,6 +161,7 @@ public class MiscTests extends Common {
|
|||||||
String data = "{\"age\":30}";
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// we shouldn't be able to update name to an address
|
// we shouldn't be able to update name to an address
|
||||||
@ -175,6 +190,7 @@ public class MiscTests extends Common {
|
|||||||
|
|
||||||
// Register the name
|
// Register the name
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -201,6 +217,7 @@ public class MiscTests extends Common {
|
|||||||
|
|
||||||
// Register the name
|
// Register the name
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -252,6 +269,7 @@ public class MiscTests extends Common {
|
|||||||
|
|
||||||
// Register the name
|
// Register the name
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Ensure the name exists and the data is correct
|
// Ensure the name exists and the data is correct
|
||||||
@ -283,6 +301,7 @@ public class MiscTests extends Common {
|
|||||||
|
|
||||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
|
||||||
// Ensure the name doesn't exist
|
// Ensure the name doesn't exist
|
||||||
assertNull(repository.getNameRepository().fromName(name));
|
assertNull(repository.getNameRepository().fromName(name));
|
||||||
@ -304,4 +323,54 @@ public class MiscTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test name registration fee increase
|
||||||
|
@Test
|
||||||
|
public void testRegisterNameFeeIncrease() throws DataException, IllegalAccessException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Set nameRegistrationUnitFeeTimestamp to a time far in the future
|
||||||
|
long futureTimestamp = 9999999999999L; // 20 Nov 2286
|
||||||
|
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFeeTimestamp", futureTimestamp, true);
|
||||||
|
assertEquals(futureTimestamp, BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp());
|
||||||
|
|
||||||
|
// Validate unit fees pre and post timestamp
|
||||||
|
assertEquals(10000000, BlockChain.getInstance().getUnitFee()); // 0.1 QORT
|
||||||
|
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFee()); // 5 QORT
|
||||||
|
|
||||||
|
// Register-name
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String name = "test-name";
|
||||||
|
String data = "{\"age\":30}";
|
||||||
|
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
assertEquals(10000000L, transactionData.getFee().longValue());
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Set nameRegistrationUnitFeeTimestamp to a time in the past
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFeeTimestamp", now - 1000L, true);
|
||||||
|
assertEquals(now - 1000L, BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp());
|
||||||
|
|
||||||
|
// Register a different name
|
||||||
|
// First try with the default unit fee
|
||||||
|
String name2 = "test-name-2";
|
||||||
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, data);
|
||||||
|
assertEquals(10000000L, transactionData.getFee().longValue());
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(alice);
|
||||||
|
ValidationResult result = transaction.importAsUnconfirmed();
|
||||||
|
assertTrue("Transaction should be invalid", ValidationResult.INSUFFICIENT_FEE == result);
|
||||||
|
|
||||||
|
// Now try using correct fee (this is specified by the UI, via the /transaction/unitfee API endpoint)
|
||||||
|
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, data);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
|
assertEquals(500000000L, transactionData.getFee().longValue());
|
||||||
|
transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(alice);
|
||||||
|
result = transaction.importAsUnconfirmed();
|
||||||
|
assertTrue("Transaction should be valid", ValidationResult.OK == result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import org.qortal.test.common.BlockUtils;
|
|||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
import org.qortal.test.common.TransactionUtils;
|
import org.qortal.test.common.TransactionUtils;
|
||||||
import org.qortal.test.common.transaction.TestTransaction;
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class UpdateTests extends Common {
|
public class UpdateTests extends Common {
|
||||||
|
|
||||||
@ -34,6 +36,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||||
|
|
||||||
// Check name, reduced name, and data exist
|
// Check name, reduced name, and data exist
|
||||||
@ -100,6 +103,7 @@ public class UpdateTests extends Common {
|
|||||||
String constantReducedName = "initia1-name";
|
String constantReducedName = "initia1-name";
|
||||||
|
|
||||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
@ -147,6 +151,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
@ -225,6 +230,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
@ -282,6 +288,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
@ -323,6 +330,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
@ -385,6 +393,7 @@ public class UpdateTests extends Common {
|
|||||||
String initialData = "{\"age\":30}";
|
String initialData = "{\"age\":30}";
|
||||||
|
|
||||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
// Check initial name exists
|
// Check initial name exists
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 6,
|
"shareBinFix": 6,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"maxBlockSize": 2097152,
|
"maxBlockSize": 2097152,
|
||||||
"maxBytesPerUnitFee": 1024,
|
"maxBytesPerUnitFee": 1024,
|
||||||
"unitFee": "0.1",
|
"unitFee": "0.1",
|
||||||
|
"nameRegistrationUnitFee": "5",
|
||||||
"requireGroupForApproval": false,
|
"requireGroupForApproval": false,
|
||||||
"minAccountLevelToRewardShare": 5,
|
"minAccountLevelToRewardShare": 5,
|
||||||
"maxRewardSharesPerMintingAccount": 20,
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -15,6 +15,5 @@
|
|||||||
"tempDataPath": "data-test/_temp",
|
"tempDataPath": "data-test/_temp",
|
||||||
"listsPath": "lists-test",
|
"listsPath": "lists-test",
|
||||||
"storagePolicy": "FOLLOWED_OR_VIEWED",
|
"storagePolicy": "FOLLOWED_OR_VIEWED",
|
||||||
"maxStorageCapacity": 104857600,
|
"maxStorageCapacity": 104857600
|
||||||
"localAuthBypassEnabled": true
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ fi
|
|||||||
|
|
||||||
printf "Searching for auto-update transactions to approve...\n";
|
printf "Searching for auto-update transactions to approve...\n";
|
||||||
|
|
||||||
tx=$( curl --silent --url "http://localhost:${port}/arbitrary/search?txGroupId=1&service=1&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
|
tx=$( curl --silent --url "http://localhost:${port}/arbitrary/search?txGroupId=1&service=AUTO_UPDATE&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
|
||||||
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
|
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
|
Loading…
Reference in New Issue
Block a user