diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip
index 2a63f092..d2300a0b 100755
--- a/WindowsInstaller/Qortal.aip
+++ b/WindowsInstaller/Qortal.aip
@@ -19,10 +19,10 @@
-
+
-
+
@@ -174,7 +174,7 @@
-
+
diff --git a/lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar b/lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
new file mode 100644
index 00000000..5e7c3677
Binary files /dev/null and b/lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar differ
diff --git a/lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom b/lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
new file mode 100644
index 00000000..106adc38
--- /dev/null
+++ b/lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
@@ -0,0 +1,9 @@
+
+
+ 4.0.0
+ org.ciyam
+ AT
+ 1.3.8
+ POM was created from install:install-file
+
diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml
index ff99d403..8f8b1f6e 100644
--- a/lib/org/ciyam/AT/maven-metadata-local.xml
+++ b/lib/org/ciyam/AT/maven-metadata-local.xml
@@ -3,13 +3,14 @@
org.ciyamAT
- 1.3.7
+ 1.3.81.3.41.3.51.3.61.3.7
+ 1.3.8
- 20200812131412
+ 20200925114415
diff --git a/pom.xml b/pom.xml
index 3986e8e9..03ee9fdd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,20 +3,19 @@
4.0.0org.qortalqortal
- 1.3.5
+ 1.3.7jarbf9fb800.15.61.64${maven.build.timestamp}
- 1.3.7
+ 1.3.83.61.81.2.228.1-jre
- 2.5.0-fixed
- 2.5.0
+ 2.5.12.29.19.4.29.v202005212.12.1
@@ -407,12 +406,6 @@
hsqldb${hsqldb.version}
-
- org.hsqldb
- sqltool
- ${hsqldb-sqltool.version}
- test
- org.ciyam
diff --git a/src/main/java/org/qortal/api/Security.java b/src/main/java/org/qortal/api/Security.java
index 2449f781..448f951a 100644
--- a/src/main/java/org/qortal/api/Security.java
+++ b/src/main/java/org/qortal/api/Security.java
@@ -5,10 +5,20 @@ import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
-public class Security {
+import org.qortal.settings.Settings;
+
+public abstract class Security {
+
+ public static final String API_KEY_HEADER = "X-API-KEY";
- // TODO: replace with proper authentication
public static void checkApiCallAllowed(HttpServletRequest request) {
+ String expectedApiKey = Settings.getInstance().getApiKey();
+ String passedApiKey = request.getHeader(API_KEY_HEADER);
+
+ if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) ||
+ (passedApiKey != null && !passedApiKey.equals(expectedApiKey)))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
+
InetAddress remoteAddr;
try {
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
@@ -19,4 +29,5 @@ public class Security {
if (!remoteAddr.isLoopbackAddress())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
+
}
diff --git a/src/main/java/org/qortal/api/model/ActivitySummary.java b/src/main/java/org/qortal/api/model/ActivitySummary.java
index 27b5ed8d..e8e9a3aa 100644
--- a/src/main/java/org/qortal/api/model/ActivitySummary.java
+++ b/src/main/java/org/qortal/api/model/ActivitySummary.java
@@ -1,5 +1,6 @@
package org.qortal.api.model;
+import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
@@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ActivitySummary {
- public int blockCount;
- public int transactionCount;
- public int assetsIssued;
- public int namesRegistered;
+ private int blockCount;
+ private int assetsIssued;
+ private int namesRegistered;
// Assuming TransactionType values are contiguous so 'length' equals count
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
- public Map transactionCountByType = new EnumMap<>(TransactionType.class);
+ private Map transactionCountByType = new EnumMap<>(TransactionType.class);
+ private int totalTransactionCount = 0;
public ActivitySummary() {
// Needed for JAXB
}
+ public int getBlockCount() {
+ return this.blockCount;
+ }
+
+ public void setBlockCount(int blockCount) {
+ this.blockCount = blockCount;
+ }
+
+ public int getTotalTransactionCount() {
+ return this.totalTransactionCount;
+ }
+
+ public int getAssetsIssued() {
+ return this.assetsIssued;
+ }
+
+ public void setAssetsIssued(int assetsIssued) {
+ this.assetsIssued = assetsIssued;
+ }
+
+ public int getNamesRegistered() {
+ return this.namesRegistered;
+ }
+
+ public void setNamesRegistered(int namesRegistered) {
+ this.namesRegistered = namesRegistered;
+ }
+
+ public Map getTransactionCountByType() {
+ return Collections.unmodifiableMap(this.transactionCountByType);
+ }
+
+ public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
+ this.transactionCountByType.put(transactionType, transactionCount);
+
+ this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
+ }
+
+ public void setTransactionCountByType(Map transactionCountByType) {
+ this.transactionCountByType = new EnumMap<>(transactionCountByType);
+
+ this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
+ }
+
}
diff --git a/src/main/java/org/qortal/api/model/NodeInfo.java b/src/main/java/org/qortal/api/model/NodeInfo.java
index 86ed6971..16a4df75 100644
--- a/src/main/java/org/qortal/api/model/NodeInfo.java
+++ b/src/main/java/org/qortal/api/model/NodeInfo.java
@@ -11,6 +11,7 @@ public class NodeInfo {
public String buildVersion;
public long buildTimestamp;
public String nodeId;
+ public boolean isTestNet;
public NodeInfo() {
}
diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java
index 39b4bd71..20e4da5a 100644
--- a/src/main/java/org/qortal/api/resource/AddressesResource.java
+++ b/src/main/java/org/qortal/api/resource/AddressesResource.java
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
@@ -473,6 +474,7 @@ public class AddressesResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String computePublicize(String rawBytes58) {
Security.checkApiCallAllowed(request);
diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java
index 7b07551a..f24389bf 100644
--- a/src/main/java/org/qortal/api/resource/AdminResource.java
+++ b/src/main/java/org/qortal/api/resource/AdminResource.java
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
@@ -40,7 +41,6 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ActivitySummary;
@@ -57,6 +57,7 @@ import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -118,6 +119,7 @@ public class AdminResource {
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
+ nodeInfo.isTestNet = Settings.getInstance().isTestNet();
return nodeInfo;
}
@@ -132,6 +134,7 @@ public class AdminResource {
)
}
)
+ @SecurityRequirement(name = "apiKey")
public NodeStatus status() {
Security.checkApiCallAllowed(request);
@@ -152,6 +155,7 @@ public class AdminResource {
)
}
)
+ @SecurityRequirement(name = "apiKey")
public String shutdown() {
Security.checkApiCallAllowed(request);
@@ -180,7 +184,10 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public ActivitySummary summary() {
+ Security.checkApiCallAllowed(request);
+
ActivitySummary summary = new ActivitySummary();
LocalDate date = LocalDate.now();
@@ -192,16 +199,13 @@ public class AdminResource {
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
int endHeight = repository.getBlockRepository().getBlockchainHeight();
- summary.blockCount = endHeight - startHeight;
+ summary.setBlockCount(endHeight - startHeight);
- summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
+ summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
- for (Integer count : summary.transactionCountByType.values())
- summary.transactionCount += count;
+ summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
- summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
-
- summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
+ summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
return summary;
} catch (DataException e) {
@@ -209,6 +213,30 @@ public class AdminResource {
}
}
+ @GET
+ @Path("/enginestats")
+ @Operation(
+ summary = "Fetch statistics snapshot for core engine",
+ responses = {
+ @ApiResponse(
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ array = @ArraySchema(
+ schema = @Schema(
+ implementation = Controller.StatsSnapshot.class
+ )
+ )
+ )
+ )
+ }
+ )
+ @SecurityRequirement(name = "apiKey")
+ public Controller.StatsSnapshot getEngineStats() {
+ Security.checkApiCallAllowed(request);
+
+ return Controller.getInstance().getStatsSnapshot();
+ }
+
@GET
@Path("/mintingaccounts")
@Operation(
@@ -221,6 +249,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public List getMintingAccounts() {
Security.checkApiCallAllowed(request);
@@ -267,6 +296,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
+ @SecurityRequirement(name = "apiKey")
public String addMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
@@ -319,6 +349,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String deleteMintingAccount(String key58) {
Security.checkApiCallAllowed(request);
@@ -418,6 +449,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String orphan(String targetHeightString) {
Security.checkApiCallAllowed(request);
@@ -435,8 +467,6 @@ public class AdminResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
- } catch (ApiException e) {
- throw e;
}
}
@@ -461,6 +491,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String forceSync(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
@@ -492,8 +523,6 @@ public class AdminResource {
return syncResult.name();
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
- } catch (ApiException e) {
- throw e;
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (InterruptedException e) {
@@ -501,4 +530,151 @@ public class AdminResource {
}
}
+ @GET
+ @Path("/repository/data")
+ @Operation(
+ summary = "Export sensitive/node-local data from repository.",
+ description = "Exports data to .script files on local machine"
+ )
+ @ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public String exportRepository() {
+ Security.checkApiCallAllowed(request);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+
+ blockchainLock.lockInterruptibly();
+
+ try {
+ repository.exportNodeLocalData();
+ return "true";
+ } finally {
+ blockchainLock.unlock();
+ }
+ } catch (InterruptedException e) {
+ // We couldn't lock blockchain to perform export
+ return "false";
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
+ @POST
+ @Path("/repository/data")
+ @Operation(
+ summary = "Import data into repository.",
+ description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
+ requestBody = @RequestBody(
+ required = true,
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "string", example = "MintingAccounts.script"
+ )
+ )
+ ),
+ responses = {
+ @ApiResponse(
+ description = "\"true\"",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public String importRepository(String filename) {
+ Security.checkApiCallAllowed(request);
+
+ // Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
+ if (Settings.getInstance().getApiKey() == null)
+ filename = "import.script";
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+
+ blockchainLock.lockInterruptibly();
+
+ try {
+ repository.importDataFromFile(filename);
+ repository.saveChanges();
+
+ return "true";
+ } finally {
+ blockchainLock.unlock();
+ }
+ } catch (InterruptedException e) {
+ // We couldn't lock blockchain to perform import
+ return "false";
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
+ @POST
+ @Path("/repository/checkpoint")
+ @Operation(
+ summary = "Checkpoint data in repository.",
+ description = "Forces repository to checkpoint uncommitted writes.",
+ responses = {
+ @ApiResponse(
+ description = "\"true\"",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public String checkpointRepository() {
+ Security.checkApiCallAllowed(request);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+
+ blockchainLock.lockInterruptibly();
+
+ try {
+ repository.checkpoint(true);
+ repository.saveChanges();
+
+ return "true";
+ } finally {
+ blockchainLock.unlock();
+ }
+ } catch (InterruptedException e) {
+ // We couldn't lock blockchain to perform checkpoint
+ return "false";
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
+ @DELETE
+ @Path("/repository")
+ @Operation(
+ summary = "Perform maintenance on repository.",
+ description = "Requires enough free space to rebuild repository. This will pause your node for a while."
+ )
+ @ApiErrors({ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public void performRepositoryMaintenance() {
+ Security.checkApiCallAllowed(request);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+
+ blockchainLock.lockInterruptibly();
+
+ try {
+ repository.performPeriodicMaintenance();
+ } finally {
+ blockchainLock.unlock();
+ }
+ } catch (InterruptedException e) {
+ // No big deal
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
}
diff --git a/src/main/java/org/qortal/api/resource/ApiDefinition.java b/src/main/java/org/qortal/api/resource/ApiDefinition.java
index ae7de00c..f9ec7459 100644
--- a/src/main/java/org/qortal/api/resource/ApiDefinition.java
+++ b/src/main/java/org/qortal/api/resource/ApiDefinition.java
@@ -1,11 +1,17 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import io.swagger.v3.oas.annotations.security.SecuritySchemes;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.qortal.api.Security;
+
@OpenAPIDefinition(
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
tags = {
@@ -30,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
})
}
)
+@SecuritySchemes({
+ @SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
+ @SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
+})
public class ApiDefinition {
}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java
index 9a8fc8d5..6ad7d6ea 100644
--- a/src/main/java/org/qortal/api/resource/ChatResource.java
+++ b/src/main/java/org/qortal/api/resource/ChatResource.java
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
@@ -156,6 +157,7 @@ public class ChatResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String buildChat(ChatTransactionData transactionData) {
Security.checkApiCallAllowed(request);
@@ -203,6 +205,7 @@ public class ChatResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String buildChat(String rawBytes58) {
Security.checkApiCallAllowed(request);
diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java
index a5a21336..47c0ed62 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
@@ -224,6 +225,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request);
diff --git a/src/main/java/org/qortal/api/resource/PeersResource.java b/src/main/java/org/qortal/api/resource/PeersResource.java
index 80cb5fa5..70f0e3e9 100644
--- a/src/main/java/org/qortal/api/resource/PeersResource.java
+++ b/src/main/java/org/qortal/api/resource/PeersResource.java
@@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -26,10 +29,17 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ConnectedPeer;
+import org.qortal.controller.Controller;
+import org.qortal.controller.Synchronizer;
+import org.qortal.controller.Synchronizer.SynchronizationResult;
+import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Network;
+import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
@@ -122,6 +132,7 @@ public class PeersResource {
)
}
)
+ @SecurityRequirement(name = "apiKey")
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
@@ -159,6 +170,7 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
+ @SecurityRequirement(name = "apiKey")
public String addPeer(String address) {
Security.checkApiCallAllowed(request);
@@ -213,6 +225,7 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
+ @SecurityRequirement(name = "apiKey")
public String removePeer(String address) {
Security.checkApiCallAllowed(request);
@@ -248,6 +261,7 @@ public class PeersResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
+ @SecurityRequirement(name = "apiKey")
public String removeKnownPeers(String address) {
Security.checkApiCallAllowed(request);
@@ -260,4 +274,68 @@ public class PeersResource {
}
}
+ @POST
+ @Path("/commonblock")
+ @Operation(
+ summary = "Report common block with given peer.",
+ requestBody = @RequestBody(
+ required = true,
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "string", example = "node2.qortal.org"
+ )
+ )
+ ),
+ responses = {
+ @ApiResponse(
+ description = "the block",
+ content = @Content(
+ array = @ArraySchema(
+ schema = @Schema(
+ implementation = BlockSummaryData.class
+ )
+ )
+ )
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public List commonBlock(String targetPeerAddress) {
+ Security.checkApiCallAllowed(request);
+
+ try {
+ // Try to resolve passed address to make things easier
+ PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
+ InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
+
+ List peers = Network.getInstance().getHandshakedPeers();
+ Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
+
+ if (targetPeer == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ int ourInitialHeight = Controller.getInstance().getChainHeight();
+ boolean force = true;
+ List peerBlockSummaries = new ArrayList<>();
+
+ SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
+ if (findCommonBlockResult != SynchronizationResult.OK)
+ return null;
+
+ return peerBlockSummaries;
+ }
+ } catch (IllegalArgumentException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
+ } catch (UnknownHostException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ }
+
}
diff --git a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
index 683258f9..24d00a4c 100644
--- a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
+++ b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
@@ -257,7 +257,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
if (crossChainTradeData.mode == AcctMode.OFFERING)
// We want when trade was created, not when it was last updated
- atStateTimestamp = atState.getCreation();
+ atStateTimestamp = crossChainTradeData.creationTimestamp;
else
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java
index 5aba1b73..e82ab14e 100644
--- a/src/main/java/org/qortal/at/AT.java
+++ b/src/main/java/org/qortal/at/AT.java
@@ -51,16 +51,17 @@ public class AT {
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
- byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
+ byte[] codeBytes = machineState.getCodeBytes();
+ byte[] codeHash = Crypto.digest(codeBytes);
- this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
+ this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
- this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
+ this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
}
// Getters / setters
@@ -106,12 +107,11 @@ public class AT {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
}
- long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
long atFees = api.calcFinalFees(state);
- this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
+ this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
return api.getTransactions();
}
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index e1273072..b977a613 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -6,6 +6,8 @@ import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -15,6 +17,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
@@ -791,15 +794,46 @@ public class Block {
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
}
- public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List blockSummaries) {
+ public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List blockSummaries, int maxHeight) {
BigInteger cumulativeWeight = BigInteger.ZERO;
int parentHeight = commonBlockHeight;
byte[] parentBlockSignature = commonBlockSignature;
+ NumberFormat formatter = new DecimalFormat("0.###E0");
+ boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
for (BlockSummaryData blockSummaryData : blockSummaries) {
- cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData));
+ StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
+
+ if (isLogging)
+ stringBuilder.append(formatter.format(cumulativeWeight)).append(" -> ");
+
+ cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT);
+ if (isLogging)
+ stringBuilder.append(formatter.format(cumulativeWeight)).append(" + ");
+
+ BigInteger blockWeight = calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
+ if (isLogging)
+ stringBuilder.append("(height: ")
+ .append(parentHeight + 1)
+ .append(", online: ")
+ .append(blockSummaryData.getOnlineAccountsCount())
+ .append(") ")
+ .append(formatter.format(blockWeight));
+
+ cumulativeWeight = cumulativeWeight.add(blockWeight);
+ if (isLogging)
+ stringBuilder.append(" -> ").append(formatter.format(cumulativeWeight));
+
+ if (isLogging && blockSummaries.size() > 1)
+ LOGGER.debug(() -> stringBuilder.toString()); //NOSONAR S1612 (false positive?)
+
parentHeight = blockSummaryData.getHeight();
parentBlockSignature = blockSummaryData.getSignature();
+
+ /* Potential future consensus change: only comparing the same number of blocks.
+ if (parentHeight >= maxHeight)
+ break;
+ */
}
return cumulativeWeight;
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index 8f6ddab7..e631f930 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -531,7 +531,8 @@ public class BlockChain {
private static void rebuildBlockchain() throws DataException {
// (Re)build repository
- RepositoryManager.rebuild();
+ if (!RepositoryManager.wasPristineAtOpen())
+ RepositoryManager.rebuild();
try (final Repository repository = RepositoryManager.getRepository()) {
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
@@ -567,7 +568,8 @@ public class BlockChain {
--height;
orphanBlockData = repository.getBlockRepository().fromHeight(height);
- Controller.getInstance().onNewBlock(orphanBlockData);
+ repository.discardChanges(); // clear transaction status to prevent deadlocks
+ Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
return true;
diff --git a/src/main/java/org/qortal/controller/AtStatesTrimmer.java b/src/main/java/org/qortal/controller/AtStatesTrimmer.java
new file mode 100644
index 00000000..f141abd7
--- /dev/null
+++ b/src/main/java/org/qortal/controller/AtStatesTrimmer.java
@@ -0,0 +1,77 @@
+package org.qortal.controller;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.data.block.BlockData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+import org.qortal.utils.NTP;
+
+public class AtStatesTrimmer implements Runnable {
+
+ private static final Logger LOGGER = LogManager.getLogger(AtStatesTrimmer.class);
+
+ @Override
+ public void run() {
+ Thread.currentThread().setName("AT States trimmer");
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ repository.getATRepository().prepareForAtStateTrimming();
+ repository.saveChanges();
+
+ while (!Controller.isStopping()) {
+ repository.discardChanges();
+
+ Thread.sleep(Settings.getInstance().getAtStatesTrimInterval());
+
+ BlockData chainTip = Controller.getInstance().getChainTip();
+ if (chainTip == null || NTP.getTime() == null)
+ continue;
+
+ // Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
+ if (Controller.getInstance().isSynchronizing())
+ continue;
+
+ long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
+ // We want to keep AT states near the tip of our copy of blockchain so we can process/orphan nearby blocks
+ long chainTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
+
+ long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
+ int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
+
+ int trimStartHeight = repository.getATRepository().getAtTrimHeight();
+
+ int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
+ int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
+
+ if (trimStartHeight >= upperTrimHeight)
+ continue;
+
+ int numAtStatesTrimmed = repository.getATRepository().trimAtStates(trimStartHeight, upperTrimHeight, Settings.getInstance().getAtStatesTrimLimit());
+ repository.saveChanges();
+
+ if (numAtStatesTrimmed > 0) {
+ LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
+ numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
+ trimStartHeight, upperTrimHeight));
+ } else {
+ // Can we move onto next batch?
+ if (upperTrimmableHeight > upperBatchHeight) {
+ repository.getATRepository().setAtTrimHeight(upperBatchHeight);
+ repository.getATRepository().prepareForAtStateTrimming();
+ repository.saveChanges();
+
+ LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
+ }
+ }
+ }
+ } catch (DataException e) {
+ LOGGER.warn(String.format("Repository issue trying to trim AT states: %s", e.getMessage()));
+ } catch (InterruptedException e) {
+ // Time to exit
+ }
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index aa80246d..46a29cf9 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -306,8 +306,10 @@ public class BlockMinter extends Thread {
}
if (newBlockMinted) {
- BlockData newBlockData = newBlock.getBlockData();
// Notify Controller and broadcast our new chain to network
+ BlockData newBlockData = newBlock.getBlockData();
+
+ repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(newBlockData);
Network network = Network.getInstance();
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index c9510742..de1be71e 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -16,16 +16,24 @@ import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
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.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -87,6 +95,7 @@ import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.utils.Base58;
import org.qortal.utils.ByteArray;
+import org.qortal.utils.DaemonThreadFactory;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
@@ -134,11 +143,24 @@ public class Controller extends Thread {
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
private volatile boolean notifyGroupMembershipChange = false;
- private volatile BlockData chainTip = null;
+ private static final int BLOCK_CACHE_SIZE = 10; // To cover typical Synchronizer request + a few spare
+ /** Latest blocks on our chain. Note: tail/last is the latest block. */
+ private final Deque latestBlocks = new LinkedList<>();
+
+ /** Cache of BlockMessages, indexed by block signature */
+ @SuppressWarnings("serial")
+ private final LinkedHashMap blockMessageCache = new LinkedHashMap<>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return this.size() > BLOCK_CACHE_SIZE;
+ }
+ };
private long repositoryBackupTimestamp = startTime; // ms
+ private long repositoryCheckpointTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
+
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
/** Whether we can mint new blocks, as reported by BlockMinter. */
@@ -181,6 +203,47 @@ public class Controller extends Thread {
/** Cache of latest blocks' online accounts */
Deque> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
+ // Stats
+ @XmlAccessorType(XmlAccessType.FIELD)
+ public static class StatsSnapshot {
+ public static class GetBlockMessageStats {
+ public AtomicLong requests = new AtomicLong();
+ public AtomicLong cacheHits = new AtomicLong();
+ public AtomicLong unknownBlocks = new AtomicLong();
+ public AtomicLong cacheFills = new AtomicLong();
+
+ public GetBlockMessageStats() {
+ }
+ }
+ public GetBlockMessageStats getBlockMessageStats = new GetBlockMessageStats();
+
+ public static class GetBlockSummariesStats {
+ public AtomicLong requests = new AtomicLong();
+ public AtomicLong cacheHits = new AtomicLong();
+ public AtomicLong fullyFromCache = new AtomicLong();
+
+ public GetBlockSummariesStats() {
+ }
+ }
+ public GetBlockSummariesStats getBlockSummariesStats = new GetBlockSummariesStats();
+
+ public static class GetBlockSignaturesV2Stats {
+ public AtomicLong requests = new AtomicLong();
+ public AtomicLong cacheHits = new AtomicLong();
+ public AtomicLong fullyFromCache = new AtomicLong();
+
+ public GetBlockSignaturesV2Stats() {
+ }
+ }
+ public GetBlockSignaturesV2Stats getBlockSignaturesV2Stats = new GetBlockSignaturesV2Stats();
+
+ public AtomicLong latestBlocksCacheRefills = new AtomicLong();
+
+ public StatsSnapshot() {
+ }
+ }
+ private final StatsSnapshot stats = new StatsSnapshot();
+
// Constructors
private Controller(String[] args) {
@@ -236,21 +299,36 @@ public class Controller extends Thread {
/** Returns current blockchain height, or 0 if it's not available. */
public int getChainHeight() {
- BlockData blockData = this.chainTip;
- if (blockData == null)
- return 0;
+ synchronized (this.latestBlocks) {
+ BlockData blockData = this.latestBlocks.peekLast();
+ if (blockData == null)
+ return 0;
- return blockData.getHeight();
+ return blockData.getHeight();
+ }
}
/** Returns highest block, or null if it's not available. */
public BlockData getChainTip() {
- return this.chainTip;
+ synchronized (this.latestBlocks) {
+ return this.latestBlocks.peekLast();
+ }
}
- /** Cache new blockchain tip. */
- public void setChainTip(BlockData blockData) {
- this.chainTip = blockData;
+ public void refillLatestBlocksCache() throws DataException {
+ // Set initial chain height/tip
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
+
+ synchronized (this.latestBlocks) {
+ this.latestBlocks.clear();
+
+ for (int i = 0; i < BLOCK_CACHE_SIZE && blockData != null; ++i) {
+ this.latestBlocks.addFirst(blockData);
+ blockData = repository.getBlockRepository().fromHeight(blockData.getHeight() - 1);
+ }
+ }
+ }
}
public ReentrantLock getBlockchainLock() {
@@ -332,13 +410,8 @@ public class Controller extends Thread {
try {
BlockChain.validate();
- // Set initial chain height/tip
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().getLastBlock();
-
- Controller.getInstance().setChainTip(blockData);
- LOGGER.info(String.format("Our chain height at start-up: %d", blockData.getHeight()));
- }
+ Controller.getInstance().refillLatestBlocksCache();
+ LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
} catch (DataException e) {
LOGGER.error("Couldn't validate blockchain", e);
Gui.getInstance().fatalError("Blockchain validation issue", e);
@@ -413,6 +486,11 @@ public class Controller extends Thread {
Thread.currentThread().setName("Controller");
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
+ final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
+
+ ExecutorService trimExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory());
+ trimExecutor.execute(new AtStatesTrimmer());
+ trimExecutor.execute(new OnlineAccountsSignaturesTrimmer());
try {
while (!isStopping) {
@@ -454,6 +532,18 @@ public class Controller extends Thread {
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
arbitraryDataRequests.entrySet().removeIf(entry -> entry.getValue().getC() < requestMinimumTimestamp);
+ // Time to 'checkpoint' uncommitted repository writes?
+ if (now >= repositoryCheckpointTimestamp + repositoryCheckpointInterval) {
+ repositoryCheckpointTimestamp = now + repositoryCheckpointInterval;
+
+ if (Settings.getInstance().getShowCheckpointNotification())
+ SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "DB_CHECKPOINT"),
+ Translator.INSTANCE.translate("SysTray", "PERFORMING_DB_CHECKPOINT"),
+ MessageType.INFO);
+
+ RepositoryManager.checkpoint(true);
+ }
+
// Give repository a chance to backup (if enabled)
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
repositoryBackupTimestamp = now + repositoryBackupInterval;
@@ -486,7 +576,17 @@ public class Controller extends Thread {
}
}
} catch (InterruptedException e) {
+ // Clear interrupted flag so we can shutdown trim threads
+ Thread.interrupted();
// Fall-through to exit
+ } finally {
+ trimExecutor.shutdownNow();
+
+ try {
+ trimExecutor.awaitTermination(2L, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ // We tried...
+ }
}
}
@@ -556,9 +656,10 @@ public class Controller extends Thread {
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
boolean hasStatusChanged = false;
+ BlockData priorChainTip = this.getChainTip();
synchronized (this.syncLock) {
- this.syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
+ this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
// Only update SysTray if we're potentially changing height
if (this.syncPercent < 100) {
@@ -570,8 +671,6 @@ public class Controller extends Thread {
if (hasStatusChanged)
updateSysTray();
- BlockData priorChainTip = this.chainTip;
-
try {
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
switch (syncResult) {
@@ -640,9 +739,6 @@ public class Controller extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
- // Update chain-tip, systray, notify peers, websockets, etc.
- this.onNewBlock(newChainTip);
-
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
}
@@ -827,15 +923,109 @@ public class Controller extends Thread {
}
}
+ /**
+ * Callback for when we've received a new block.
+ *
+ * See WARNING for {@link EventBus#notify(Event)}
+ * to prevent deadlocks.
+ */
public void onNewBlock(BlockData latestBlockData) {
// Protective copy
BlockData blockDataCopy = new BlockData(latestBlockData);
- this.setChainTip(blockDataCopy);
+ synchronized (this.latestBlocks) {
+ BlockData cachedChainTip = this.latestBlocks.peekLast();
+
+ if (cachedChainTip != null && Arrays.equals(cachedChainTip.getSignature(), blockDataCopy.getReference())) {
+ // Chain tip is parent for new latest block, so we can safely add new latest block
+ this.latestBlocks.addLast(latestBlockData);
+
+ // Trim if necessary
+ if (this.latestBlocks.size() >= BLOCK_CACHE_SIZE)
+ this.latestBlocks.pollFirst();
+ } else {
+ if (cachedChainTip != null)
+ // Chain tip didn't match - potentially abnormal behaviour?
+ LOGGER.debug(() -> String.format("Cached chain tip %.8s not parent for new latest block %.8s (reference %.8s)",
+ Base58.encode(cachedChainTip.getSignature()),
+ Base58.encode(blockDataCopy.getSignature()),
+ Base58.encode(blockDataCopy.getReference())));
+
+ // Defensively rebuild cache
+ try {
+ this.stats.latestBlocksCacheRefills.incrementAndGet();
+
+ this.refillLatestBlocksCache();
+ } catch (DataException e) {
+ LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
+ }
+ }
+ }
+
+ this.onNewOrOrphanedBlock(blockDataCopy, NewBlockEvent::new);
+ }
+
+ public static class OrphanedBlockEvent implements Event {
+ private final BlockData blockData;
+
+ public OrphanedBlockEvent(BlockData blockData) {
+ this.blockData = blockData;
+ }
+
+ public BlockData getBlockData() {
+ return this.blockData;
+ }
+ }
+
+ /**
+ * Callback for when we've orphaned a block.
+ *
+ * See WARNING for {@link EventBus#notify(Event)}
+ * to prevent deadlocks.
+ */
+ public void onOrphanedBlock(BlockData latestBlockData) {
+ // Protective copy
+ BlockData blockDataCopy = new BlockData(latestBlockData);
+
+ synchronized (this.latestBlocks) {
+ BlockData cachedChainTip = this.latestBlocks.pollLast();
+ boolean refillNeeded = false;
+
+ if (cachedChainTip != null && Arrays.equals(cachedChainTip.getReference(), blockDataCopy.getSignature())) {
+ // Chain tip was parent for new latest block that has been orphaned, so we're good
+
+ // However, if we've emptied the cache then we will need to refill it
+ refillNeeded = this.latestBlocks.isEmpty();
+ } else {
+ if (cachedChainTip != null)
+ // Chain tip didn't match - potentially abnormal behaviour?
+ LOGGER.debug(() -> String.format("Cached chain tip %.8s (reference %.8s) was not parent for new latest block %.8s",
+ Base58.encode(cachedChainTip.getSignature()),
+ Base58.encode(cachedChainTip.getReference()),
+ Base58.encode(blockDataCopy.getSignature())));
+
+ // Defensively rebuild cache
+ refillNeeded = true;
+ }
+
+ if (refillNeeded)
+ try {
+ this.stats.latestBlocksCacheRefills.incrementAndGet();
+
+ this.refillLatestBlocksCache();
+ } catch (DataException e) {
+ LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
+ }
+ }
+
+ this.onNewOrOrphanedBlock(blockDataCopy, OrphanedBlockEvent::new);
+ }
+
+ private void onNewOrOrphanedBlock(BlockData blockDataCopy, Function eventConstructor) {
requestSysTrayUpdate = true;
// Notify listeners, trade-bot, etc.
- EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
+ EventBus.INSTANCE.notify(eventConstructor.apply(blockDataCopy));
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;
@@ -935,11 +1125,31 @@ public class Controller extends Thread {
private void onNetworkGetBlockMessage(Peer peer, Message message) {
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
byte[] signature = getBlockMessage.getSignature();
+ this.stats.getBlockMessageStats.requests.incrementAndGet();
+
+ ByteArray signatureAsByteArray = new ByteArray(signature);
+
+ BlockMessage cachedBlockMessage = this.blockMessageCache.get(signatureAsByteArray);
+
+ // Check cached latest block message
+ if (cachedBlockMessage != null) {
+ this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
+
+ // We need to duplicate it to prevent multiple threads setting ID on the same message
+ BlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
+
+ if (!peer.sendMessage(clonedBlockMessage))
+ peer.disconnect("failed to send block");
+
+ return;
+ }
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
+
if (blockData == null) {
// We don't have this block
+ this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
@@ -954,10 +1164,19 @@ public class Controller extends Thread {
Block block = new Block(repository, blockData);
- Message blockMessage = new BlockMessage(block);
+ BlockMessage blockMessage = new BlockMessage(block);
blockMessage.setId(message.getId());
+
+ // This call also causes the other needed data to be pulled in from repository
if (!peer.sendMessage(blockMessage))
peer.disconnect("failed to send block");
+
+ // If request is for a recent block, cache it
+ if (getChainHeight() - blockData.getHeight() <= BLOCK_CACHE_SIZE) {
+ this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
+
+ this.blockMessageCache.put(new ByteArray(blockData.getSignature()), blockMessage);
+ }
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
}
@@ -1004,59 +1223,110 @@ public class Controller extends Thread {
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
- byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
+ final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
+ this.stats.getBlockSummariesStats.requests.incrementAndGet();
- try (final Repository repository = RepositoryManager.getRepository()) {
- List blockSummaries = new ArrayList<>();
-
- int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
-
- do {
- BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
-
- if (blockData == null)
- // No more blocks to send to peer
- break;
-
- BlockSummaryData blockSummary = new BlockSummaryData(blockData);
- blockSummaries.add(blockSummary);
- parentSignature = blockData.getSignature();
- } while (blockSummaries.size() < numberRequested);
-
- Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
+ // If peer's parent signature matches our latest block signature
+ // then we can short-circuit with an empty response
+ BlockData chainTip = getChainTip();
+ if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
+ Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
- } catch (DataException e) {
- LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
+
+ return;
}
+
+ List blockSummaries = new ArrayList<>();
+
+ // Attempt to serve from our cache of latest blocks
+ synchronized (this.latestBlocks) {
+ blockSummaries = this.latestBlocks.stream()
+ .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
+ .map(BlockSummaryData::new)
+ .collect(Collectors.toList());
+ }
+
+ if (blockSummaries.isEmpty()) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
+
+ BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
+
+ while (blockData != null && blockSummaries.size() < numberRequested) {
+ BlockSummaryData blockSummary = new BlockSummaryData(blockData);
+ blockSummaries.add(blockSummary);
+
+ blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
+ }
+ } catch (DataException e) {
+ LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
+ }
+ } else {
+ this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
+
+ if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
+ this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
+ }
+
+ Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
+ blockSummariesMessage.setId(message.getId());
+ if (!peer.sendMessage(blockSummariesMessage))
+ peer.disconnect("failed to send block summaries");
}
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
- byte[] parentSignature = getSignaturesMessage.getParentSignature();
+ final byte[] parentSignature = getSignaturesMessage.getParentSignature();
+ this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
- try (final Repository repository = RepositoryManager.getRepository()) {
- List signatures = new ArrayList<>();
-
- do {
- BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
-
- if (blockData == null)
- // No more signatures to send to peer
- break;
-
- parentSignature = blockData.getSignature();
- signatures.add(parentSignature);
- } while (signatures.size() < getSignaturesMessage.getNumberRequested());
-
- Message signaturesMessage = new SignaturesMessage(signatures);
+ // If peer's parent signature matches our latest block signature
+ // then we can short-circuit with an empty response
+ BlockData chainTip = getChainTip();
+ if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
+ Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
- } catch (DataException e) {
- LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
+
+ return;
}
+
+ List signatures = new ArrayList<>();
+
+ // Attempt to serve from our cache of latest blocks
+ synchronized (this.latestBlocks) {
+ signatures = this.latestBlocks.stream()
+ .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
+ .map(BlockData::getSignature)
+ .collect(Collectors.toList());
+ }
+
+ if (signatures.isEmpty()) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ int numberRequested = getSignaturesMessage.getNumberRequested();
+ BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
+
+ while (blockData != null && signatures.size() < numberRequested) {
+ signatures.add(blockData.getSignature());
+
+ blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
+ }
+ } catch (DataException e) {
+ LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
+ }
+ } else {
+ this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
+
+ if (signatures.size() >= getSignaturesMessage.getNumberRequested())
+ this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
+ }
+
+ Message signaturesMessage = new SignaturesMessage(signatures);
+ signaturesMessage.setId(message.getId());
+ if (!peer.sendMessage(signaturesMessage))
+ peer.disconnect("failed to send signatures (v2)");
}
private void onNetworkHeightV2Message(Peer peer, Message message) {
@@ -1406,26 +1676,6 @@ public class Controller extends Thread {
// Refresh our online accounts signatures?
sendOurOnlineAccountsInfo();
-
- // Trim blockchain by removing 'old' online accounts signatures
- long upperMintedTimestamp = now - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
- trimOldOnlineAccountsSignatures(upperMintedTimestamp);
- }
-
- private void trimOldOnlineAccountsSignatures(long upperMintedTimestamp) {
- try (final Repository repository = RepositoryManager.tryRepository()) {
- if (repository == null)
- return;
-
- int numBlocksTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(upperMintedTimestamp);
-
- if (numBlocksTrimmed > 0)
- LOGGER.debug(() -> String.format("Trimmed old online accounts signatures from %d block%s", numBlocksTrimmed, (numBlocksTrimmed != 1 ? "s" : "")));
-
- repository.saveChanges();
- } catch (DataException e) {
- LOGGER.warn(String.format("Repository issue trying to trim old online accounts signatures: %s", e.getMessage()));
- }
}
private void sendOurOnlineAccountsInfo() {
@@ -1687,4 +1937,8 @@ public class Controller extends Thread {
return now - offset;
}
+ public StatsSnapshot getStatsSnapshot() {
+ return this.stats;
+ }
+
}
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java b/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java
new file mode 100644
index 00000000..e9b846fc
--- /dev/null
+++ b/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java
@@ -0,0 +1,75 @@
+package org.qortal.controller;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.block.BlockChain;
+import org.qortal.data.block.BlockData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+import org.qortal.utils.NTP;
+
+public class OnlineAccountsSignaturesTrimmer implements Runnable {
+
+ private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsSignaturesTrimmer.class);
+
+ private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
+
+ public void run() {
+ Thread.currentThread().setName("Online Accounts trimmer");
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Don't even start trimming until initial rush has ended
+ Thread.sleep(INITIAL_SLEEP_PERIOD);
+
+ while (!Controller.isStopping()) {
+ repository.discardChanges();
+
+ Thread.sleep(Settings.getInstance().getOnlineSignaturesTrimInterval());
+
+ BlockData chainTip = Controller.getInstance().getChainTip();
+ if (chainTip == null || NTP.getTime() == null)
+ continue;
+
+ // Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
+ if (Controller.getInstance().isSynchronizing())
+ continue;
+
+ // Trim blockchain by removing 'old' online accounts signatures
+ long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
+ int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
+
+ int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
+
+ int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
+ int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
+
+ if (trimStartHeight >= upperTrimHeight)
+ continue;
+
+ int numSigsTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(trimStartHeight, upperTrimHeight);
+ repository.saveChanges();
+
+ if (numSigsTrimmed > 0) {
+ LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
+ numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
+ trimStartHeight, upperTrimHeight));
+ } else {
+ // Can we move onto next batch?
+ if (upperTrimmableHeight > upperBatchHeight) {
+ repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
+ repository.saveChanges();
+
+ LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
+ }
+ }
+ }
+ } catch (DataException e) {
+ LOGGER.warn(String.format("Repository issue trying to trim online accounts signatures: %s", e.getMessage()));
+ } catch (InterruptedException e) {
+ // Time to exit
+ }
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 8dca5b05..06850a1b 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -175,7 +175,7 @@ public class Synchronizer {
* @throws DataException
* @throws InterruptedException
*/
- private SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List blockSummariesFromCommon) throws DataException, InterruptedException {
+ public SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List blockSummariesFromCommon) throws DataException, InterruptedException {
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
// Failing that, back off exponentially
int step = INITIAL_BLOCK_STEP;
@@ -313,18 +313,21 @@ public class Synchronizer {
List ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
// Populate minter account levels for both lists of block summaries
- populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
+ populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
+
+ final int mutualHeight = commonBlockHeight - 1 + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
- BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
- BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
+ BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
+ BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
+
+ NumberFormat formatter = 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)));
// If our blockchain has greater weight then don't synchronize with peer
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
- NumberFormat formatter = 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)));
return SynchronizationResult.INFERIOR_CHAIN;
}
}
@@ -405,12 +408,15 @@ public class Synchronizer {
Block block = new Block(repository, orphanBlockData);
block.orphan();
+ LOGGER.trace(String.format("Orphaned block height %d, sig %.8s", ourHeight, Base58.encode(orphanBlockData.getSignature())));
+
repository.saveChanges();
--ourHeight;
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
- Controller.getInstance().onNewBlock(orphanBlockData);
+ repository.discardChanges(); // clear transaction status to prevent deadlocks
+ Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
@@ -431,6 +437,8 @@ public class Synchronizer {
newBlock.process();
+ LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
+
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
@@ -513,6 +521,8 @@ public class Synchronizer {
newBlock.process();
+ LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
+
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
diff --git a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
index ba173f43..d708f91c 100644
--- a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
+++ b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
@@ -610,38 +610,26 @@ public class BitcoinACCTv1 implements ACCT {
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
- return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
- byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
- return populateTradeData(repository, creatorPublicKey, atStateData);
+ ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
- public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
+ public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
@@ -649,7 +637,7 @@ public class BitcoinACCTv1 implements ACCT {
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
- tradeData.creationTimestamp = atStateData.getCreation();
+ tradeData.creationTimestamp = creationTimestamp;
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
diff --git a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
index 04f8fa72..082c78e9 100644
--- a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
+++ b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
@@ -561,38 +561,26 @@ public class LitecoinACCTv1 implements ACCT {
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
- return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
- byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
- return populateTradeData(repository, creatorPublicKey, atStateData);
+ ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
- *
- * @param repository
- * @param atAddress
- * @throws DataException
*/
- public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
+ public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
@@ -600,7 +588,7 @@ public class LitecoinACCTv1 implements ACCT {
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
- tradeData.creationTimestamp = atStateData.getCreation();
+ tradeData.creationTimestamp = creationTimestamp;
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
diff --git a/src/main/java/org/qortal/crypto/Crypto.java b/src/main/java/org/qortal/crypto/Crypto.java
index a21ac594..49cdd2fb 100644
--- a/src/main/java/org/qortal/crypto/Crypto.java
+++ b/src/main/java/org/qortal/crypto/Crypto.java
@@ -1,5 +1,6 @@
package org.qortal.crypto;
+import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -42,6 +43,27 @@ public abstract class Crypto {
}
}
+ /**
+ * Returns 32-byte SHA-256 digest of message passed in input.
+ *
+ * @param input
+ * variable-length byte[] message
+ * @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
+ */
+ public static byte[] digest(ByteBuffer input) {
+ if (input == null)
+ return null;
+
+ try {
+ // SHA2-256
+ MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ sha256.update(input);
+ return sha256.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("SHA-256 message digest not available");
+ }
+ }
+
/**
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
*
diff --git a/src/main/java/org/qortal/data/at/ATStateData.java b/src/main/java/org/qortal/data/at/ATStateData.java
index b8c13e0d..e689f5ae 100644
--- a/src/main/java/org/qortal/data/at/ATStateData.java
+++ b/src/main/java/org/qortal/data/at/ATStateData.java
@@ -5,7 +5,6 @@ public class ATStateData {
// Properties
private String ATAddress;
private Integer height;
- private Long creation;
private byte[] stateData;
private byte[] stateHash;
private Long fees;
@@ -14,10 +13,9 @@ public class ATStateData {
// Constructors
/** Create new ATStateData */
- public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
+ public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
this.ATAddress = ATAddress;
this.height = height;
- this.creation = creation;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
@@ -26,21 +24,21 @@ public class ATStateData {
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
- this(ATAddress, height, null, null, stateHash, fees, isInitial);
+ this(ATAddress, height, null, stateHash, fees, isInitial);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
- this(ATAddress, null, null, null, stateHash, null, false);
+ this(ATAddress, null, null, stateHash, null, false);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
- this(ATAddress, null, null, null, stateHash, fees, false);
+ this(ATAddress, null, null, stateHash, fees, false);
}
// Getters / setters
@@ -58,10 +56,6 @@ public class ATStateData {
this.height = height;
}
- public Long getCreation() {
- return this.creation;
- }
-
public byte[] getStateData() {
return this.stateData;
}
diff --git a/src/main/java/org/qortal/event/EventBus.java b/src/main/java/org/qortal/event/EventBus.java
index e0014a20..63c80143 100644
--- a/src/main/java/org/qortal/event/EventBus.java
+++ b/src/main/java/org/qortal/event/EventBus.java
@@ -20,6 +20,21 @@ public enum EventBus {
}
}
+ /**
+ * WARNING: before calling this method,
+ * make sure repository holds no locks, e.g. by calling
+ * repository.discardChanges().
+ *
+ * This is because event listeners might open a new
+ * repository session which will deadlock HSQLDB
+ * if it tries to CHECKPOINT.
+ *
+ * The HSQLDB deadlock occurs because the caller's
+ * repository session blocks the CHECKPOINT until
+ * their transaction is closed, yet event listeners
+ * new sessions are blocked until CHECKPOINT is
+ * completed, hence deadlock.
+ */
public void notify(Event event) {
List clonedListeners;
diff --git a/src/main/java/org/qortal/globalization/Translator.java b/src/main/java/org/qortal/globalization/Translator.java
index 8f0b6136..6481dde7 100644
--- a/src/main/java/org/qortal/globalization/Translator.java
+++ b/src/main/java/org/qortal/globalization/Translator.java
@@ -10,12 +10,12 @@ import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.qortal.settings.Settings;
public enum Translator {
INSTANCE;
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
- private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private static final Map resourceBundles = new HashMap<>();
@@ -34,7 +34,7 @@ public enum Translator {
}
public String translate(String className, String key) {
- return this.translate(className, DEFAULT_LANG, key);
+ return this.translate(className, Settings.getInstance().getLocaleLang(), key);
}
public Set keySet(String className, String lang) {
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index 9c8ce8a8..84f29ac9 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -33,6 +33,7 @@ import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
+import com.google.common.hash.HashCode;
import com.google.common.net.HostAndPort;
import com.google.common.net.InetAddresses;
@@ -348,21 +349,37 @@ public class Peer {
if (this.byteBuffer == null)
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
+ final int priorPosition = this.byteBuffer.position();
final int bytesRead = this.socketChannel.read(this.byteBuffer);
if (bytesRead == -1) {
this.disconnect("EOF");
return;
}
- LOGGER.trace(() -> String.format("Received %d bytes from peer %s", bytesRead, this));
+ LOGGER.trace(() -> {
+ if (bytesRead > 0) {
+ byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
+ this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
+ String leadingHex = HashCode.fromBytes(leadingBytes).toString();
+
+ return String.format("Received %d bytes, starting %s, into byteBuffer[%d] from peer %s",
+ bytesRead,
+ leadingHex,
+ priorPosition,
+ this);
+ } else {
+ return String.format("Received %d bytes into byteBuffer[%d] from peer %s", bytesRead, priorPosition, this);
+ }
+ });
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
while (true) {
final Message message;
// Can we build a message from buffer now?
+ ByteBuffer readOnlyBuffer = this.byteBuffer.asReadOnlyBuffer().flip();
try {
- message = Message.fromByteBuffer(this.byteBuffer);
+ message = Message.fromByteBuffer(readOnlyBuffer);
} catch (MessageException e) {
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
this.disconnect(e.getMessage());
@@ -387,6 +404,13 @@ public class Peer {
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
+ // Tidy up buffers:
+ this.byteBuffer.flip();
+ // Read-only, flipped buffer's position will be after end of message, so copy that
+ this.byteBuffer.position(readOnlyBuffer.position());
+ // Copy bytes after read message to front of buffer, adjusting position accordingly, reset limit to capacity
+ this.byteBuffer.compact();
+
BlockingQueue queue = this.replyQueues.get(message.getId());
if (queue != null) {
// Adding message to queue will unblock thread waiting for response
@@ -399,7 +423,7 @@ public class Peer {
// Add message to pending queue
if (!this.pendingMessages.offer(message)) {
- LOGGER.info(String.format("No room to queue message from peer %s - discarding", this));
+ LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
return;
}
@@ -454,10 +478,24 @@ public class Peer {
while (outputBuffer.hasRemaining()) {
int bytesWritten = this.socketChannel.write(outputBuffer);
+ LOGGER.trace(() -> String.format("Sent %d bytes of %s message with ID %d to peer %s",
+ bytesWritten,
+ message.getType().name(),
+ message.getId(),
+ this));
+
if (bytesWritten == 0)
// Underlying socket's internal buffer probably full,
// so wait a short while for bytes to actually be transmitted over the wire
- this.socketChannel.wait(1L);
+
+ /*
+ * NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
+ * as this releases the lock held by synchronized() above
+ * and would allow another thread to send another message,
+ * potentially interleaving them on-the-wire, causing checksum failures
+ * and connection loss.
+ */
+ Thread.sleep(1L); //NOSONAR squid:S2276
}
}
} catch (MessageException e) {
diff --git a/src/main/java/org/qortal/network/message/BlockMessage.java b/src/main/java/org/qortal/network/message/BlockMessage.java
index 8ca86ee6..b07dc8b1 100644
--- a/src/main/java/org/qortal/network/message/BlockMessage.java
+++ b/src/main/java/org/qortal/network/message/BlockMessage.java
@@ -34,6 +34,7 @@ public class BlockMessage extends Message {
super(MessageType.BLOCK);
this.block = block;
+ this.blockData = block.getBlockData();
this.height = block.getBlockData().getHeight();
}
@@ -93,4 +94,10 @@ public class BlockMessage extends Message {
}
}
+ public BlockMessage cloneWithNewId(int newId) {
+ BlockMessage clone = new BlockMessage(this.block);
+ clone.setId(newId);
+ return clone;
+ }
+
}
diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java
index 9dfdc6bc..cc90fe81 100644
--- a/src/main/java/org/qortal/network/message/Message.java
+++ b/src/main/java/org/qortal/network/message/Message.java
@@ -160,80 +160,72 @@ public abstract class Message {
/**
* Attempt to read a message from byte buffer.
*
- * @param byteBuffer
+ * @param readOnlyBuffer
* @return null if no complete message can be read
* @throws MessageException
*/
- public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
+ public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
try {
- byteBuffer.flip();
-
- ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
-
// Read only enough bytes to cover Message "magic" preamble
byte[] messageMagic = new byte[MAGIC_LENGTH];
- readBuffer.get(messageMagic);
+ readOnlyBuffer.get(messageMagic);
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
// Didn't receive correct Message "magic"
throw new MessageException("Received incorrect message 'magic'");
// Find supporting object
- int typeValue = readBuffer.getInt();
+ int typeValue = readOnlyBuffer.getInt();
MessageType messageType = MessageType.valueOf(typeValue);
if (messageType == null)
// Unrecognised message type
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
// Optional message ID
- byte hasId = readBuffer.get();
+ byte hasId = readOnlyBuffer.get();
int id = -1;
if (hasId != 0) {
- id = readBuffer.getInt();
+ id = readOnlyBuffer.getInt();
if (id <= 0)
// Invalid ID
throw new MessageException("Invalid negative ID");
}
- int dataSize = readBuffer.getInt();
+ int dataSize = readOnlyBuffer.getInt();
if (dataSize > MAX_DATA_SIZE)
// Too large
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
+ // Don't have all the data yet?
+ if (dataSize > 0 && dataSize + CHECKSUM_LENGTH > readOnlyBuffer.remaining())
+ return null;
+
ByteBuffer dataSlice = null;
if (dataSize > 0) {
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
- readBuffer.get(expectedChecksum);
+ readOnlyBuffer.get(expectedChecksum);
- // Remember this position in readBuffer so we can pass to Message subclass
- dataSlice = readBuffer.slice();
-
- // Consume data from buffer
- byte[] data = new byte[dataSize];
- readBuffer.get(data);
-
- // We successfully read all the data bytes, so we can set limit on dataSlice
+ // Slice data in readBuffer so we can pass to Message subclass
+ dataSlice = readOnlyBuffer.slice();
dataSlice.limit(dataSize);
// Test checksum
- byte[] actualChecksum = generateChecksum(data);
+ byte[] actualChecksum = generateChecksum(dataSlice);
if (!Arrays.equals(expectedChecksum, actualChecksum))
throw new MessageException("Message checksum incorrect");
+
+ // Reset position after being consumed by generateChecksum
+ dataSlice.position(0);
+ // Update position in readOnlyBuffer
+ readOnlyBuffer.position(readOnlyBuffer.position() + dataSize);
}
- Message message = messageType.fromByteBuffer(id, dataSlice);
-
- // We successfully read a message, so bump byteBuffer's position to reflect this
- byteBuffer.position(readBuffer.position());
-
- return message;
+ return messageType.fromByteBuffer(id, dataSlice);
} catch (BufferUnderflowException e) {
// Not enough bytes to fully decode message...
return null;
- } finally {
- byteBuffer.compact();
}
}
@@ -241,6 +233,10 @@ public abstract class Message {
return Arrays.copyOfRange(Crypto.digest(data), 0, CHECKSUM_LENGTH);
}
+ protected static byte[] generateChecksum(ByteBuffer dataBuffer) {
+ return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
+ }
+
public byte[] toBytes() throws MessageException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java
index f3c2b16d..b21a4909 100644
--- a/src/main/java/org/qortal/repository/ATRepository.java
+++ b/src/main/java/org/qortal/repository/ATRepository.java
@@ -87,6 +87,21 @@ public interface ATRepository {
*/
public List getBlockATStatesAtHeight(int height) throws DataException;
+ /** Returns height of first trimmable AT state. */
+ public int getAtTrimHeight() throws DataException;
+
+ /** Sets new base height for AT state trimming.
+ *
+ * NOTE: performs implicit repository.saveChanges().
+ */
+ public void setAtTrimHeight(int trimHeight) throws DataException;
+
+ /** Hook to allow repository to prepare/cache info for AT state trimming. */
+ public void prepareForAtStateTrimming() throws DataException;
+
+ /** Trims full AT state data between passed heights. Returns number of trimmed rows. */
+ public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException;
+
/**
* Save ATStateData into repository.
*
diff --git a/src/main/java/org/qortal/repository/BlockRepository.java b/src/main/java/org/qortal/repository/BlockRepository.java
index 4265b71f..937607cf 100644
--- a/src/main/java/org/qortal/repository/BlockRepository.java
+++ b/src/main/java/org/qortal/repository/BlockRepository.java
@@ -143,13 +143,21 @@ public interface BlockRepository {
*/
public List getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
+ /** Returns height of first trimmable online accounts signatures. */
+ public int getOnlineAccountsSignaturesTrimHeight() throws DataException;
+
+ /** Sets new base height for trimming online accounts signatures.
+ *
+ * NOTE: performs implicit repository.saveChanges().
+ */
+ public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException;
+
/**
- * Trim online accounts signatures from blocks older than passed timestamp.
+ * Trim online accounts signatures from blocks between passed heights.
*
- * @param timestamp
* @return number of blocks trimmed
*/
- public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
+ public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException;
/**
* Returns first (lowest height) block that doesn't link back to specified block.
diff --git a/src/main/java/org/qortal/repository/Repository.java b/src/main/java/org/qortal/repository/Repository.java
index cc3a5336..527b23f3 100644
--- a/src/main/java/org/qortal/repository/Repository.java
+++ b/src/main/java/org/qortal/repository/Repository.java
@@ -47,6 +47,12 @@ public interface Repository extends AutoCloseable {
public void backup(boolean quick) throws DataException;
+ public void checkpoint(boolean quick) throws DataException;
+
public void performPeriodicMaintenance() throws DataException;
+ public void exportNodeLocalData() throws DataException;
+
+ public void importDataFromFile(String filename) throws DataException;
+
}
diff --git a/src/main/java/org/qortal/repository/RepositoryFactory.java b/src/main/java/org/qortal/repository/RepositoryFactory.java
index 94c9627e..e5b29d1b 100644
--- a/src/main/java/org/qortal/repository/RepositoryFactory.java
+++ b/src/main/java/org/qortal/repository/RepositoryFactory.java
@@ -2,6 +2,8 @@ package org.qortal.repository;
public interface RepositoryFactory {
+ public boolean wasPristineAtOpen();
+
public RepositoryFactory reopen() throws DataException;
public Repository getRepository() throws DataException;
diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java
index e375be96..9f5cf239 100644
--- a/src/main/java/org/qortal/repository/RepositoryManager.java
+++ b/src/main/java/org/qortal/repository/RepositoryManager.java
@@ -8,6 +8,13 @@ public abstract class RepositoryManager {
repositoryFactory = newRepositoryFactory;
}
+ public static boolean wasPristineAtOpen() throws DataException {
+ if (repositoryFactory == null)
+ throw new DataException("No repository available");
+
+ return repositoryFactory.wasPristineAtOpen();
+ }
+
public static Repository getRepository() throws DataException {
if (repositoryFactory == null)
throw new DataException("No repository available");
@@ -35,6 +42,14 @@ public abstract class RepositoryManager {
}
}
+ public static void checkpoint(boolean quick) {
+ try (final Repository repository = getRepository()) {
+ repository.checkpoint(quick);
+ } catch (DataException e) {
+ // Checkpoint is best-effort so don't complain
+ }
+ }
+
public static void rebuild() throws DataException {
RepositoryFactory oldRepositoryFactory = repositoryFactory;
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
index f8b6ce4d..82af283b 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
@@ -50,7 +50,7 @@ public class HSQLDBATRepository implements ATRepository {
boolean hadFatalError = resultSet.getBoolean(10);
boolean isFrozen = resultSet.getBoolean(11);
- Long frozenBalance = resultSet.getLong(11);
+ Long frozenBalance = resultSet.getLong(12);
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
@@ -118,7 +118,7 @@ public class HSQLDBATRepository implements ATRepository {
boolean hadFatalError = resultSet.getBoolean(10);
boolean isFrozen = resultSet.getBoolean(11);
- Long frozenBalance = resultSet.getLong(11);
+ Long frozenBalance = resultSet.getLong(12);
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
@@ -147,7 +147,7 @@ public class HSQLDBATRepository implements ATRepository {
bindParams.add(codeHash);
if (isExecutable != null) {
- sql.append("AND is_finished = ? ");
+ sql.append("AND is_finished != ? ");
bindParams.add(isExecutable);
}
@@ -248,22 +248,22 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
- String sql = "SELECT created_when, state_data, state_hash, fees, is_initial "
+ String sql = "SELECT state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
- + "WHERE AT_address = ? AND height = ? "
+ + "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ + "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
if (resultSet == null)
return null;
- long created = resultSet.getLong(1);
- byte[] stateData = resultSet.getBytes(2); // Actually BLOB
- byte[] stateHash = resultSet.getBytes(3);
- long fees = resultSet.getLong(4);
- boolean isInitial = resultSet.getBoolean(5);
+ byte[] stateData = resultSet.getBytes(1); // Actually BLOB
+ byte[] stateHash = resultSet.getBytes(2);
+ long fees = resultSet.getLong(3);
+ boolean isInitial = resultSet.getBoolean(4);
- return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
+ return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e);
}
@@ -271,12 +271,13 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getLatestATState(String atAddress) throws DataException {
- String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial "
+ String sql = "SELECT height, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
- + "WHERE AT_address = ? "
- // AT_address then height so the compound primary key is used as an index
- // Both must be the same direction also
- + "ORDER BY AT_address DESC, height DESC "
+ + "JOIN ATStatesData USING (AT_address, height) "
+ + "WHERE ATStates.AT_address = ? "
+ // Order by AT_address and height to use compound primary key as index
+ // Both must be the same direction (DESC) also
+ + "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 ";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
@@ -284,13 +285,12 @@ public class HSQLDBATRepository implements ATRepository {
return null;
int height = resultSet.getInt(1);
- long created = resultSet.getLong(2);
- byte[] stateData = resultSet.getBytes(3); // Actually BLOB
- byte[] stateHash = resultSet.getBytes(4);
- long fees = resultSet.getLong(5);
- boolean isInitial = resultSet.getBoolean(6);
+ byte[] stateData = resultSet.getBytes(2); // Actually BLOB
+ byte[] stateHash = resultSet.getBytes(3);
+ long fees = resultSet.getLong(4);
+ boolean isInitial = resultSet.getBoolean(5);
- return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
+ return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e);
}
@@ -303,21 +303,22 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024);
List