Merge branch 'master' into qdn-metadata

# Conflicts:
#	src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java
This commit is contained in:
CalDescent 2022-02-13 15:50:12 +00:00
commit 4821139501
85 changed files with 2558 additions and 915 deletions

26
Dockerfile Normal file
View 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"]

View File

@ -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

View File

@ -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(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(appDir + &quot;log4j2.properties&quot;, appDir + &quot;log4j2-orig.properties&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite %AppDir%\log4j2.properties to update logfile storage path&#13;&#10;var fin = fso.OpenTextFile(appDir + &quot;log4j2-orig.properties&quot;, ForReading, false); // no create&#13;&#10;var fout = fso.OpenTextFile(appDir + &quot;log4j2.properties&quot;, ForWriting, true); // can create&#13;&#10;&#13;&#10;// Copy lines with rewriting where necessary&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;var line = fin.ReadLine();&#13;&#10;&#13;&#10;&#9;var start = line.indexOf(&quot;property.dirname&quot;);&#13;&#10;&#9;if (start &gt; 0) {&#13;&#10;&#9;&#9;// line: # property.dirname = ...appdata...&#13;&#10;&#9;&#9;// uncomment/replace this line for Windows&#13;&#10;&#9;&#9;fout.WriteLine( &quot;property.dirname = &quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) );&#13;&#10;&#9;} else {&#13;&#10;&#9;&#9;// not found - output verbatim&#13;&#10;&#9;&#9;fout.WriteLine( line );&#13;&#10;&#9;}&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_4"/> <ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(appDir + &quot;log4j2.properties&quot;, appDir + &quot;log4j2-orig.properties&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite %AppDir%\log4j2.properties to update logfile storage path&#13;&#10;var fin = fso.OpenTextFile(appDir + &quot;log4j2-orig.properties&quot;, ForReading, false); // no create&#13;&#10;var fout = fso.OpenTextFile(appDir + &quot;log4j2.properties&quot;, ForWriting, true); // can create&#13;&#10;&#13;&#10;// Copy lines with rewriting where necessary&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;var line = fin.ReadLine();&#13;&#10;&#13;&#10;&#9;var start = line.indexOf(&quot;property.dirname&quot;);&#13;&#10;&#9;if (start &gt; 0) {&#13;&#10;&#9;&#9;// line: # property.dirname = ...appdata...&#13;&#10;&#9;&#9;// uncomment/replace this line for Windows&#13;&#10;&#9;&#9;fout.WriteLine( &quot;property.dirname = &quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) );&#13;&#10;&#9;} else {&#13;&#10;&#9;&#9;// not found - output verbatim&#13;&#10;&#9;&#9;fout.WriteLine( line );&#13;&#10;&#9;}&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_4"/>
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_3"/> <ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;dataPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;data\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" 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"/>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

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

View File

@ -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;
} }
/** /**

View File

@ -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())) {

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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

View File

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

View File

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

View File

@ -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");

View File

@ -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),

View File

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

View File

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

View File

@ -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

View File

@ -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() {

View File

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

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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.
* *

View File

@ -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");

View File

@ -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;

View File

@ -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 {

View File

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

View File

@ -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() {

View File

@ -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;

View File

@ -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) {

View File

@ -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,

View File

@ -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>

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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
} }

View File

@ -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